Позвольте мне префикс этого, сказав, что я знаю, что такое foreach
, что он делает и как его использовать. Этот вопрос касается того, как это работает под капотом, и мне не нужны ответы типа «вот как вы зацикливаете массив с помощью foreach
».
Долгое время я предполагал, что foreach
работает с самим массивом. Затем я нашел много ссылок на тот факт, что он работает с копией массива, и с тех пор я решил, что это конец истории. Но недавно я начал дискуссию по этому поводу, и после небольшого экспериментирования обнаружил, что на самом деле это не 100% правда.
Позвольте мне показать, что я имею в виду. Для следующих тестовых случаев мы будем работать со следующим массивом:
$array = array(1, 2, 3, 4, 5);
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 */
Это ясно показывает, что мы не работаем напрямую с исходным массивом - иначе цикл продолжался бы вечно, поскольку мы постоянно помещаем элементы в массив во время цикла. Но на всякий случай:
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 запускается в первый раз, внутренний указатель массива автоматически сбрасывается на первый элемент массива.
Верно ... похоже, это предполагает, что foreach
полагается на указатель на массив исходного массива. Но мы только что доказали, что не работаем с исходным массивом , верно? Не совсем так.
// Move the array pointer on one to make sure it doesn't affect the loop
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)
*/
Итак, несмотря на то, что мы не работаем напрямую с исходным массивом, мы работаем напрямую с указателем исходного массива - тот факт, что указатель находится в конце массива в конце цикла, показывает это. Но это не может быть правдой - если бы это было так, то тестовый пример 1 зацикливался бы бесконечно.
В руководстве по PHP также говорится:
Поскольку foreach полагается на указатель внутреннего массива, его изменение в цикле может привести к неожиданному поведению.
Что ж, давайте выясним, что это за «неожиданное поведение» (технически любое поведение является неожиданным, поскольку я больше не знаю, чего ожидать).
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 */
... в этом нет ничего неожиданного, на самом деле, похоже, это подтверждает теорию "копии источника".
Вопрос
Что здесь происходит? Мой C-fu недостаточно хорош, чтобы я мог сделать правильный вывод, просто взглянув на исходный код PHP, я был бы признателен, если бы кто-нибудь мог перевести его на английский для меня.
Мне кажется, что foreach
работает с копией массива, но устанавливает указатель массива исходного массива в конец массива после цикла.
- Это правильно и вся история?
- Если нет, то что он на самом деле делает?
- Есть ли ситуация, когда использование функций, регулирующих указатель массива (
each()
,reset()
и др.) Во времяforeach
может повлиять на результат цикла?
7 ответов
foreach
поддерживает итерацию по трем различным типам значений:
- Массивы
- Нормальные объекты
- объекты
Traversable
Далее я постараюсь точно объяснить, как итерация работает в разных случаях. Безусловно, самый простой случай - это объекты Traversable
, поскольку для них foreach
по сути является только синтаксическим сахаром для кода в следующих строках:
foreach ($it as $k => $v) { /* ... */ }
/* translates to: */
if ($it instanceof IteratorAggregate) {
$it = $it->getIterator();
}
for ($it->rewind(); $it->valid(); $it->next()) {
$v = $it->current();
$k = $it->key();
/* ... */
}
Для внутренних классов фактических вызовов методов можно избежать за счет использования внутреннего API, который по сути просто отражает интерфейс Iterator
на уровне C.
Итерация массивов и простых объектов значительно сложнее. Прежде всего, следует отметить, что в PHP «массивы» на самом деле являются упорядоченными словарями, и они будут перемещаться в соответствии с этим порядком (который соответствует порядку вставки, если вы не использовали что-то вроде sort
). Это противоположно итерации по естественному порядку ключей (как часто работают списки на других языках) или отсутствию определенного порядка вообще (как часто работают словари на других языках).
То же самое применимо и к объектам, поскольку свойства объекта можно рассматривать как другой (упорядоченный) словарь, отображающий имена свойств в их значения, плюс некоторая обработка видимости. В большинстве случаев свойства объекта фактически не сохраняются таким довольно неэффективным способом. Однако, если вы начнете перебирать объект, обычно используемое упакованное представление будет преобразовано в настоящий словарь. В этот момент итерация простых объектов становится очень похожей на итерацию массивов (вот почему я здесь особо не обсуждаю итерацию простых объектов).
Все идет нормально. Перебирать словарь не может быть слишком сложно, правда? Проблемы начинаются, когда вы понимаете, что массив / объект может изменяться во время итерации. Это может произойти несколькими способами:
- Если вы выполняете итерацию по ссылке с помощью
foreach ($arr as &$v)
, то$arr
превращается в ссылку, и вы можете изменить ее во время итерации. - В PHP 5 то же самое применяется, даже если вы выполняете итерацию по значению, но массив заранее был ссылкой:
$ref =& $arr; foreach ($ref as $v)
- Объекты имеют семантику передачи байтов, которая для большинства практических целей означает, что они ведут себя как ссылки. Таким образом, объекты всегда можно изменить во время итерации.
Проблема с разрешением изменений во время итерации возникает в том случае, когда элемент, на котором вы сейчас находитесь, удален. Допустим, вы используете указатель, чтобы отслеживать, в каком элементе массива вы находитесь в данный момент. Если этот элемент теперь освобожден, у вас останется висящий указатель (обычно это приводит к segfault).
Есть разные способы решения этой проблемы. В этом отношении PHP 5 и PHP 7 существенно различаются, и ниже я опишу оба поведения. Резюмируя, можно сказать, что подход PHP 5 был довольно глупым и приводил ко всем видам странных краевых проблем, в то время как более активный подход PHP 7 приводит к более предсказуемому и последовательному поведению.
В качестве последней предварительной подготовки следует отметить, что PHP использует подсчет ссылок и копирование при записи для управления памятью. Это означает, что если вы «копируете» значение, вы фактически просто повторно используете старое значение и увеличиваете его счетчик ссылок (refcount). Только после того, как вы выполните какую-либо модификацию, будет сделана настоящая копия (называемая «дублированием»). См. Вам лгут для более подробного введение по этой теме.
PHP 5
Указатель внутреннего массива и HashPointer
Массивы в PHP 5 имеют один выделенный «внутренний указатель на массив» (IAP), который должным образом поддерживает модификации: всякий раз, когда элемент удаляется, будет проверяться, указывает ли IAP на этот элемент. Если это так, вместо этого выполняется переход к следующему элементу.
Хотя foreach
действительно использует IAP, существует дополнительная сложность: существует только один IAP, но один массив может быть частью нескольких циклов foreach
:
// Using by-ref iteration here to make sure that it's really
// the same array in both loops and not a copy
foreach ($arr as &$v1) {
foreach ($arr as &$v) {
// ...
}
}
Для поддержки двух одновременных циклов только с одним указателем внутреннего массива foreach
выполняет следующие махинации: перед выполнением тела цикла foreach
создает резервную копию указателя на текущий элемент и его хэш в отдельном foreach HashPointer
. После выполнения тела цикла IAP вернется к этому элементу, если он все еще существует. Однако, если элемент был удален, мы просто будем использовать его там, где в данный момент находится IAP. Эта схема в некотором роде работает, но есть много странного поведения, которое вы можете избежать, некоторые из которых я продемонстрирую ниже.
Дублирование массива
IAP - это видимая особенность массива (предоставляемая через семейство функций current
), поскольку такие изменения в IAP считаются модификациями в семантике копирования при записи. К сожалению, это означает, что foreach
во многих случаях вынужден дублировать массив, по которому выполняется итерация. Точные условия:
- Массив не является ссылкой (is_ref = 0). Если это ссылка, то изменения в ней предполагается для распространения, поэтому ее не следует дублировать.
- В массиве refcount> 1. Если
refcount
равно 1, то массив не используется совместно, и мы можем изменять его напрямую.
Если массив не дублируется (is_ref = 0, refcount = 1), то только его refcount
будет увеличиваться (*). Кроме того, если foreach
используется по ссылке, то (потенциально дублированный) массив будет преобразован в ссылку.
Рассмотрим этот код как пример дублирования:
function iterate($arr) {
foreach ($arr as $v) {}
}
$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);
Здесь $arr
будет продублирован, чтобы предотвратить утечку изменений IAP на $arr
на $outerArr
. В условиях приведенных выше условий массив не является ссылкой (is_ref = 0) и используется в двух местах (refcount = 2). Это требование неудачно и является артефактом неоптимальной реализации (здесь нет никаких проблем с модификацией во время итерации, поэтому нам действительно не нужно использовать IAP в первую очередь).
(*) Увеличение refcount
здесь звучит безобидно, но нарушает семантику копирования при записи (COW): это означает, что мы собираемся изменить IAP массива refcount = 2, в то время как COW требует, чтобы модификации могли только выполняться для значений refcount = 1. Это нарушение приводит к видимому для пользователя изменению поведения (в то время как COW обычно прозрачен), поскольку изменение IAP в повторяемом массиве будет наблюдаться - но только до первой модификации массива, не связанной с IAP. Вместо этого были бы три «действительных» варианта: а) всегда дублировать, б) не увеличивать refcount
и, таким образом, позволять произвольно изменять повторяемый массив в цикле, или в) не использовать IAP вообще (решение PHP 7).
Порядок продвижения по должности
Есть одна последняя деталь реализации, о которой вы должны знать, чтобы правильно понимать примеры кода ниже. «Обычный» способ прохождения некоторой структуры данных в псевдокоде выглядел бы примерно так:
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
code();
move_forward(arr);
}
Однако foreach
, будучи довольно особенной снежинкой, предпочитает действовать немного иначе:
reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
move_forward(arr);
code();
}
А именно, указатель массива уже перемещен вперед до выполнения тела цикла. Это означает, что пока тело цикла работает с элементом $i
, IAP уже находится в элементе $i+1
. По этой причине в примерах кода, показывающих модификацию во время итерации, всегда будет unset
элемент следующий , а не текущий.
Примеры: ваши тестовые примеры.
Три описанных выше аспекта должны дать вам в основном полное представление об особенностях реализации foreach
, и мы можем перейти к обсуждению некоторых примеров.
На этом этапе легко объяснить поведение ваших тестовых примеров:
В тестовых случаях 1 и 2
$array
начинается с refcount = 1, поэтому он не будет дублироватьсяforeach
: увеличивается толькоrefcount
. Когда тело цикла впоследствии изменяет массив (в этой точке refcount = 2), в этой точке произойдет дублирование. Foreach продолжит работу над неизмененной копией$array
.В тестовом примере 3 снова массив не дублируется, поэтому
foreach
будет изменять IAP переменной$array
. В конце итерации IAP равен NULL (что означает, что итерация завершена), на чтоeach
указывает, возвращаяfalse
.В тестовых примерах 4 и 5 как
each
, так иreset
являются ссылочными функциями.$array
имеетrefcount=2
, когда он передается им, поэтому он должен быть продублирован. Таким образом,foreach
снова будет работать над отдельным массивом.
Примеры: эффекты current
в foreach
Хороший способ показать различные варианты поведения дублирования - это наблюдать за поведением функции current()
внутри цикла foreach
. Рассмотрим этот пример:
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 2 2 2 2 */
Здесь вы должны знать, что current()
- это функция по ссылке (на самом деле: Preferred-ref), даже если она не изменяет массив. Это должно быть для того, чтобы хорошо работать со всеми другими функциями, такими как next
, которые все являются ссылочными. Передача по ссылке подразумевает, что массив должен быть разделен, и поэтому $array
и foreach-array
будут разными. Причина, по которой вы получаете 2
вместо 1
, также упоминалась выше: foreach
перемещает указатель массива до выполнения кода пользователя, а не после. Таким образом, хотя код находится на первом элементе, foreach
уже переместил указатель на второй.
Теперь попробуем небольшую модификацию:
$ref = &$array;
foreach ($array as $val) {
var_dump(current($array));
}
/* Output: 2 3 4 5 false */
Здесь у нас есть случай is_ref = 1, поэтому массив не копируется (как и выше). Но теперь, когда это ссылка, массив больше не нужно дублировать при переходе к функции by-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 */
Здесь счетчик ссылок $array
равен 2, когда цикл запущен, так что на этот раз нам действительно нужно выполнить дублирование заранее. Таким образом, $array
и массив, используемый foreach, будут полностью отделены с самого начала. Вот почему вы получаете позицию IAP, где бы она ни находилась до цикла (в данном случае это была первая позиция).
Примеры: изменение во время итерации
Попытка учесть модификации во время итерации - это то место, откуда возникли все наши проблемы с foreach, поэтому мы рассмотрим несколько примеров для этого случая.
Рассмотрим эти вложенные циклы по одному и тому же массиву (где итерация по ссылке используется, чтобы убедиться, что это действительно один и тот же массив):
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Output: (1, 1) (1, 3) (1, 4) (1, 5)
Ожидается, что здесь отсутствует (1, 2)
в выходных данных, поскольку элемент 1
был удален. Что, вероятно, неожиданно, так это то, что внешний цикл останавливается после первого элемента. Это почему?
Причина этого заключается в описанном выше взломе вложенного цикла: перед запуском тела цикла текущая позиция IAP и хэш копируются в HashPointer
. После тела цикла он будет восстановлен, но только если элемент все еще существует, в противном случае вместо него используется текущая позиция IAP (какой бы она ни была). В приведенном выше примере это именно тот случай: текущий элемент внешнего цикла был удален, поэтому он будет использовать IAP, который уже был отмечен внутренним циклом как завершенный!
Еще одним следствием механизма резервного копирования + восстановления HashPointer
является то, что изменения IAP через reset()
и т. Д. Обычно не влияют на foreach
. Например, следующий код выполняется так, как если бы reset()
вообще не присутствовал:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
var_dump($value);
reset($array);
}
// output: 1, 2, 3, 4, 5
Причина в том, что, хотя reset()
временно изменяет IAP, он будет восстановлен до текущего элемента foreach после тела цикла. Чтобы заставить reset()
повлиять на цикл, вы должны дополнительно удалить текущий элемент, чтобы механизм резервного копирования / восстановления отказал:
$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
Но эти примеры все еще разумны. Настоящее веселье начинается, если вы помните, что восстановление HashPointer
использует указатель на элемент и его хэш, чтобы определить, существует ли он еще. Но: у хэшей есть коллизии, и указатели можно использовать повторно! Это означает, что при тщательном выборе ключей массива мы можем заставить foreach
поверить, что удаленный элемент все еще существует, поэтому он перейдет непосредственно к нему. Пример:
$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
Здесь обычно следует ожидать вывода 1, 1, 3, 4
в соответствии с предыдущими правилами. Как происходит то, что 'FYFY'
имеет тот же хэш, что и удаленный элемент 'EzFY'
, а распределитель повторно использует ту же самую ячейку памяти для хранения элемента. Таким образом, foreach завершается прямым переходом к вновь вставленному элементу, сокращая, таким образом, цикл.
Подстановка повторяющейся сущности во время цикла
Последний странный случай, о котором я хотел бы упомянуть, заключается в том, что 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 просто начнет итерацию другого объекта с самого начала, как только произойдет замена.
PHP 7
Итераторы хеш-таблиц
Если вы все еще помните, основная проблема с итерацией массива заключалась в том, как обрабатывать удаление элементов в середине итерации. В PHP 5 для этой цели использовался единственный внутренний указатель массива (IAP), что было несколько неоптимально, поскольку один указатель массива приходилось растягивать для поддержки нескольких одновременных циклов foreach и взаимодействия с reset()
и т. Д. . более того.
PHP 7 использует другой подход, а именно поддерживает создание произвольного количества внешних безопасных итераторов хеш-таблицы. Эти итераторы должны быть зарегистрированы в массиве, с этого момента они имеют ту же семантику, что и IAP: если элемент массива удален, все итераторы хэш-таблицы, указывающие на этот элемент, будут перемещены к следующему элементу.
Это означает, что foreach
больше не будет использовать IAP вообще . Цикл foreach
не будет абсолютно никак влиять на результаты current()
и т. Д., А его собственное поведение никогда не будет зависеть от таких функций, как reset()
и т. Д.
Дублирование массива
Еще одно важное изменение между PHP 5 и PHP 7 связано с дублированием массивов. Теперь, когда IAP больше не используется, итерация массива по значению будет выполнять только приращение refcount
(вместо дублирования массива) во всех случаях. Если массив изменяется во время цикла foreach
, в этот момент произойдет дублирование (в соответствии с копированием при записи), и foreach
продолжит работу со старым массивом.
В большинстве случаев это изменение прозрачно и не имеет никакого другого эффекта, кроме повышения производительности. Однако есть один случай, когда это приводит к другому поведению, а именно в случае, когда массив заранее был ссылкой:
$array = [1, 2, 3, 4, 5];
$ref = &$array;
foreach ($array as $val) {
var_dump($val);
$array[2] = 0;
}
/* Old output: 1, 2, 0, 4, 5 */
/* New output: 1, 2, 3, 4, 5 */
Раньше особым случаем была итерация ссылочных массивов по значению. В этом случае дублирования не произошло, поэтому все изменения массива во время итерации будут отражены в цикле. В PHP 7 этого особого случая нет: итерация массива по значению будет всегда работать с исходными элементами, игнорируя любые изменения во время цикла.
Это, конечно, не относится к итерации по ссылке. Если вы выполняете итерацию по ссылке, все изменения будут отражены в цикле. Интересно, что то же самое верно и для итерации простых объектов по значению:
$obj = new stdClass;
$obj->foo = 1;
$obj->bar = 2;
foreach ($obj as $val) {
var_dump($val);
$obj->bar = 42;
}
/* Old and new output: 1, 42 */
Это отражает семантику объектов по дескрипторам (т. Е. Они ведут себя как ссылки даже в контекстах по значению).
Примеры
Давайте рассмотрим несколько примеров, начиная с ваших тестовых случаев:
Тестовые случаи 1 и 2 сохраняют тот же результат: итерация массива по значению всегда продолжает работать с исходными элементами. (В этом случае даже
refcounting
и поведение дублирования точно такое же для PHP 5 и PHP 7).Изменения в тестовом примере 3:
Foreach
больше не использует IAP, поэтому цикл не влияет наeach()
. Он будет иметь одинаковый результат до и после.Тестовые примеры 4 и 5 остаются прежними:
each()
иreset()
будут дублировать массив перед изменением IAP, в то время какforeach
по-прежнему использует исходный массив. (Не то чтобы изменение IAP имело бы значение, даже если бы массив был общим.)
Второй набор примеров был связан с поведением current()
при различных конфигурациях reference/refcounting
. Это больше не имеет смысла, поскольку цикл полностью не влияет на current()
, поэтому его возвращаемое значение всегда остается неизменным.
Однако мы получаем некоторые интересные изменения при рассмотрении модификаций во время итерации. Я надеюсь, что вы сочтете новое поведение более разумным. Первый пример:
$array = [1, 2, 3, 4, 5];
foreach ($array as &$v1) {
foreach ($array as &$v2) {
if ($v1 == 1 && $v2 == 1) {
unset($array[1]);
}
echo "($v1, $v2)\n";
}
}
// Old output: (1, 1) (1, 3) (1, 4) (1, 5)
// New output: (1, 1) (1, 3) (1, 4) (1, 5)
// (3, 1) (3, 3) (3, 4) (3, 5)
// (4, 1) (4, 3) (4, 4) (4, 5)
// (5, 1) (5, 3) (5, 4) (5, 5)
Как видите, внешний цикл больше не прерывается после первой итерации. Причина в том, что у обоих циклов теперь есть полностью отдельные итераторы хэш-таблицы, и больше не происходит перекрестного заражения обоих циклов через общий IAP.
Еще один странный крайний случай, который сейчас исправлен, - это странный эффект, который вы получаете, когда удаляете и добавляете элементы, которые имеют одинаковый хэш:
$array = ['EzEz' => 1, 'EzFY' => 2, 'FYEz' => 3];
foreach ($array as &$value) {
unset($array['EzFY']);
$array['FYFY'] = 4;
var_dump($value);
}
// Old output: 1, 4
// New output: 1, 3, 4
Раньше механизм восстановления HashPointer переходил прямо к новому элементу, потому что он «выглядел» так, как будто он был таким же, как удаленный элемент (из-за столкновения хэша и указателя). Поскольку мы больше ни в чем не полагаемся на хэш элемента, это больше не проблема.
$foo = $array
перед циклом;)
Bucket
являются частью двусвязного списка для хеш-коллизий, а также частью двусвязного списка для порядка;)
iterate($outerArr);
, а не iterate($arr);
где-то.
В примере 3 вы не изменяете массив. Во всех других примерах вы изменяете либо содержимое, либо указатель внутреннего массива. Это важно, когда дело касается массивов PHP из-за семантики оператора присваивания.
Оператор присваивания для массивов в PHP больше похож на ленивого клона. Присвоение одной переменной другой, содержащей массив, приведет к клонированию массива, в отличие от большинства языков. Однако фактическое клонирование не будет выполнено, если в этом нет необходимости. Это означает, что клонирование будет иметь место только при изменении любой из переменных (копирование при записи).
Вот пример:
$a = array(1,2,3);
$b = $a; // This is lazy cloning of $a. For the time
// being $a and $b point to the same internal
// data structure.
$a[] = 3; // Here $a changes, which triggers the actual
// cloning. From now on, $a and $b are two
// different data structures. The same would
// happen if there were a change in $b.
Возвращаясь к вашим тестовым примерам, вы легко можете представить, что foreach
создает своего рода итератор со ссылкой на массив. Эта ссылка работает точно так же, как переменная $b
в моем примере. Однако итератор вместе со ссылкой действуют только во время цикла, а затем они оба отбрасываются. Теперь вы можете видеть, что во всех случаях, кроме 3, массив изменяется во время цикла, в то время как эта дополнительная ссылка жива. Это запускает клон, и это объясняет, что здесь происходит!
Вот отличная статья о другом побочном эффекте этого поведения копирования при записи: Тернарный оператор PHP: быстро или нет?
each()
в конце первого тестового примера, где мы видим, что указатель исходного массива указывает на второй элемент, поскольку массив был изменен во время первой итерации. Это также, кажется, демонстрирует, что foreach
перемещает указатель массива перед выполнением блока кода цикла, чего я не ожидал - я думал, что он сделает это в конце. Большое спасибо, это хорошо меня проясняет.
Некоторые моменты, на которые следует обратить внимание при работе с foreach()
:
А) foreach
работает с предполагаемой копией исходного массива. Это означает, что foreach()
будет иметь ОБЩЕЕ хранилище данных до тех пор, пока prospected copy
не будет не созданы foreach Notes / User comments.
Б) Что запускает предполагаемую копию ? Предполагаемая копия создается на основе политики copy-on-write
, то есть всякий раз, когда массив, переданный в foreach()
, изменяется, создается клон исходного массива.
C) Исходный массив и итератор foreach()
будут иметь DISTINCT SENTINEL VARIABLES
, то есть один для исходного массива, а другой для foreach
; см. тестовый код ниже. SPL, Итераторы и Итератор массива.
Вопрос о переполнении стека адресов Как убедиться, что значение сбрасывается в цикле foreach в PHP? случаи (3,4,5) вашего вопроса.
В следующем примере показано, что each () и reset () НЕ влияют на переменные SENTINEL
. (for example, the current index variable)
итератора foreach()
.
$array = array(1, 2, 3, 4, 5);
list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";
foreach($array as $key => $val){
echo "foreach: $key => $val<br/>";
list($key2,$val2) = each($array);
echo "each() Original(inside): $key2 => $val2<br/>";
echo "--------Iteration--------<br/>";
if ($key == 3){
echo "Resetting original array pointer<br/>";
reset($array);
}
}
list($key2, $val2) = each($array);
echo "each() Original (outside): $key2 => $val2<br/>";
Вывод:
each() Original (outside): 0 => 1
foreach: 0 => 1
each() Original(inside): 1 => 2
--------Iteration--------
foreach: 1 => 2
each() Original(inside): 2 => 3
--------Iteration--------
foreach: 2 => 3
each() Original(inside): 3 => 4
--------Iteration--------
foreach: 3 => 4
each() Original(inside): 4 => 5
--------Iteration--------
Resetting original array pointer
foreach: 4 => 5
each() Original(inside): 0=>1
--------Iteration--------
each() Original (outside): 1 => 2
foreach
работает с потенциальной копией массива, но не создает фактическую копию, если она не нужна.
foreach
копирует массив в 100% случаев. Я очень хочу знать. Спасибо за ваши комментарии
for
или foreach
. Вы не увидите значительной разницы между ними, потому что фактического копирования не происходит.
SHARED data storage
зарезервировано до или до copy-on-write
, но (из моего фрагмента кода) очевидно, что всегда будет ДВА набора SENTINEL variables
по одному для original array
и другие для foreach
. Спасибо, что имеет смысл
ПРИМЕЧАНИЕ ДЛЯ PHP 7
Чтобы обновить этот ответ, поскольку он приобрел некоторую популярность: этот ответ больше не применяется с PHP 7. Как объяснено в "Обратно несовместимые изменения", в PHP 7 foreach работает с копией массива, поэтому любые изменения самого массива не отражаются в цикле foreach. Подробнее по ссылке.
Объяснение (цитата из php.net): < / сильный>
Первая форма перебирает массив, заданный array_expression. На каждой итерации значение текущего элемента присваивается $ value, а указатель внутреннего массива продвигается на единицу (поэтому на следующей итерации вы будете смотреть на следующий элемент).
Итак, в вашем первом примере у вас есть только один элемент в массиве, и когда указатель перемещается, следующий элемент не существует, поэтому после добавления нового элемента foreach заканчивается, потому что он уже «решил», что это последний элемент.
Во втором примере вы начинаете с двух элементов, и цикл foreach не находится в последнем элементе, поэтому он оценивает массив на следующей итерации и, таким образом, понимает, что в массиве появился новый элемент.
Я считаю, что все это является следствием части пояснения в документации на каждой итерации , что, вероятно, означает, что foreach
выполняет всю логику перед вызовом кода в {}
.
Контрольный пример
Если вы запустите это:
<?
$array = Array(
'foo' => 1,
'bar' => 2
);
foreach($array as $k=>&$v) {
$array['baz']=3;
echo $v." ";
}
print_r($array);
?>
Вы получите такой результат:
1 2 3 Array
(
[foo] => 1
[bar] => 2
[baz] => 3
)
Это означает, что он принял модификацию и прошел через нее, потому что она была изменена «вовремя». Но если вы сделаете это:
<?
$array = Array(
'foo' => 1,
'bar' => 2
);
foreach($array as $k=>&$v) {
if ($k=='bar') {
$array['baz']=3;
}
echo $v." ";
}
print_r($array);
?>
Ты получишь:
1 2 Array
(
[foo] => 1
[bar] => 2
[baz] => 3
)
Это означает, что этот массив был изменен, но поскольку мы изменили его, когда foreach
уже был в последнем элементе массива, он «решил» больше не выполнять цикл, и, хотя мы добавили новый элемент, мы добавили его » слишком поздно », и он не прошел.
Подробное объяснение можно прочитать на странице Как на самом деле работает PHP foreach?, где объясняется внутреннее устройство. это поведение.
Согласно документации, предоставленной руководством по PHP.
На каждой итерации значение текущего элемента присваивается $ v и внутреннему
указатель массива продвигается на единицу (поэтому на следующей итерации вы будете смотреть на следующий элемент).
Итак, согласно вашему первому примеру:
$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
$array['bar']=2;
echo($v);
}
$array
имеет только один элемент, поэтому при выполнении foreach 1 назначается $v
, и у него нет другого элемента для перемещения указателя
Но во втором примере:
$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
$array['baz']=3;
echo($v);
}
$array
имеет два элемента, поэтому теперь $ array оценивает нулевые индексы и перемещает указатель на единицу. Для первой итерации цикла добавлен $array['baz']=3;
как передача по ссылке.
Отличный вопрос, потому что многие разработчики, даже опытные, сбиты с толку тем, как PHP обрабатывает массивы в циклах foreach. В стандартном цикле foreach PHP создает копию массива, который используется в цикле. Копия удаляется сразу после завершения цикла. Это прозрачно в работе простого цикла foreach. Например:
$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
echo "{$item}\n";
}
Это выводит:
apple
banana
coconut
Таким образом, копия создается, но разработчик этого не замечает, потому что исходный массив не упоминается ни в цикле, ни после его завершения. Однако, когда вы пытаетесь изменить элементы в цикле, вы обнаруживаете, что они не изменились, когда вы закончите:
$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
$item = strrev ($item);
}
print_r($set);
Это выводит:
Array
(
[0] => apple
[1] => banana
[2] => coconut
)
Никакие изменения от оригинала не могут быть уведомлениями, на самом деле нет никаких изменений от оригинала, даже если вы явно присвоили значение $ item. Это потому, что вы работаете с $ item в том виде, в каком он отображается в копии $ set, над которой работаете. Вы можете переопределить это, взяв $ item по ссылке, например:
$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
$item = strrev($item);
}
print_r($set);
Это выводит:
Array
(
[0] => elppa
[1] => ananab
[2] => tunococ
)
Таким образом, очевидно и наблюдаемо, что когда $ item работает по ссылке, изменения, внесенные в $ item, вносятся в элементы исходного $ set. Использование $ item по ссылке также не позволяет PHP создавать копию массива. Чтобы проверить это, сначала мы покажем небольшой скрипт, демонстрирующий копию:
$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
$set[] = ucfirst($item);
}
print_r($set);
Это выводит:
Array
(
[0] => apple
[1] => banana
[2] => coconut
[3] => Apple
[4] => Banana
[5] => Coconut
)
Как показано в примере, PHP скопировал $ set и использовал его для выполнения цикла, но когда $ set использовался внутри цикла, PHP добавил переменные в исходный массив, а не в скопированный массив. По сути, PHP использует только скопированный массив для выполнения цикла и присвоения $ item. Из-за этого вышеупомянутый цикл выполняется только 3 раза, и каждый раз он добавляет другое значение в конец исходного $ set, оставляя исходный $ set с 6 элементами, но никогда не входя в бесконечный цикл.
Однако что, если бы мы использовали $ item по ссылке, как я упоминал ранее? К вышеуказанному тесту добавлен один символ:
$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
$set[] = ucfirst($item);
}
print_r($set);
В результате получается бесконечный цикл. Обратите внимание, что на самом деле это бесконечный цикл, вам придется либо убить сценарий самостоятельно, либо подождать, пока у вашей ОС не закончится память. Я добавил в свой скрипт следующую строку, чтобы PHP очень быстро исчерпал память, я предлагаю вам сделать то же самое, если вы собираетесь запускать эти тесты с бесконечным циклом:
ini_set("memory_limit","1M");
Итак, в этом предыдущем примере с бесконечным циклом мы видим причину, по которой PHP был написан для создания копии массива для выполнения цикла. Когда копия создается и используется только структурой самой конструкции цикла, массив остается статическим на протяжении всего выполнения цикла, поэтому вы никогда не столкнетесь с проблемами.
Цикл PHP foreach можно использовать с Indexed arrays
, Associative arrays
и Object public variables
.
В цикле foreach первое, что делает php, - это то, что он создает копию массива, который нужно повторить. Затем PHP выполняет итерацию по этому новому copy
массива, а не по исходному. Это показано в следующем примере:
<?php
$numbers = [1,2,3,4,5,6,7,8,9]; # initial values for our array
echo '<pre>', print_r($numbers, true), '</pre>', '<hr />';
foreach($numbers as $index => $number){
$numbers[$index] = $number + 1; # this is making changes to the origial array
echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # showing data from the copied array
}
echo '<hr />', '<pre>', print_r($numbers, true), '</pre>'; # shows the original values (also includes the newly added values).
Помимо этого, php также позволяет использовать iterated values as a reference to the original array value
. Это показано ниже:
<?php
$numbers = [1,2,3,4,5,6,7,8,9];
echo '<pre>', print_r($numbers, true), '</pre>';
foreach($numbers as $index => &$number){
++$number; # we are incrementing the original value
echo 'Inside of the array = ', $index, ': ', $number, '<br />'; # this is showing the original value
}
echo '<hr />';
echo '<pre>', print_r($numbers, true), '</pre>'; # we are again showing the original value
Примечание. Это не позволяет использовать original array indexes
как references
.
Источник: http://dwellupper.io/post/47/understanding -php-foreach-loop-with-examples
Object public variables
неверно или, в лучшем случае, вводит в заблуждение. Вы не можете использовать объект в массиве без правильного интерфейса (например, Traversible), и когда вы делаете foreach((array)$obj ...
, вы фактически работаете с простым массивом, а не с объектом.
Похожие вопросы
Связанные вопросы
Новые вопросы
php
PHP — это открытый, мультипарадигмальный, динамически типизированный и интерпретируемый язык сценариев, изначально разработанный для веб-разработки на стороне сервера. Используйте этот тег для вопросов о программировании на языке PHP.
foreach ($array as &$value)
) - PHP должен знать текущую позицию в исходном массиве, даже если он фактически выполняет итерацию по копии.