https://pixabay.com
https://pixabay.com

Вы когда-нибудь задавались вопросом: “Какая польза от yield в PHP?”. Позвольте мне избавить вас от поиска в Google; Я с удовольствием раскрою вам пару ключевых моментов о yield:

  1. Что такое yield.

  2. Различия между yield и return.

  3. Варианты использования yield.

  4. Заключение.

  5. Ссылки.

1. Что такое “yield”

Функция-генератор выглядит так же, как и обычная функция, за исключением того, что вместо всего лишь одного значения генератор вырабатывает (yields) столько значений, сколько ему нужно.

Взгляните на следующий пример:

function getValues() {
   yield 'value';
}
// вывод строки "value"
echo getValues();

Конечно, это не будет работать. Предыдущий пример выдаст ошибку: Object of class Generator could not be converted to string. Позвольте мне объяснить почему:

2. Различия между “yield” и “return”

Полученная ошибка говорит нам о том, что функция getValues​​() не возвращает строку, как мы могли бы этого ожидать. Давайте проверим ее тип:

function getValues() {
   return 'value';
}
var_dump(getValues()); // string(5) "value"
function getValues() {
   yield 'value';
}
var_dump(getValues()); // class Gene(0) {}rator#1 

Класс Generator реализует интерфейс Iterator, поэтому для получения значений вам необходимо проитерировать по результатам функции getValue():

foreach (getValues() as $value) {
  echo $value;
}
// можно также сделать это через переменную
$values = getValues();
foreach ($values as $value) {
  echo $value;
}

Но различия на этом не заканчиваются!

Генератор позволяет вам писать код с использованием оператора foreach для итерации по набору данных без необходимости создавать в памяти массив, что, однако, может приводить к превышению лимита памяти.

В следующем примере мы создадим массив из 800000 элементов и вернем его из функции getValues​​(), контролируя память, выделенную для этого фрагмента кода, с помощью функции memory_get_usage(). Мы будем запрашивать потребление памяти через каждые 200000 добавленных элементов, что означает, что контрольных точек будет четыре:

<?php
function getValues() {
   $valuesArray = [];
   // get the initial memory usage
   echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
   for ($i = 1; $i < 800000; $i++) {
      $valuesArray[] = $i;

      // let us do profiling, so we measure the memory usage
      if (($i % 200000) == 0) {
         // get memory usage in megabytes
         echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL;
      }
   }
   return $valuesArray;
}

$myValues = getValues(); // building the array here once we call the function
foreach ($myValues as $value) {}

Вот такие результаты потребления памяти мы получили для примера, приведенного выше:

0.34 MB
8.35 MB
16.35 MB
32.35 MB

Несколько строк нашего кода потребляют более 30 мегабайт памяти. Каждый раз, когда мы добавляем элемент в массив $valuesArray, мы увеличиваем его размер в памяти.

Давайте рассмотрим тот же пример, только с использованием yield:

<?php
function getValues() {
   // get the initial memory usage
   echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB' . PHP_EOL;
   for ($i = 1; $i < 800000; $i++) {
      yield $i;

      // let us do profiling, so we measure the memory usage
      if (($i % 200000) == 0) {
         // get memory usage in megabytes
         echo round(memory_get_usage() / 1024 / 1024, 2) . ' MB'. PHP_EOL;
      }
   }
}

$myValues = getValues(); // no action taken until we loop over the values
foreach ($myValues as $value) {} // start generating values here

Результат для этого варианта кода может вас поразить:

0.34 MB
0.34 MB
0.34 MB
0.34 MB

Конечно это не означает, что вам нужно повсеместно переходить от return к yield, но если в своем приложении вы создаете огромные массивы, которые могут вызывать проблемы с памятью на сервере, yield однозначно будет решением вашей проблемы.

3. Варианты использования “yield”

Существует много вариантов использования yield, но я выделю пару из них:

  • a. Используя yield, вы также можете использовать return:

function getValues() {
  yield 'value';
  return 'returnValue';
}
$values = getValues();
foreach ($values as $value) {}
echo $values->getReturn(); // 'returnValue'
  • b. Возврат пар ключ-значение:

function getValues() {
  yield 'key' => 'value';
}
$values = getValues();
foreach ($values as $key => $value) {
  echo $key . ' => ' . $value;
}

Подробнее об этом можно почитать здесь.

4. Заключение

Основная цель этой статьи - показать на примерах, в чем разница между yield и return в контексте потребления памяти. По моему мнению, это очень важно знать каждому разработчику.

5. Ссылки

  1. http://php.net/manual/en/language.generators.syntax.php

  2. http://php.net/manual/en/class.generator.php

  3. http://php.net/manual/en/language.generators.php

  4. http://php.net/manual/en/function.memory-get-usage.php


