О тонкостях работы foreach в PHP

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


Внимание: этот текст подразумевает наличие базовых знаний о функциональности zval'ов в PHP, в частности вы должны знать что такое refcount и is_ref.
foreach работает с сущностями разных типов: с массивами, с простыми объектами (где перечисляются доступные свойства) и с Traversable-объектами (вернее, объектами, у которых определен внутренний обработчик get_iterator). Здесь мы, в основном, говорим о массивах, но я скажу и об остальных в самом конце.

Прежде чем приступить, пара слов о массивах и их обходе, важная для понимания контекста.


Как работает обход массивов



Массивы в PHP являются упорядоченными хеш-таблицами (элементы хеша объединены в двусвязный список) и foreach обходит массив, следуя указанному порядку.

PHP включает в себя два способа обхода массива:
  • Первый способ — внутренний указатель массива. Этот указатель является частью структуры HashTable и представляет собой просто указатель на текущий элемент хеш-таблицы. Внутренний указатель массива можно безнаказанно изменять, то есть если текущий элемент удалён, внутренний указатель массива будет перемещен на следующий.
  • Второй механизм итерации — внешний указатель массива, под названием HashPosition. Это практически то же самое, что и внутренний указатель массива, но он не является частью HashTable. Этот внешний способ итерации не безопасен к изменениям. Если вы удалите элемент, на который указывает HashPosition, то останетесь с висячим указателем, что приведёт к ошибке сегментации.


Таким образом, внешние указатели массива могут быть использованы только когда вы полностью уверены, что при обходе никакого пользовательского кода выполняться не будет. А такой код может оказаться в самом неожиданном месте, типа обработчика ошибок или деструктора. Вот почему в большинстве случаев PHP приходится использовать внутренний указатель вместо внешнего. Если бы это было иначе, PHP мог бы упасть из-за segmentation fault, как только пользователь начнет делать что-нибудь необычное.

Проблема внутреннего указателя в том, что он является частью HashTable. Так что, когда вы изменяете его, HashTable меняется вместе с ним. И коль скоро обращение к массивам в PHP делается по значению (а не по ссылке), вы вынуждены копировать массив, чтобы в цикле обходить его элементы.

Простой пример, показывающий важность копирования (кстати, не такая большая редкость), это вложенная итерация:

foreach ($array as $a) {
    foreach ($array as $b) {
        // ...
    }
}


Здесь вы хотите чтобы оба цикла были независимым, а не хитро перебрасывались одним указателем.

Итак, мы дошли до foreach.

Обход массива в foreach



Теперь вы знаете, для чего foreach приходится создавать копию массива, прежде чем обойти его. Но это явно не вся история. Сделает PHP копию или нет, зависит от нескольких факторов:

  • Если итерируемый массив является ссылкой, копирования не произойдёт, вместо этого будет выполнен addref:

    $ref =& $array; // $array has is_ref=1 now
    foreach ($array as $val) {
        // ...
    }
    

    Почему? Потому что любое изменение массива должно распространяться по ссылке, включая внутренний указатель. Если бы foreach сделал копию в этом случае, он бы разрушил семантику ссылки.
  • Если массив имеет refcount=1, копирование, опять таки, не будет выполнено. refcount=1 означает, что массив не используется в другом месте и foreach может использовать его напрямую. Если refcount больше одного, значит массив разделен с другими переменными и для того чтобы избежать изменения, foreach должен скопировать его (независимо от случая ссылки, описанного выше).
  • Если массив обходится по ссылкам (foreach ($array as &$ref)), то — независимо от функции копирования или не-копирования — массив станет ссылкой.


Итак, это первая часть тайны: функция копирования. Вторая часть это то, как текущая итерация выполняется, и она тоже довольно странная. «Обычный» образец итерации, который вы уже знаете (и который часто используется в PHP — отдельно от foreach) выглядит примерно так (псевдокод):

reset();
while (get_current_data(&data) == SUCCESS) {
    code();
    move_forward();
}

итерация foreach выглядит немного иначе:

reset();
while (get_current_data(&data) == SUCCESS) {
    move_forward();
    code();
}


Отличие в том, что move_forward() выполняется в начале, а не в конце цикла. Таким образом, когда код пользователя использует элемент $i, внутренний указатель массива уже указывает на элемент $i+1.

