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

в 15:40, , рубрики: foreach, iterable, iteration, iterator, iterators, php, PHP 7, ооп, Программирование

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

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

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

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 всё-так не вошли. Впрочем, практическая ценность итерации по таким объектам не особо велика, так что нет повода расстраиваться…

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

Успехов на собеседовании и в работе!

Автор: Альберт Степанцев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js