Готовимся к собеседованию по PHP: Всё об итерации и немного про псевдотип «iterable»

от автора

Не секрет, что на собеседованиях любят задавать каверзные вопросы. Не всегда адекватные, не всегда имеющие отношение к реальности, но факт остается фактом — задают. Конечно, вопрос вопросу рознь, и иногда вопрос, на первый взгляд кажущийся вам дурацким, на самом деле направлен на проверку того, насколько хорошо вы знаете язык, на котором пишете.

И, разумеется, какими бы вам странными и некорректными ни казались вопросы на собеседовании, приходить нужно всё-таки подготовленным, зная тот язык, за программирование на котором вам собираются платить.

image

Третья часть серии статей посвящена одному из самых объемных понятий в современном PHP — итерации, итераторам и итерируемым сущностям. Я постарался свести в один текст некий минимум знаний об этом вопросе, пригодный для самоподготовки к собеседованию на позицию разработчика на PHP.

Две предыдущие части:

Массивы в 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() обязан вернуть значение первого элемента.

Простейший пример реализации интерфейса Iterator

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"; } 

Travesable и 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 складывалась странная картина. С одной стороны стояли итерируемые объекты, реализующие Traversabe через 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           --> object           --> Traversable ---> Iterator                            --> IteratorAggregate                            --> Generator 

Что еще почитать?

Успехов на собеседовании и в работе!
ссылка на оригинал статьи https://habrahabr.ru/post/324934/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *