И, разумеется, какими бы вам странными и некорректными ни казались вопросы на собеседовании, приходить нужно всё-таки подготовленным, зная тот язык, за программирование на котором вам собираются платить.
Третья часть серии статей посвящена одному из самых объемных понятий в современном PHP — итерации, итераторам и итерируемым сущностям. Я постарался свести в один текст некий минимум знаний об этом вопросе, пригодный для самоподготовки к собеседованию на позицию разработчика на PHP.
Две предыдущие части:
- Готовимся к собеседованию по PHP: ключевое слово «static»
- Готовимся к собеседованию по PHP: псевдотип «callable»
Массивы в PHP
Давайте начнем с самого начала.
В PHP есть массивы. Массивы в PHP являются ассоциативными, то есть хранят в себе пары (ключ, значение), где ключом должен быть int или string, а значение может иметь любой тип.
Пример:
$arr = ['foo' => 'bar', 'baz' => 42, 'arr' => [1, 2, 3]];
Ключ и значение разделяются символом "=>". Иногда ключ иначе называют «индексом», в PHP это равнозначные термины.
На массивах в PHP определен довольно полный набор операций:
// Вставка в массив
$arr['new'] = 'some value';
// Вставка с автоматической генерацией индекса
$arr[] = 'another value';
// Доступ к элементу по ключу
echo $arr['foo'];
echo $arr[$bar];
// Удаление элемента по индексу
unset($arr['foo']);
// "Распаковка" массива
[$foo, $bar, $baz] = $arr;
Также имеется множество функций для работы с массивами — десятки и сотни их!
Однако самым, пожалуй, главным свойством массивов в PHP является возможность последовательно пройтись по всем элементам массива, получая все пары «ключ-значение» по порядку.
Итерация по массивам
Процесс прохода по массиву называется «итерацией» (или «перебором») (кстати, каждый шаг, получение каждой отдельной пары «ключ-значение» — тоже «итерация»), а сам массив, таким образом, является итерируемой («перебираемой») сущностью.
Самый простой пример процесса итерации это, конечно же, совместный цикл, реализованный оператором foreach:
foreach ($arr as $key=>$val) {
echo $key . '=>' . $val;
echo "\n";
}
Обратите внимание на всё тот же знак "=>", который разделяет ключ и значение в заголовке цикла.
Но как же PHP понимает — какой элемент массива взять на конкретном шаге цикла? Какой взять следующим? И когда остановиться?
Для ответа на этот вопрос следует знать о существовании так называемого «внутреннего указателя», существующего в каждом массиве. Этот невидимый указатель указывает на «текущий» элемент и умеет сдвигаться на шаг вперед — на следующий элемент или снова сбрасываться на первый элемент.
Для прямой работы с внутренним указателем в PHP существуют функции, которые проще всего изучить на примере:
$arr = [1, 2, 3];
// Сбрасываем внутренний указатель, устанавливая его на первый элемент
reset($arr);
// key() возвращает ключ текущего элемента, на который указывает внутренний указатель, либо null в случае если указатель вышел за границу массива
while ( null !== ($key = key($arr)) ) {
// current() возвращает значение текущего элемента, на который указывает внутренний указатель
echo $key . '=>' . current($arr);
echo "\n";
// next() сдвигает внутренний указатель массива на один элемент вперед
next($arr);
}
Легко заметить, что приведенный пример кода фактически эквивалентен ранее использовавшемуся циклу foreach, и что foreach является как бы синтаксическим сахаром для функций reset(), key(), current(), next() (а еще есть функции end() и prev() — для организации перебора в обратном порядке).
Это утверждение было верным до PHP 7, однако сейчас дело обстоит немного не так — цикл foreach перестал использовать тот же самый внутренний указатель, что reset(), next() и другие функции итерации, поэтому перестал изменять его позицию.
Промежуточный итог
Итак, подведем краткий итог, как устроена итерация по массивам в PHP:
- С каждым массивом связан внутренний указатель
- Он может быть сброшен на начало (или конец) массива
- Он может быть передвинут на следующий (предыдущий) элемент
- Мы можем проверить, не достигнут ли конец — не вышел ли указатель за пределы массива?
- И можем получить ключ и значение текущего элемента (на который указывает указатель)
Такое устройство позволяет нам организовывать итерацию по массиву (перебор его элементов) в виде цикла. Но при этом важно понимать, что цикл foreach, хотя и устроен аналогично, работает не с тем же самым внутренним указателем, что и функции reset(), key(), current() и т.п., а со своим собственным, локальным для цикла.
Итерация по объектам
Объекты, как и массивы, являются итерируемыми сущностями. Обход объектов идет по их видимым в данном контексте свойствам, причем ключами служат имена свойств.
class Foo
{
public $first = 1;
public $second = 2;
protected $third = 3;
public function iterate()
{
foreach ($this as $key => $value) {
echo $key . '=>' . $value;
echo "\n";
}
}
}
$foo = new Foo;
foreach ($foo as $key => $value) {
echo $key . '=>' . $value;
echo "\n";
}
/*
Будет выведено
first=>1
second=>2
*/
$foo->iterate();
/*
Будет выведено
first=>1
second=>2
third=>3
*/
Однако такая итерация, по видимым свойствам, зачастую бывает совершенно бесполезной. Самый частый пример — это некий объект, который хранит набор значений во внутреннем защищенном хранилище. Например вот так:
class Storage
{
protected $storage = [];
public function set($key, $val)
{
$this->storage[$key] = $val;
}
public function get($key)
{
return $this->storage[$key];
}
}
Как же организовать итерацию по такому объекту, у которого нет публичных свойств? И как вообще организовать итерацию по какому-то собственному нестандартному алгоритму?
Интерфейс Iterator
Для реализации собственных алгоритмов итерации PHP (а точнее SPL) предоставляет специальный интерфейс Iterator, состоящий из пяти методов:
// Метод должен вернуть значение текущего элемента
public function current();
// Метод должен вернуть ключ текущего элемента
public function key();
// Метод должен сдвинуть "указатель" на следующий элемент
public function next(): void;
// Метод должен поставить "указатель" на первый элемент
public function rewind(): void;
// Метод должен проверять - не вышел ли указатель за границы?
public function valid(): bool
Ваш класс должен реализовать эти методы и тогда вы получите возможность итерировать объекты этого класса с помощью цикла foreach в соответствии с реализованным алгоритмом.
N.B. «Указатель», который упоминается здесь в описании методов интерфейса Iterator — чистая абстракция, в отличие от реально существующего внутреннего указателя массивов. Только от вас зависит, как именно вы реализуете эту абстракцию, важен только результат — например последовательный вызов методов rewind() и current() обязан вернуть значение первого элемента.
class Example
implements Iterator
{
protected $storage = [];
public function set($key, $val)
{
$this->storage[$key] = $val;
}
public function get($key)
{
return $this->storage[$key];
}
public function current()
{
return current($this->storage);
}
public function key()
{
return key($this->storage);
}
public function next(): void
{
next($this->storage);
}
public function rewind(): void
{
reset($this->storage);
}
public function valid(): bool
{
return null !== key($this->storage);
}
}
$test = new Example;
$test->set('foo', 'bar');
$test->set('baz', 42);
foreach ($test as $key => $val) {
echo $key . '=>' . $val;
echo "\n";
}
Traversable и IteratorAggregate
Строго говоря, итерироваться с помощью foreach нам позволяет интерфейс Traversable, а Iterator является его наследником. Особенность Traversable заключается в том, что его нельзя реализовать напрямую (этакий «абстрактный интерфейс») и пользоваться в своих приложениях нужно всё-таки интерфейсом Iterator или его «младшим братом» IteratorAggregate. О нём и поговорим.
В SPL включено несколько встроенных классов итераторов, которые позволяют вам обернуть в объект-итератор некую другую сущность, например массив:
$iterator = new ArrayIterator([1, 2, 3]);
foreach ($iterator as $key => $val) {
// ...
}
Список таких готовых обёрток-итераторов довольно велик и включает в себя такие небесполезные классы как DirectoryIterator (итерирует по списку файлов в заданной директории), RecursiveArrayIterator (рекурсивный обход вложенных массивов), FilterIterator (обход с отбрасыванием нежелательных значений) и другие, опять же десятки их.
Использование готовых итераторов и интерфейса IteratorAggregate позволяет нам значительно упростить создание собственных классов-итераторов. Так, весьма длинный класс под спойлером выше, может быть сокращен примерно до такого:
class Example
implements IteratorAggregate
{
protected $storage = [];
public function set($key, $val)
{
$this->storage[$key] = $val;
}
public function get($key)
{
return $this->storage[$key];
}
public function getIterator(): Traversable
{
return new ArrayIterator($this->storage);
}
}
— результат будет таким же, как и при собственноручной реализации интерфейса Iterator.
А генераторы?
Ну разумеется. Мы же их используем через foreach!
class Generator implements Iterator
Впрочем, генераторы — это тема отдельной статьи. Пока же достаточно сказать, что в механизме генераторов нет ничего волшебного — для итерации используется всё тот же интерфейс Iterator. За исключением одного «но» — генератор нельзя «перемотать на начало», если итерация уже началась, то вызов метода rewind() выбросит исключение.
Тип iterable
До PHP 7.1 складывалась странная картина. С одной стороны стояли итерируемые объекты, реализующие Traversable через Iterator или IteratorAggregate. На этой же стороне были генераторы, как использующие тот же механизм. А на другой стороне — массивы и «нативная» итерация по видимым свойствам объектов. Фактически существовали два типа итерируемых сущностей, имеющих идентичное поведение, но не имеющих ничего общего.
В 7.1, наконец, эта нелогичность была устранена и у нас появился очередной «псевдотип» (а точнее кастомный тип) «iterable».
Когда однажды мы дождемся появления в PHP оператора type, определение типа iterable можно будет записать так:
type iterable = array | Traversable;
Данный тип объединяет в себе массивы и всех наследников Traversable и обозначает тип значений, по которым можно итерироваться с помощью foreach:
function doSomething(iterable $it)
{
foreach ($it as $key=>$val) {
// do something
}
}
И что же получается?
Получается вот такая диаграмма типов:
iterable ---> array
--> Traversable ---> Iterator
--> IteratorAggregate
--> Generator
Стоит отметить, что объекты, допускающие нативную итерацию по своим видимым свойствам («просто object» тип), в тип iterable всё-так не вошли. Впрочем, практическая ценность итерации по таким объектам не особо велика, так что нет повода расстраиваться…
Что еще почитать?
- ru.wikipedia.org/wiki/%D0%90%D1%81%D1%81%D0%BE%D1%86%D0%B8%D0%B0%D1%82%D0%B8%D0%B2%D0%BD%D1%8B%D0%B9_%D0%BC%D0%B0%D1%81%D1%81%D0%B8%D0%B2
- https://ru.wikipedia.org/wiki/%D0%A6%D0%B8%D0%BA%D0%BB_(%D0%BF%D1%80%D0%BE%D0%B3%D1%80%D0%B0%D0%BC%D0%BC%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D0%B5)#.D0.A1.D0.BE.D0.B2.D0.BC.D0.B5.D1.81.D1.82.D0.BD.D1.8B.D0.B9_.D1.86.D0.B8.D0.BA.D0.BB
- php.net/manual/ru/language.types.array.php
- php.net/manual/ru/control-structures.foreach.php
- php.net/manual/ru/language.oop5.iterations.php
- php.net/manual/ru/spl.iterators.php
- php.net/manual/ru/class.traversable.php
Успехов на собеседовании и в работе!
Комментарии (21)
gzhegow
27.03.2017 21:23-12Вам скучно жить без того, чтобы почитать что-нибудь умное?
Вот скажите, чем вас не устраивает функция pluck() [вот черт, ее же в php нету!], функция array_combine [вот черт, она же в пхп тупее утюга] и обычный олдскульный foreach?
На кой черт вам делать итерацию внутри обьекта и перебирать все его свойства?
Что вам мешает запросить список свойств в виде массива строк, а потом пройтись по ним через foreach?
Вы просто хотите стать умнее, или что? Какая польза в осознании итераторов? Ну кроме вашей возможности показать тимлиду как вы ловко перебираете данными в обьекте, и что вы, значит, понимаете, что объект это не то же самое что массив?
90% задач — собрать данные, объединить по массиву ключей, осуществить поиск, сохранить в базу.
Вы жить не можете без того, чтобы собрать это на итераторах?AlexLeonov
27.03.2017 21:27+2На кой черт вам делать итерацию внутри обьекта и перебирать все его свойства?
PHP очень гибкий язык. Собственно, об этом и написана статья.
Он позволяет вам реализовать любой ваш собственный алгоритм итерации, а не только «перебирать все его свойства», причем сделать это можно довольно элегантно — берете стандартный интерфейс и реализуете его так, как вам пожелается.
Разделять интерфейс и реализацию, понимаете? Не этому ли учат на первых лекциях по ООП?
90% задач — собрать данные, объединить по массиву ключей, осуществить поиск, сохранить в базу.
И тут нас настигают две проблемы.
1. Массив — не объект, у него нет специализированного типа, значит нет контроля типа, приходится городить сложную валидацию вместо простого instanceof
2. Массив — не объект, у него нет методов, поэтому приходится городить «внешние методы» и тут см. п. 1.
И мы с вами очень плавно переходим к тому, что нам нужны всё-таки типы и методы… А это что? Это классы и объекты. Чёрт… Засада какая!
Вы просто хотите стать умнее, или что?
Куда уж больше-то :)
symbix
27.03.2017 22:54+1Вот пример из практики.
Есть старый код. Функция возвращает массив, в который вычитываются строки из базы данных. Этот массив обрабатывается в куче мест по коду.
База разрослась и массив такой огромный, что уже неприлично. Надо вычитывать данные кусками. Переписывать все 20 мест, где происходит итерация по массиву? Да ну, неохота, да и рискованно слишком (это легаси никто не покрывал тестами). Куда практичнее реализовать в этой самой функции возврат итератора — код меняется только в одном месте, все легко протестировать.
alexlcdee
28.03.2017 09:46Стоит тогда еще и ArrayAccess реализовать, т.к. в старом коде может оказаться конструккия вида
if(isset($users[500]) && $users[500]['name'] == 'Uasyaa') { $users[500]['role'] = 'Admin'; }
Fesor
27.03.2017 23:08+2Какая польза в осознании итераторов?
Например у нас есть проект, и мы получаем 500-ку. Смотрим в код ви видим.
foreach ($this->getAllUsers() as $user) { // много всякого }
Пользователей у нас например пара сотен тысяч и оно просто валится по памяти.
Вопрос, как можно быстро сделать выборку данных чанками с использованием курсора с минимальным количеством изменений (а это только изменения в методе getAllUsers). Итераторы/генераторы применять нельзя, ибо зачем их осознавать.
Ну и всякие там "бесконечные" загрузки, стрим парсера… итераторы позволяют нам "инкапсулировать" сложности работы с данными под красивый и удобный интерфейс. Что бы клиентский код (то есть код который юзает наш код) не заморачивался с тем, вернули мы ему просто массивчик захардкоженный, или мы там под копотом поиск в ширину делаем на файловой системе.
p.s. только сейчас заметил что уже запостили похожий случай.
gzhegow
28.03.2017 09:28-6Ой с Вами даже общаться неинтересно, все такие разбирающиеся и понимающие, все такие умные что не передать. Язык с порогом входа в 1 день превратили в язык с порогом входа 10 лет и изучением типов итераторов, минусите, усложняйте, долбайтесь, Ваша жизнь не наша.
Откуда у тебя массив в 100 тысяч юзеров и на кой черт тебе массив в 200 тысяч? Ты слышал про SQL запросы, которые сделают за тебя выборку за доли секунды? другой раз кажется что нет.
И когда у тебя 200 тысяч айдишников уже отфильтрованных запросом. то обновить по ID — фигня вопрос. А вот выбирать из массива пхп из сотни тысяч записей десяток или сотню и делать это на PHP — да тут полюбому все ляжет, потому что гаечный ключ ремонтирует кран, а молотком забивают гвозди. Не наоборот.
Но ваш мультикультурализм умиляет. Если кто-то не согласен — надо его заминусить. Долбайтесь, ваш мирок узок.AlexLeonov
28.03.2017 09:46Если кто-то не согласен — надо его заминусить.
Из ваших комментов непонятно — с чем именно вы несогласны?
С существованием в PHP типа iterable? Так не вопрос, предложите RFC на его удаление из языка, обоснуйте, дождитесь голосования…
Я полагаю, что если бы вы формулировали свои мысли чуть более четко и чуть менее экспрессивно — минусов было бы меньше. Минусят не вашу точку зрения, а невозможность ее понять.
VolCh
28.03.2017 10:43+1Никто вам лично не запрещает писать в стиле PHP3 под PHP7.1 (c учётом замены или даже полного выпиливания некоторых расширений, небольшого изменения поведения и прочих потерь совместимости). Но в целом, средние задачи за 20 лет усложнились и язык адаптируется к тому, чтобы их решение было проще. Причём средствами PHP. Тот же SQL (в чистом, максимально переносимом виде, без хранимых процедур, триггеров и т. п.) далеко не все задачи может решать, даже по фильтрации данных из базы, если фильтровать надо по значению, вычисленному по базе циклическим или рекурсивным алгоритмом. А вы тут возмущаетесь про итераторы, которые в языке появились более 10 лет назад и как раз позволяют использовать обычный олдскульный foreach для сложных задач.
UnknownQq
28.03.2017 11:03+1Вы понимаете, что пришли критиковать огромную толпу народа и стоите ровно в её середине?
Существуют разные реализации и разные ситуации, когда тебе «в наследство» достается «код», который нужно здесь и сейчас подкрутить и чтобы оно быстро завелось. Бывает всякое и далеко не всегда можно все быстро «разрулить», и сделать как положено. В подобных ситуациях нужно знать расстояние для маневра.
А статья — отличная, впрочем, как и некоторые комментарии, что разъясняют для чего это надо.
m_z
28.03.2017 10:48Использование готовых итераторов и интерфейса IteratorAggregate позволяет нам значительно упростить создание собственных классов-итераторов.
…
результат будет таким же, как и при собственноручной реализации интерфейса Iterator.
Из статьи может показаться, что IteratorAggregate нужен только для готовых итераторов. Более точно: IteratorAggregate позволяет отделить код итератора от итерируемого объекта. И отличие такой реализации в том, что можно обходить один и тот же объект во вложенных foreach-циклах (т.к. итератор создается для каждого цикла свой).AlexLeonov
28.03.2017 11:05Как обычно — комментарии более ценны, чем сама статья. Спасибо за дополнение!
vlreshet
28.03.2017 12:17
А с какой версии php это работает? Попробовал не седьмой — не взлетает. Да и в принципе впервые о таком слышу, и документация вроде молчит…[$foo, $bar, $baz] = $arr;
AlexLeonov
28.03.2017 12:20С версии 7.1
http://sandbox.onlinephpfunctions.com/code/4848817d5b26d623ebed8e4109aae49b064cd34b
Нет никаких причин не обновиться :)vlreshet
28.03.2017 12:21+1Спасибо!
AlexLeonov
28.03.2017 12:23Всегда пожалуйста.
Фактически, такая запись — аналог оператора list, но чуть более умный. Можно, например, указывать, какой конкретно элемент (по ключу) в какую переменную будет развернут.
Lexx918
Куда важнее, сложнее и интереснее знать про работу с этим добром — Дерево классов итераторов SPL
Но этой ссылки нет даже в списке доп.литературы.
AlexLeonov
Ну как же нет…
Вот же: «В SPL включено несколько встроенных классов итераторов» и далее по тексту — ровно эта ссылка.
Впрочем, вы правы, список важный, продублировал в списке литературы.
И опять же вы говорите «важнее». «Список классов из SPL знать наизусть» важнее чем что? Чем знать о существовании iterable и о том, что это такое? Позвольте не согласиться.
Lexx918
Да, надо знать наизусть, если хочешь получить сертификат ZCE. Да, надо знать, что это и как этим пользоваться, если хочешь пройти собеседование с HR в том же Badoo и добраться до собеседования с технарями (из личного печального опыта, в поддержку #меняневзяли).
Соглашусь, что это лишнее и пользы на первых порах — ноль. Но если уж взялся, то доведи дел до конца. А то какие-то полумеры получаются. Тем более что задачу из темы статьи в итоге статья не решает. Только как ликбез для начинающих.
AlexLeonov
Спасибо за отзыв. Учту в будущих статьях.