Такой режим работы foreach также является причиной, по которой внутренний указатель массива переходит к следующему элементу, если текущий удалён, а не к предыдущему (как вы могли бы ожидать). Всё сделано так, чтобы отлично работать с foreach (но, очевидно, со всем остальным будет работать не так хорошо, пропуская элементы).

Последствия для кода



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

Второе следствие в том, что обычно не должно быть других следствий. Поведение foreach, в основном, вполне понятно пользователю и просто работает как следует. Вас не должно волновать, как происходит копирование (и происходит ли оно вообще), и в какой конкретно момент времени перемещается указатель.

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

Большую коллекцию поведения в пограничных случаях, которые появляются, когда вы модифицируете массив в ходе итерации, можно найти в тестах PHP. Вы можете начать с этого теста, после чего изменять 012 на 013 в адресе, и так далее. Вы увидите, как поведение foreach будет проявляться в разных ситуациях (всякие комбинации ссылок и.т.д.).

А сейчас вернёмся к вашим примерам:

foreach ($array as $item) {
  echo "$item\n";
  $array[] = $item;
}
print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 2 3 4 5 1 2 3 4 5 */


Здесь $array имеет refcount=1 до цикла, так что он не будет копирован, но получит addref. Как только вы присвоите значение $array[], zval будет разделен, так что массив, к которому вы добавляете элементы и итерируемый массив будут двумя разными массивами.

foreach ($array as $key => $item) {
  $array[$key + 1] = $item + 2;
  echo "$item\n";
}

print_r($array);

/* Output in loop:    1 2 3 4 5
   $array after loop: 1 3 4 5 6 7 */


Та же ситуация, что и в первом тесте.

// Сдвигаем указатель на единицу, чтобы убедиться, что это не влияет на foreach
var_dump(each($array));

foreach ($array as $item) {
  echo "$item\n";
}

var_dump(each($array));

/* Output
  array(4) {
    [1]=>
    int(1)
    ["value"]=>
    int(1)
    [0]=>
    int(0)
    ["key"]=>
    int(0)
  }
  1
  2
  3
  4
  5
  bool(false)
*/


Снова та же история. Во время цикла foreach, у вас refcount=1 и вы получаете только addref, внутренний указатель $array будет изменён. В конце цикла указатель становится NULL (это означает что итерация закончена). each демонстрирует это, возвращая false.

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */


foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */


Функции each и reset обе обращаются по ссылке. $array имеет refcount=2 когда доходит до них, в результате чего он должен быть разделен. Снова foreach сработает на отдельном массиве.

Но эти примеры недостаточно убедительны. Поведение начинает быть по настоящему непредсказуемым, когда вы используете current в цикле:

foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */


Здесь вы должны иметь в виду, что current тоже обращается по ссылке, несмотря на то, что не изменяет массив. Это нужно, чтобы согласованно работать со всеми остальными функциями, вроде next, которые обращаются по ссылке (current, вообще-то, предпочтительно-ref функция; она может получить значение, но использует ссылку, если сможет). Ссылка означает, что массив должен быть отделён, следовательно, $array и копия $array, которую использует foreach, будут независимы. Почему вы получаете 2, а не 1, также упомянуто выше: foreach увеличивает указатель массива до начала кода пользователя, а не после. Так что, даже если код все еще работает с первым элементом, foreach уже переместил указатель ко второму.

Теперь попробуем сделать небольшое изменение:

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */


Здесь у нас is_ref=1, так что массив не копирован (так как и выше). Но сейчас когда есть is_ref, массив больше не нужно разделять, передавая по ссылке к current. Теперь current и foreach работают с одним массивом. Вы видите массив сдвинутым на единицу как раз из-за того, как foreach обращается с указателем.

То же самое вы увидите, когда будете делать обход массива по ссылкам:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */


Здесь самое важное — то, что foreach назначит нашему $array is_ref=1, когда он будет обходить его в цикле по ссылке, так что получится то же, что и выше.

Еще одна небольшая вариация, здесь мы присвоим наш массив еще одной переменной:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 1 1 1 1 1 */


Здесь refcount массива $array принимает значение 2, когда цикл начался, так что нужно сделать копию, прежде чем начинать. Таким образом, $array и массив используемый foreach будут разными с самого начала. Вот почему вы получаете ту позицию внутреннего указателя массива, которая была актуальна до начала цикла (в этом случае он был в первой позиции).

Итерация объектов



При итерации объектов имеет смысл рассмотреть два случая:

