Что делает оператор yield php
Генератор предоставляет функцию, которая генерирует набор значений.
Для возвращения значения из функции применяется оператор yield . Но в отличие от return оператор yield сохраняет состояние функции, позволяя ей продолжать работу с того места, когда остановилось ее выполнение.
Например, определим простейшую функцию генератора:
function generator()
Здесь функция генератора фактически возвращает только одно число 21. Тем не менее мы можем перебирать результат функции генератора в цикле как стандартный массив:
foreach(generator() as $number) < echo $number; // 21 >
Подобным образом генератор может возвращать и большее количество значений:
function generateNumbers() < for ($i = 0; $i > foreach(generateNumbers() as $number) < echo $number; // 012345 >
В данном случае функция генератора generateNumbers() с помощью цикла генерирует значения от 0 до 5 включительно. Это все равно, если бы написали:
function generateNumbers()
При переборе в цикле мы фактически перебираем результат функции как обычный массив, каждый элемент которого имеет числовой индекс, начиная с нуля:
> foreach(generateNumbers() as $index => $number) < echo "$index - $number
"; // 012345 > ?>
0 - 10 1 - 11 2 - 12 3 - 13 4 - 14 5 - 15
С помощью оператора from можно определять массив — источник данных для генератора:
function generateNumbers() < yield 1; yield from [2, 3, 4]; yield 5; >foreach(generateNumbers() as $number) < echo $number; // 12345 >
В данном случае функция generateNumbers() для генерации часть данных берет из массива [2, 3, 4] с помощью выражения yield from [2, 3, 4] .
Функция генератора, как и любая функция может принимать параметры, что позволяет настраивать поведение генератора:
function generateNumbers($start, $end) < for($i = $start; $i < $end; $i++)< yield $i; >> foreach(generateNumbers(4, 9) as $number) < echo $number; // 45678 >
Но естественно может возникнуть вопрос: а зачем нужны генераторы? Разве мы не можем с тем же успехом перебират обычный массив? Например:
$numbers = [1, 2, 3, 4, 5]; foreach($numbers as $number) < echo $number; // 12345 >
Дело в том, что при работе с массивом весь массив загружается в память. При небольших объемах проблема может быть игнорироваться. Но чем больше размер массива, соответственно тем больше издержки и потери в производительности. Именно эту проблему и призваны решить генераторы, которые извлекают только одно значение одномоментно при обращении к функции, экономя тем самым вычислительные ресурсы.
Storing into an array (e.g. with iterator_to_array() )
yield from does not reset the keys. It preserves the keys returned by the Traversable object, or array . Thus some values may share a common key with another yield or yield from, which, upon insertion into an array, will overwrite former values with that key.
A common case where this matters is iterator_to_array() returning a keyed array by default, leading to possibly unexpected results. iterator_to_array() has a second parameter preserve_keys which can be set to false to collect all the values while ignoring the keys returned by the Generator .
Example #5 yield from with iterator_to_array()
function inner () yield 1 ; // key 0
yield 2 ; // key 1
yield 3 ; // key 2
>
function gen () yield 0 ; // key 0
yield from inner (); // keys 0-2
yield 4 ; // key 1
>
// pass false as second parameter to get an array [0, 1, 2, 3, 4]
var_dump ( iterator_to_array ( gen ()));
?>?php
The above example will output:
array(3) < [0]=>int(1) [1]=> int(4) [2]=> int(3) >
Example #6 Basic use of yield from
function count_to_ten () yield 1 ;
yield 2 ;
yield from [ 3 , 4 ];
yield from new ArrayIterator ([ 5 , 6 ]);
yield from seven_eight ();
yield 9 ;
yield 10 ;
>
?php
function seven_eight () yield 7 ;
yield from eight ();
>
function eight () yield 8 ;
>
foreach ( count_to_ten () as $num ) echo » $num » ;
>
?>
The above example will output:
1 2 3 4 5 6 7 8 9 10
Example #7 yield from and return values
function count_to_ten () yield 1 ;
yield 2 ;
yield from [ 3 , 4 ];
yield from new ArrayIterator ([ 5 , 6 ]);
yield from seven_eight ();
return yield from nine_ten ();
>
?php
function seven_eight () yield 7 ;
yield from eight ();
>
function eight () yield 8 ;
>
function nine_ten () yield 9 ;
return 10 ;
>
$gen = count_to_ten ();
foreach ( $gen as $num ) echo » $num » ;
>
echo $gen -> getReturn ();
?>
The above example will output:
1 2 3 4 5 6 7 8 9 10
Что делает оператор yield php
Итератор по сути представляет блок кода, который использует оператор yield для перебора набора значений. Данный блок кода может представлять тело метода, оператора или блок get в свойствах.
Итератор использует две специальных инструкции:
- yield return : определяет возвращаемый элемент
- yield break : указывает, что последовательность больше не имеет элементов
Рассмотрим небольшой пример:
Numbers numbers = new Numbers(); foreach (int n in numbers) < Console.WriteLine(n); >class Numbers < public IEnumeratorGetEnumerator() < for (int i = 0; i < 6; i++) < yield return i * i; >> >
В классе Numbers метод GetEnumerator() фактически представляет итератор. С помощью оператора yield return возвращается некоторое значение (в данном случае квадрат числа).
В программе с помощью цикла foreach мы можем перебрать объект Numbers как обычную коллекцию. При получении каждого элемента в цикле foreach будет срабатывать оператор yield return, который будет возвращать один элемент и запоминать текущую позицию.
Благодаря итераторам мы можем пойти дальше и легко реализовать перебор числа в цикле foreach:
foreach(var n in 5) Console.WriteLine(n); foreach (var n in -5) Console.WriteLine(n); static class Int32Extension < public static IEnumeratorGetEnumerator(this int number) < int k = (number >0)? number: 0; for (int i = number - k; i >
В данном случае итератор реализован как метод расширения для типа int или System.Int32. В методе итератора фактически возвращаем все целочисленные значения от 0 до текущего числа. Консольный вывод:
0 1 2 3 4 5 -5 -4 -3 -2 -1 0
Другой пример: пусть у нас есть коллекция Company, которая представляет компанию и которая хранит в массиве personnel штат сотрудников — объектов Person. Используем оператор yield для перебора этой коллекции:
class Person < public string Name < get; >public Person(string name) =>Name = name; > class Company < Person[] personnel; public Company(Person[] personnel) =>this.personnel = personnel; public int Length => personnel.Length; public IEnumerator GetEnumerator() < for (int i = 0; i < personnel.Length; i++) < yield return personnel[i]; >> >
Метод GetEnumerator() представляет итератор. И когда мы будем осуществлять перебор в объекте Company в цикле foreach, то будет идти обращение к вызову yield return personnel[i]; . При обращении к оператору yield return будет сохраняться текущее местоположение. И когда метод foreach перейдет к следующей итерации для получения нового объекта, итератор начнет выполнения с этого местоположения.
Ну и в основной программе в цикле foreach выполняется собственно перебор, благодаря реализации итератора:
var people = new Person[] < new Person("Tom"), new Person("Bob"), new Person("Sam") >; var microsoft = new Company(people); foreach(Person employee in microsoft)
Хотя при реализации итератора в методе GetEnumerator() применялся перебор массива в цикле for, но это необязательно делать. Мы можем просто определить несколько вызовов оператора yield return :
public IEnumerator GetEnumerator()
В этом случае при каждом вызове оператора yield return итератор также будет запоминать текущее местоположение и при последующих вызовах начинать с него.
Именованный итератор
Выше для создания итератора мы использовали метод GetEnumerator . Но оператор yield можно использовать внутри любого метода, только такой метод должен возвращать объект интерфейса IEnumerable . Подобные методы еще называют именованными итераторами .
Создадим такой именованный итератор в классе Company и используем его:
class Person < public string Name < get; >public Person(string name) =>Name = name; > class Company < Person[] personnel; public Company(Person[] personnel) =>this.personnel = personnel; public int Length => personnel.Length; public IEnumerable GetPersonnel(int max) < for (int i = 0; i < max; i++) < if (i == personnel.Length) < yield break; >else < yield return personnel[i]; >> > >
Определенный здесь итератор — метод IEnumerable GetPersonnel(int max) в качестве параметра принимает количество выводимых объектов. В процессе работы программы может сложиться, что его значение будет больше, чем длина массива personnel. И чтобы не произошло ошибки, используется оператор yield break . Этот оператор прерывает выполнение итератора.
var people = new Person[] < new Person("Tom"), new Person("Bob"), new Person("Sam") >; var microsoft = new Company(people); foreach(Person employee in microsoft.GetPersonnel(5))
Вызов microsoft.GetPersonnel(5) будет возвращать набор из не более чем 5 объектов Person. Но так как у нас всего три таких объекта, то в методе GetPersonnel после трех операций сработает оператор yield break .
“Yield” и деликатная работа с памятью в PHP
Вы когда-нибудь задавались вопросом: “Какая польза от yield в PHP?”. Позвольте мне избавить вас от поиска в Google; Я с удовольствием раскрою вам пару ключевых моментов о yield:
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)
Но различия на этом не заканчиваются!
Генератор позволяет вам писать код с использованием оператора foreach для итерации по набору данных без необходимости создавать в памяти массив, что, однако, может приводить к превышению лимита памяти.
В следующем примере мы создадим массив из 800000 элементов и вернем его из функции getValues() , контролируя память, выделенную для этого фрагмента кода, с помощью функции memory_get_usage(). Мы будем запрашивать потребление памяти через каждые 200000 добавленных элементов, что означает, что контрольных точек будет четыре:
> 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:
> > $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, но я выделю пару из них:
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. Ссылки
- http://php.net/manual/en/language.generators.syntax.php
- http://php.net/manual/en/class.generator.php
- http://php.net/manual/en/language.generators.php
- http://php.net/manual/en/function.memory-get-usage.php
Данная статья переведена в преддверии старта курса PHP Developer. Basic. Узнать подробнее о курсе можно по ссылке.
