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

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

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

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



Успехов на собеседовании и в работе!
Поделиться с друзьями
-->

Комментарии (21)


  1. Lexx918
    27.03.2017 19:39

    Куда важнее, сложнее и интереснее знать про работу с этим добром — Дерево классов итераторов SPL
    Но этой ссылки нет даже в списке доп.литературы.


    1. AlexLeonov
      27.03.2017 19:44
      +1

      Ну как же нет…
      Вот же: «В SPL включено несколько встроенных классов итераторов» и далее по тексту — ровно эта ссылка.

      Впрочем, вы правы, список важный, продублировал в списке литературы.

      И опять же вы говорите «важнее». «Список классов из SPL знать наизусть» важнее чем что? Чем знать о существовании iterable и о том, что это такое? Позвольте не согласиться.


      1. Lexx918
        27.03.2017 19:56
        +1

        Да, надо знать наизусть, если хочешь получить сертификат ZCE. Да, надо знать, что это и как этим пользоваться, если хочешь пройти собеседование с HR в том же Badoo и добраться до собеседования с технарями (из личного печального опыта, в поддержку #меняневзяли).
        Соглашусь, что это лишнее и пользы на первых порах — ноль. Но если уж взялся, то доведи дел до конца. А то какие-то полумеры получаются. Тем более что задачу из темы статьи в итоге статья не решает. Только как ликбез для начинающих.


        1. AlexLeonov
          27.03.2017 20:28

          Спасибо за отзыв. Учту в будущих статьях.


  1. gzhegow
    27.03.2017 21:23
    -12

    Вам скучно жить без того, чтобы почитать что-нибудь умное?

    Вот скажите, чем вас не устраивает функция pluck() [вот черт, ее же в php нету!], функция array_combine [вот черт, она же в пхп тупее утюга] и обычный олдскульный foreach?

    На кой черт вам делать итерацию внутри обьекта и перебирать все его свойства?
    Что вам мешает запросить список свойств в виде массива строк, а потом пройтись по ним через foreach?

    Вы просто хотите стать умнее, или что? Какая польза в осознании итераторов? Ну кроме вашей возможности показать тимлиду как вы ловко перебираете данными в обьекте, и что вы, значит, понимаете, что объект это не то же самое что массив?

    90% задач — собрать данные, объединить по массиву ключей, осуществить поиск, сохранить в базу.
    Вы жить не можете без того, чтобы собрать это на итераторах?


    1. AlexLeonov
      27.03.2017 21:27
      +2

      На кой черт вам делать итерацию внутри обьекта и перебирать все его свойства?

      PHP очень гибкий язык. Собственно, об этом и написана статья.

      Он позволяет вам реализовать любой ваш собственный алгоритм итерации, а не только «перебирать все его свойства», причем сделать это можно довольно элегантно — берете стандартный интерфейс и реализуете его так, как вам пожелается.

      Разделять интерфейс и реализацию, понимаете? Не этому ли учат на первых лекциях по ООП?

      90% задач — собрать данные, объединить по массиву ключей, осуществить поиск, сохранить в базу.

      И тут нас настигают две проблемы.
      1. Массив — не объект, у него нет специализированного типа, значит нет контроля типа, приходится городить сложную валидацию вместо простого instanceof
      2. Массив — не объект, у него нет методов, поэтому приходится городить «внешние методы» и тут см. п. 1.
      И мы с вами очень плавно переходим к тому, что нам нужны всё-таки типы и методы… А это что? Это классы и объекты. Чёрт… Засада какая!

      Вы просто хотите стать умнее, или что?

      Куда уж больше-то :)


    1. symbix
      27.03.2017 22:54
      +1

      Вот пример из практики.


      Есть старый код. Функция возвращает массив, в который вычитываются строки из базы данных. Этот массив обрабатывается в куче мест по коду.


      База разрослась и массив такой огромный, что уже неприлично. Надо вычитывать данные кусками. Переписывать все 20 мест, где происходит итерация по массиву? Да ну, неохота, да и рискованно слишком (это легаси никто не покрывал тестами). Куда практичнее реализовать в этой самой функции возврат итератора — код меняется только в одном месте, все легко протестировать.


      1. alexlcdee
        28.03.2017 09:46

        Стоит тогда еще и ArrayAccess реализовать, т.к. в старом коде может оказаться конструккия вида

        if(isset($users[500]) && $users[500]['name'] == 'Uasyaa') {
            $users[500]['role']  = 'Admin';
        } 
        


    1. Fesor
      27.03.2017 23:08
      +2

      Какая польза в осознании итераторов?

      Например у нас есть проект, и мы получаем 500-ку. Смотрим в код ви видим.


      foreach ($this->getAllUsers() as $user) {
         // много всякого
      }

      Пользователей у нас например пара сотен тысяч и оно просто валится по памяти.


      Вопрос, как можно быстро сделать выборку данных чанками с использованием курсора с минимальным количеством изменений (а это только изменения в методе getAllUsers). Итераторы/генераторы применять нельзя, ибо зачем их осознавать.


      Ну и всякие там "бесконечные" загрузки, стрим парсера… итераторы позволяют нам "инкапсулировать" сложности работы с данными под красивый и удобный интерфейс. Что бы клиентский код (то есть код который юзает наш код) не заморачивался с тем, вернули мы ему просто массивчик захардкоженный, или мы там под копотом поиск в ширину делаем на файловой системе.


      p.s. только сейчас заметил что уже запостили похожий случай.


    1. Assada
      28.03.2017 11:44

      Действительно. Зачем же нужны эти ваши итераторы… https://toster.ru/q/392018


  1. AlexLeonov
    27.03.2017 21:27

    не туда ответил


  1. gzhegow
    28.03.2017 09:28
    -6

    Ой с Вами даже общаться неинтересно, все такие разбирающиеся и понимающие, все такие умные что не передать. Язык с порогом входа в 1 день превратили в язык с порогом входа 10 лет и изучением типов итераторов, минусите, усложняйте, долбайтесь, Ваша жизнь не наша.

    Откуда у тебя массив в 100 тысяч юзеров и на кой черт тебе массив в 200 тысяч? Ты слышал про SQL запросы, которые сделают за тебя выборку за доли секунды? другой раз кажется что нет.

    И когда у тебя 200 тысяч айдишников уже отфильтрованных запросом. то обновить по ID — фигня вопрос. А вот выбирать из массива пхп из сотни тысяч записей десяток или сотню и делать это на PHP — да тут полюбому все ляжет, потому что гаечный ключ ремонтирует кран, а молотком забивают гвозди. Не наоборот.

    Но ваш мультикультурализм умиляет. Если кто-то не согласен — надо его заминусить. Долбайтесь, ваш мирок узок.


    1. AlexLeonov
      28.03.2017 09:46

      Если кто-то не согласен — надо его заминусить.

      Из ваших комментов непонятно — с чем именно вы несогласны?

      С существованием в PHP типа iterable? Так не вопрос, предложите RFC на его удаление из языка, обоснуйте, дождитесь голосования…

      Я полагаю, что если бы вы формулировали свои мысли чуть более четко и чуть менее экспрессивно — минусов было бы меньше. Минусят не вашу точку зрения, а невозможность ее понять.


    1. VolCh
      28.03.2017 10:43
      +1

      Никто вам лично не запрещает писать в стиле PHP3 под PHP7.1 (c учётом замены или даже полного выпиливания некоторых расширений, небольшого изменения поведения и прочих потерь совместимости). Но в целом, средние задачи за 20 лет усложнились и язык адаптируется к тому, чтобы их решение было проще. Причём средствами PHP. Тот же SQL (в чистом, максимально переносимом виде, без хранимых процедур, триггеров и т. п.) далеко не все задачи может решать, даже по фильтрации данных из базы, если фильтровать надо по значению, вычисленному по базе циклическим или рекурсивным алгоритмом. А вы тут возмущаетесь про итераторы, которые в языке появились более 10 лет назад и как раз позволяют использовать обычный олдскульный foreach для сложных задач.


    1. UnknownQq
      28.03.2017 11:03
      +1

      Вы понимаете, что пришли критиковать огромную толпу народа и стоите ровно в её середине?

      Существуют разные реализации и разные ситуации, когда тебе «в наследство» достается «код», который нужно здесь и сейчас подкрутить и чтобы оно быстро завелось. Бывает всякое и далеко не всегда можно все быстро «разрулить», и сделать как положено. В подобных ситуациях нужно знать расстояние для маневра.

      А статья — отличная, впрочем, как и некоторые комментарии, что разъясняют для чего это надо.


  1. m_z
    28.03.2017 10:48

    Использование готовых итераторов и интерфейса IteratorAggregate позволяет нам значительно упростить создание собственных классов-итераторов.

    результат будет таким же, как и при собственноручной реализации интерфейса Iterator.

    Из статьи может показаться, что IteratorAggregate нужен только для готовых итераторов. Более точно: IteratorAggregate позволяет отделить код итератора от итерируемого объекта. И отличие такой реализации в том, что можно обходить один и тот же объект во вложенных foreach-циклах (т.к. итератор создается для каждого цикла свой).


    1. AlexLeonov
      28.03.2017 11:05

      Как обычно — комментарии более ценны, чем сама статья. Спасибо за дополнение!


  1. vlreshet
    28.03.2017 12:17

    [$foo, $bar, $baz] = $arr;
    
    А с какой версии php это работает? Попробовал не седьмой — не взлетает. Да и в принципе впервые о таком слышу, и документация вроде молчит…


    1. AlexLeonov
      28.03.2017 12:20

      С версии 7.1
      http://sandbox.onlinephpfunctions.com/code/4848817d5b26d623ebed8e4109aae49b064cd34b

      Нет никаких причин не обновиться :)


      1. vlreshet
        28.03.2017 12:21
        +1

        Спасибо!


        1. AlexLeonov
          28.03.2017 12:23

          Всегда пожалуйста.
          Фактически, такая запись — аналог оператора list, но чуть более умный. Можно, например, указывать, какой конкретно элемент (по ключу) в какую переменную будет развернут.