Объект не Traversable (вернее, не определен внутренний обработчик get_iterator)


В этом случае итерация происходит почти так же, как у массивов. Та же семантика копирования. Единственное отличие: foreach запустит некий дополнительный код, чтобы пропустить свойства, недоступные в текущей области видимости. Еще пара интересных фактов:

  • Для объявленных свойств PHP реоптимизирует хеш-таблицу свойств. Если вы все-таки итерируете объект, он должен реконструировать эту хеш-таблицу (что повышает использование памяти). Не то, чтобы вам следовало беспокоиться об этом, просто имейте в виду.
  • На каждой итерации хеш-таблица свойств будет получена заново, то есть PHP будет вызывать get_properties снова, и снова, и снова. Для «обычных» свойств это не так важно, но если свойства создаются динамически (это часто делают встроенные классы) — то таблица свойств будет пересчитываться каждый раз.


Объект Traversable


В этом случае всё, что сказано выше, не будет применяться никоим образом. Также PHP не будет копировать и не будет применять никакие трюки вроде увеличения указателя до прохода цикла. Я думаю что режим прохода по обходимому (Traversable) объекту куда более предсказуем и не требует дальнейшего описания.

Замена итерируемого объекта во время цикла



Другой необычный случай, который я не упомянул — PHP допускает возможность замены итерируемого объекта во время цикла. Вы можете начать с одним массивом и продолжить, заменив его на полдороге другим. Или начать с массивом, в затем заменить его объектом:

$arr = [1, 2, 3, 4, 5];
$obj = (object) [6, 7, 8, 9, 10];

$ref =& $arr;
foreach ($ref as $val) {
    echo "$val\n";
    if ($val == 3) {
        $ref = $obj;
    }
}
/* Output: 1 2 3 6 7 8 9 10 */


Как видите, PHP просто начал обходить другую сущность, как только произошла замена.

Изменение внутреннего указателя массива во время итерации



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

Тут вы можете получить не то, что ожидали: если вызывать next или prev в теле цикла (в случае передачи по ссылке), вы увидите, что внутренний указатель переместился, но это никак не повлияло на поведение итератора. Причина в том, что foreach делает бекап текущей позиции и хеша текущего элемента в HashPointer после каждого прохода цикла. На следующей проходе foreach проверит, не менялась ли позиция внутреннего указателя и попытается восстановить ее, используя этот хеш.

Давайте посмотрим что означает «попытается». Первый пример показывает, как изменение внутреннего указателя не меняет режим foreach:

$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5


Теперь давайте попробуем сделать unset элементу, к которому обратится foreach при первом проходе (ключ 1):

$array = [1, 2, 3, 4, 5];
$ref =& $array;
foreach ($array as $value) {
    var_dump($value);
    unset($array[1]);
    reset($array);
}
// output: 1, 1, 3, 4, 5


Тут вы увидите, что счетчик сброшен, так как не удалось найти элемент с подходящим хешом.

Имейте в виду, хеш — всего лишь хеш. Случаются коллизии. Попробуем теперь так:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
    unset($array['EzFY']);
    $array['FYFZ'] = 4;
    reset($array);
    var_dump($value);
}
// output: 1 1 3 4


Работает так, как мы и ожидали. Мы удалили ключ EzFY (тот, где как раз был foreach), так что был сделан сброс. Также мы добавили дополнительный ключ, поэтому в конце мы видим 4.

И вот тут приходит неведомое. Что произойдёт, если заменить ключ FYFY с FYFZ? Давайте попробуем:

$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
$ref =& $array;
foreach ($array as $value) {
    unset($array['EzFY']);
    $array['FYFY'] = 4;
    reset($array);
    var_dump($value);
}
// output: 1 4


Сейчас цикл перешёл непосредственно к новому элементу, пропуская всё остальное. Это потому что ключ FYFY имеет коллизию с EzFY (вообще-то, все ключи из этого массива тоже). Более этого, элемент FYFY находится по тому же адресу в памяти, что и элемент EzFY который только что был удален. Так что для PHP это будет та же самая позиция с тем же хешом. Позиция «восстановлена» и происходит переход к концу массива.
Поделиться публикацией
Ой, у вас баннер убежал!

