Готовимся к собеседованию по PHP: Всё об итерации и немного про псевдотип «iterable»
И, разумеется, какими бы вам странными и некорректными ни казались вопросы на собеседовании, приходить нужно всё-таки подготовленным, зная тот язык, за программирование на котором вам собираются платить.
Третья часть серии статей посвящена одному из самых объемных понятий в современном 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/class.traversable.php
Успехов на собеседовании и в работе!