Данная статья переведена в преддверии старта курса PHP Developer. Basic. Узнать подробнее о курсе можно по ссылке.

Комментарии (5)


  1. mSnus
    29.12.2021 14:25
    +8

    Я что-то не понимаю, там сравнивается хранение в памяти массива из 800.000 элементов и одной переменной? Тёплое более широкое, чем мягкое?


  1. FanatPHP
    29.12.2021 14:40
    +17

    Очередная ересь. Чего ещё ожидать от otus, с их эталонным говнокодом, который они не первый год преподают на своих "курсах".

    Это какая-то удивительно живучая байка. Кого не спроси - каждый скажет, что "генераторы экономят память!". При том что генератор - это всего лишь синтаксический сахар.

    Память экономит не генератор. А цикл, который выдаёт по одному значению.

    В данном примере - это цикл for.

    А генератор всего лишь позволяет замаскировать обращение к этому циклу под перебор массива. В программировании это называется синтаксическим сахаром, а не экономией памяти. Генератор позволяет писать более красивый код, генератор позволяет унифицировать интерфейсы. Но вот что он не делает - так это не экономит память.

    если в своем приложении вы создаете огромные массивы, которые могут вызывать проблемы с памятью на сервере, yield однозначно будет решением вашей проблемы

    Вот так приходит человек получить "цифровые навыки от ведущих экспертов", а получает вот такую ерунду. Я понимаю, что автор статьи намеренно передёргивает, чтобы собрать побольше кликов. Но на Хабр-то зачем это тащить?

    "Если вы в своем приложении создаете огромные массивы", то генератор вам не поможет ни капли. Потому что во-первых, массив и последовательность чисел - это разные вещи. Второе - это всего лишь ничтожное подмножество первого, которое практически не встречается в реальной жизни. В то время как с реальным массивом, хранящим не просто цыферки, а какую-то осмысленную информацию, и к которому ещё и требуется произвольный доступ, генераторы помогут от слова "никак".

    А во-вторых, как уже говорилось выше, в реальности нам помогает соответствующий цикл - либо for, как в примере из статьи, либо while, который получает по одной строчке из внешнего источника данных - файла или БД. Вот они-то как раз и будут экономить память. А генератор всего лишь позволит упаковать их в красивую оберточку.


  1. borshak
    29.12.2021 14:44
    +9

    Понятно, что перевод, но непонятно, зачем так все усложнять?..

    yield используется для возврата значений промежуточных вычислений в функциях-генераторах.

    Генератор - функция с хранимым состоянием. Генератор выполняется до следующего слова yield в коде, где выбрасывает рассчитанное значение значение наружу и "засыпает", ожидая следующего вызова, чтобы потом продолжить с прерванной точки. С помощью генератора можно реализовать поддежку [эффективных по помяти] бесконечных последовательностей, по типу ряда Фибоначчи или гармонических рядов. Генераторы также называют "сопрограммами".

    С помощью генераторов реализуют протокол итерации, который используется для удобного обхода коллекций через стандартные механизмы языка (как правило, через цикл for). Поддержка протокола итераций реализована во многих языках, как то Python, JavaScript, C#, Rust.

    Использование слова return в генераторе полностью останавливает его, после чего генератор нельзя уже будет пробудить повтрно.

    Ну а потом уже можно и пример с большим массивом. А то прям какой-то детектив написан, о каких-то [мнимых] различиях совершенно разных конструкций - да еще и в стиле секрета Полишинеля...


  1. MyraJKee
    01.01.2022 00:41

    А кто-нибудь может привести реальный пример использования где yield полезен? И при этом был бы предпочтительнее других реализаций?

    Понятно что если сделать вот так вот как в статье, то можно сэкономить памяти, но в реальности у меня ещё не было случаев генерации на лету огромных массивов...


    1. FanatPHP
      01.01.2022 04:16

      Я писал выше, что память экономит не генератор, а цикл

      foreach c генератором - это всего лишь красивая обёртка для цикла

      for ($i = 1; $i < 800000; $i++) {
          echo $i;
      }

      Всю работу делает этот цикл. Генератор просто позволяет "вывернуть его наизнанку", и обращаться к получаемым в цикле значениям не внутри, а "снаружи".

      Отсюда и практическое применение генератора: оно не в "экономии памяти", а в использовании foreach. Везде, где foreach удобнее while или for, генератор будет предпочтительнее. Он позволяет писать более красивый, и - главное - более универсальный код. К примеру мы пишем обработчик каких-то данных, причем источник этих данных может быть совершенно разным - это может быть массив, или текстовый файл, или база данных.

      Без генератора нам придется либо вторые два источника приводить к массиву, тратя память, либо дублировать код три раза, поскольку мы должны будем поместить его внутри цикла.

      А генератор позволит обращаться ко всем трем источникам через foreach, то есть код можно будет написать только один раз.