Ну. И что?
Реклама
Комментарии 56
  • –5
    У меня есть один вопрос к разработчикам на php. Ребята, объясните мне, почему вы называете словарь (отображение ключей на значения) массивом? Этот вопрос терзает меня уже долгие годы и я никак не найду на него ответ. В том же пайтоне, эти вещи честно называются словарями, списками и т.д. И это единственный язык, в процессе изучения которого, я не понимал, почему тернарный вопросик возвращает мне не то, что нужно. Ах да, потому, что он тут работает с заду на перед.

    И это, наверное, единственный язык, который при выходе за пределы так называемого массива создает недостающий элемент и воспринимает его как null тем самым завершая цикл.

    Да, прошу прощения, накипело.
    • 0
      Не ради троллинга php, но мне тоже интересно почему было принято такое решение. Думаю для этого были какие-то причины.
      • +3
        Если вы про тернарный оператор, то вот здесь есть обсуждение.
        Я вот и не подозревал о том, что такое может случиться, просто потому что привык для собственного удобства чтения везде ставить скобки.
      • +17
        Трудно, наверное, когда название чего-то приносит страдания долгие годы.
        Что же они творят-то, бесстыдники-создатели php!
        • +15
          Потому что в PHP «массив» является одновременно массивом и упорядоченным словарем. Позволяет не плодить лишних сущностей. По-моему, это очень удобная фишка, которую другие языки (тот же JS) не поддерживают.

          Сам термин «массив», наверное, не слишком точный, но тут уж ничего не поделаешь, таков PHP ;).
          • 0
            Я думаю, потому, что для его создания используется ключевое слово array. Хотя согласен, это не массив в привычном понимании.
            • 0
              А что не устраивает собственно в названии «массив»?)
              Все структуры, которые Вы перечислили имеют одну общую черту: «упорядоченное отображение, которое устанавливает соответствие между значением и ключом», почему бы не назвать все это одним словом по самой элементарной структуре данных — «Массив»?)

              И, кстати, зря Вы упомянули пайтон, потому что я ни разу не встречал человека, который знает все встроенные типы данных(http://docs.python.org/2/library/datatypes.html), а вот любой программист PHP прекрасно их знает(http://php.net/manual/ru/language.types.php).
              • 0
                Вы правда считаете, что python программист при перечислении типов данных должен упомянуть datetime, а PHP программиист не должен говорить про DateTime?
                • 0
                  «Встроенные» — это __builtins__

                  >>> [item for item in dir(__builtins__)
                  ...  if isinstance(getattr(__builtins__, item), type) and item.lower() == item]
                  
                  ['bool', 'bytearray', 'bytes', 'classmethod', 'complex', 'dict', 'enumerate', 'filter', 'float', 'frozenset', 'int', 'list', 'map', 'memoryview', 'object', 'property', 'range', 'reversed', 'set', 'slice', 'staticmethod', 'str', 'super', 'tuple', 'type', 'zip']
                  
                  • 0
                    Ну тут часть — это функции преобразования. В том же php array_combine() не является типом, как zip().
                    Так что типов основных совсем мало.
                • 0
                  А почему в Perl его называют HASHем не возникает вопрос?
                  Наверное потому что ru.wikipedia.org/wiki/%C0%F1%F1%EE%F6%E8%E0%F2%E8%E2%ED%FB%E9_%EC%E0%F1%F1%E8%E2
                  Читаем определение, дочитываем до конца и делаем выводы
                  • 0
                    Потому что в PHP и словари, и просто массивы заводятся с помощью ключевого слова array.
                    И обычный массив в PHP — это словарь, ключи, которого индексы.

                    И в словарь, который на самом деле словарь, можно сделать команду push в PHP.
                  • 0
                    foreach работает со всем, что поддерживает интерфейс Traversable: с массивами, с простыми объектами (где перечисляются доступные свойства) и с Traversable-объектами (вернее, объектами, у которых определен внутренний обработчик get_iterator)
                    Эта формулировка явно смущает, ведь ни массивы, ни простые объекты не поддерживают Traversable.
                    (Для проверки достаточно попробовать передать значение в iterator_to_array или применить instanceof.)
                    • 0
                      В оригинале это звучит так: "foreach works with all kinds of traversables".
                      Тут я, скорее всего, плохо перевел. Какую формулировку предложили бы вы?
                      • 0
                        В оригинале слово traversable используется в общем смыле. Перевести можно, например, так:
                        "foreach поддерживает значения разных типов, например, обычные массивы, обычные объекты, объекты, поддерживающие Traversable".

                        Меня оригинальная фраза тоже смущает, но по другой причине. В ней «разнообразные» применяется к набору всего лишь из двух: массивы и объекты.
                        • 0
                          Думаю, вы правы. Исправил в тексте, с небольшим изменением. Спасибо за корректировку.
                    • 0
                      Level up! Предупреждён — значит, вооружён.
                      • +2
                        Лично у меня в голове отложилось только одно: чтобы не попасть в непонятное, надо всеми силами избегать модифицирования обходимого через foreach массива. Но таки да, во всем этом видна определенная логика, о наличии которой я раньше и не подозревал.
                        • –1
                          Эмпирически выведенный закон «хочешь действительно понять фреймворк — читай не документацию, а его код» применим и к языкам программирования.
                          • +4
                            Мне лично вообще трудно себе представить задачу, в которой может понадобиться модифицировать массив при обходе.
                            Мне кажется, что в 90% случаев foreach используется для чтения данных из массива
                            • 0
                              почему, такие ситуации легко представить
                              хотя я бы в них использовал while

                              массив не меняется — foreach
                              меняется — while
                              • 0
                                В PHP — и правда, сложно представить. Но такие задачи часто встречаются, например, при написании игр. Простейший пример: по экрану разбросаны квадраты, при нажатии на любой он взрывается. Тогда у нас получится цикл по коллекции объектов на экране, каждая итерация которого проверяет нажатие для конкретного объекта, и если оно успешно — сам объект удаляется из коллекции, зато добавляются новые (спрайт взрыва и искры).
                                • +2
                                  тогда у нас получится цикл по коллекции объектов на экране, каждая итерация которого проверяет нажатие для конкретного объекта, и если оно успешно

                                  OMG! это еще хуже, чем изменять массивы в цикле на php. Для этого существуют эвенты у каждого объекта и их обработчики. Представьте, что у вас 100к таких объектов? Цикл по всем и перестройка? Да ну нафиг-нафиг.
                                  • 0
                                    Неправда. Вы когда-нибудь писали игры? Специфика такова, что одновременно на экране объектов довольно немного (у меня в среднем бывало от 50 до 500), и каждый из них нужно обновить и отрисовать со скоростью 50+ FPS. Система событий в таких условиях никакого выигрыша не даст.
                                  • 0
                                    Почему нельзя сделать это через for?
                                    • 0
                                      Дело в том, что с объектами удобнее работать, когда они не просто валяются в общем массиве, но сгруппированы. Сложный объект из нескольких составных частей, которые должны двигаться вместе, удобне представить в виде группы объектов, которая имеет общую точку, относительно которой позиционируются вложенные объекты. В таком случае у нас получается по вложенному циклу на каждый такой сгруппированный объект, и понять, какой индекс цикла нужно ручками подкрутить на единицу вверх или вниз, становится абсолютно невозможно.
                                      • 0
                                        В этом случае не стоило бы явно прописать итератор для объекта?
                                        • 0
                                          Например?
                                          • 0
                                            Я говорю о том, чтобы руками указать, в каком порядке и каким образом foreach следует обходить вашу коллекцию.

                                            p.s. Хотя не вполне понимаю, что мы тут обсуждаем. Сдается мне, врядли вы собираетесь писать такого рода игру на PHP, так что как он пытается защитить итератор от перезаписи тут уже не так важно.
                            • –1
                              Ужас, психоделическая картина, коллизии, шизофрения. Опасно работать программистом!
                              • 0
                                Не ради холивара, но все-таки не могу не сказать, что в C# обход коллекций сделан куда более логично и понятно. Никаких «внутренних индексов массива» нет. На каждый обход последовательности создается специальный объект — итератор, который хранит в себе текущий индекс и удаляется после окончания обхода. Если последовательность была изменена во время обхода, итератор бросает исключение.
                                • 0
                                  это легаси из предыдущих версий.внутренний итератор, который нужен для случаев, когда вы обходите массив при помощи методов next/prev/current/reset. соответственно несколько альтернативных методов обхода как-то должны были взаимодействовать :)
                                • –1
                                  Простите, что не в тему, но уже неоднократно сталкиваюсь с проблемой count, когда нет массива(переменной) или он пустой отрабатывает на true. Может кто сталкивался, в чём подвох не могу понять, заранее спасибо.
                                  • 0
                                    В каком смысле true? Он же целое число возвращает. На пустой массив или на null он возвращает 0.
                                    • +3
                                      Прочтите внимательней документацию. count (array ()) = 0, count (false) = 1
                                      • 0
                                        Давно уже не объявлять переменные считает дурным стилем.
                                        Добавление $arr = array(); во первых много времени не занимает, во вторых как способ принудительной типизации тоже неплохо
                                        • +2
                                          Может Вам empty подойдёт?
                                          • 0
                                            наверно потому что он конвертит не массив в массив с одним елементом а потом считает.
                                            • 0
                                              use count((array)$any_data)
                                              • 0
                                                :^ ) Дык в этом то и соль, что так нельзя.
                                                var_dump((array)false);
                                                // array (size=1)
                                                //   0 => boolean false
                                                echo count((array)false); // 1
                                                
                                                • 0
                                                  Ну да, я имел ввиду что false это тоже что-то, в отличии от null и хпх пытается превратить это в массив что бы посчитать. Немного криво, согласен, но логика ясна хотя бы
                                                  • 0
                                                    мне кажется что это наиболее ожидаемое поведение:
                                                    значение null — означает, что переменная не определена или обнулена
                                                    значение false — означает, что переменная определена и имеет тип boolean со значением false
                                            • 0
                                              PHP может падать с segfault?
                                              • 0
                                                В теории, от пользовательского кода, нет.
                                                • 0
                                                  А как же.
                                                  PDO-mysql + attr_emulate_prepares=false + PHP 5.4 + FreeBSD + mod_php — и дело в шляпе.
                                                  • +3
                                                    Может.
                                                    Компактные кроссверсионные примеры:
                                                    class A
                                                    {
                                                        function __destruct()
                                                        {
                                                            return new A;
                                                        }
                                                    }
                                                    $r=new A;
                                                    

                                                    function e() {
                                                            set_error_handler("e")|A;
                                                            }
                                                    e();
                                                    
                                                  • 0
                                                    А можно поподробнее про
                                                    Это потому что ключ FYFY имеет коллизию с EzFY (вообще-то, все ключи из этого массива тоже)
                                                    • 0
                                                      Подозреваю, что подробности следует искать здесь:
                                                      github.com/php/php-src/blob/master/Zend/zend_hash.h
                                                      • 0
                                                        Меня интересует, когда коллизии могут проявлять себя.
                                                        Например isset() выдает всё корректно на приведенных ключах.
                                                        • 0
                                                          Перечитал оригинал. Хеш берется от значения, а не от ключа.
                                                          Вроде, в потом в оригинале идет «FYFY key collides with EzFY (actually all keys in that array collide)».

                                                          Любопытно. Причем, обращаясь к ключу, пхп тоже вычисляет хеш…
                                                        • 0
                                                          Да, про это я в курсе.
                                                          Я не понял про совпадение адресов. Ведь при совпадении хешей все равно сравниваются точные значения ключей.
                                                          • 0
                                                            Я не знаток внутренностей, но по первой ссылке написано:
                                                            For those who don’t know how hashtables work: When you write $array[$key] in PHP the $key is run through a fast hash function that yields an integer. This integer is then used as an offset into a “real” C array (here “array” means “chunk of memory”).

                                                            Because every hash function has collisions this C array doesn’t actually store the value we want, but a linked list of possible values. PHP then walks these values one by one and does a full key comparison until it finds the right element with our $key.
                                                            • 0
                                                              Ну любой хеш так работает (либо я чего-то не знаю). Как я и написал выше:
                                                              >>PHP then walks these values one by one and does a full key comparison until it finds the right element
                                                              После грубого сравнения хешей (была найдена нужная линия хеша) идет уже точное сравнение значений в пределах связного списка в найденной линии.

                                                              Может работать меденно? Да. находить не то? Как? Мне вот не понятно всё еще.
                                                              Один вариант: unset освобождает один из элементов линии хеша, и в ту же ячейку записывается новое (другое) значение при присвоении. В итоге внешняя ссылка на эту ячейку остается корректной. Но это имхо.
                                                              Да и все равно я такой код, как в примерах статьи, не пишу. И не сталкивался с подобным.
                                                        • 0
                                                          Мне кажется, я нашел еще на эту тему, причем из недавнего:
                                                          habrahabr.ru/post/162685/
                                                        • 0
                                                          Расскажите подробнее про коллизию хеша из самого последнего примера. У меня шок. Мне страшно дальше писать код!

                                                          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                          Самое читаемое