Питання Як PHP насправді працює?


Дозвольте мені вказувати це, сказавши, що я знаю що foreach є, робить і як його використовувати. Це питання стосується того, як він працює під капотом, і я не хочу будь-яких відповідей у ​​відповідності з правилами "так, як ви співаєте масив з foreach".


Довгий час я припускав це foreach працював з самим масивом. Тоді я знайшов багато посилань на те, що він працює з a скопіювати масиву, і я з того вважаю, що це кінець історії. Але я недавно потрапив до дискусії з цього питання, і після невеликих експериментів виявив, що насправді це не на 100% вірне.

Дозвольте мені показати, що я маю на увазі. Для наступних тестів ми будемо працювати з наступним масивом:

$array = array(1, 2, 3, 4, 5);

Тестовий випадок 1:

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 */

Це ясно показує, що ми не працюємо безпосередньо з вихідним масивом - інакше цикл буде продовжуватися назавжди, оскільки ми постійно натискаємо елементи на масив під час циклу. Але для того, щоб бути впевненим, це так:

Тестовий випадок 2:

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 спирається на масив вказівника вихідного масиву. Але ми просто довели, що ми не працює з вихідним масивом, чи не так? Ну, не цілком.

Тестовий випадок 3:

// 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 покладається на те, що вказівник внутрішнього масиву, змінюючи його всередині циклу, може призвести до несподіваної поведінки.

Ну, давайте дізнаємося, що таке "несподівана поведінка" (технічно будь-яка поведінка несподівана, оскільки я вже не знаю, чого чекати).

Тестовий випадок 4:

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

/* Output: 1 2 3 4 5 */

Тестовий кейс 5:

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

/* Output: 1 2 3 4 5 */

... Нічого, що несподівано там, насправді, здається, підтримує теорію "копії джерела".


Питання

Що тут відбувається? Мій C-FU не є достатнім для того, щоб я міг отримати правильний висновок, просто переглянувши вихідний код PHP, я б вдячний, якщо хтось міг би це перекласти на англійську для мене.

Мені здається, що foreach працює з a скопіювати масиву, але встановлює покажчик масиву вихідного масиву в кінець масиву після циклу.

  • Це правильно і ціла історія?
  • Якщо ні, то що це дійсно робить?
  • Чи є ситуація, коли використовуються функції, що регулюють покажчик масиву (each(), reset() et al.) під час a foreach може вплинути на результат циклу?

1644
2018-04-07 19:33


походження


@DaveRandom Там є a php-internals тег, який, ймовірно, повинен йти з цим, але я залишу це вам, щоб вирішити, який, якщо який-небудь з інших 5 тегів замінити. - Michael Berkowski
схоже на COW, без видалення рукоятки - zb'
Спочатку я подумав »гош, ще одне нове питання. Читайте документи ... хм, чітко невизначена поведінка ". Потім я прочитав повне питання, і треба сказати: мені це подобається. Ви доклали чимало зусиль до цього і написали всі іспити. пс є тест-тест 4 і 5 однакові? - knittl
Просто думка про те, чому це має сенс, що покажчик масиву зачіпається: PHP потрібно скинути та перемістити внутрішній покажчик масиву вихідного масиву разом з копією, оскільки користувач може попросити посилання на поточне значення (foreach ($array as &$value)) - PHP повинен знати поточну позицію в оригінальному масиві, хоча це насправді повторює копію. - Niko
@ Сеан: IMHO, PHP документація дійсно погана при описі нюансів основних функцій мови. Але це, мабуть, тому, що багато мов спеціальних випадків запечені в мові ... - Oliver Charlesworth


Відповіді:


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)
  • Об'єкти мають пропускну семантику, яка для практичних цілей означає, що вони ведуть себе як посилання. Отже, об'єкти завжди можна змінювати під час ітерації.

Проблема з можливістю модифікацій під час ітерації полягає в тому, що елемент, який ви зараз перебуваєте, видалено. Скажімо, ви використовуєте покажчик для відстеження того, який елемент масиву ви зараз знаходитесь. Якщо цей елемент зараз звільнений, у вас залишився показник, що зависає (зазвичай це призводить до сегментації).

Є різні способи вирішення цього питання. PHP 5 та PHP 7 суттєво відрізняються в цьому відношенні, і я описати обидва способи поведінки в наступному. Підсумок полягає в тому, що підхід PHP 5 був досить тупим і призвів до різного роду дивовижних проблем у виняткових ситуаціях, в той час як підхід, який використовується в PHP 7, призводить до більш передбачуваної та послідовної поведінки.

Як останній попередній, слід зазначити, що для керування пам'яттю PHP використовує підрахунок відліку та copy-on-write. Це означає, що якщо ви "копіюєте" значення, ви насправді просто використовуєте стару величину та збільште свій контрольний номер (refcount). Тільки коли ви виконаєте якусь модифікацію, буде виконана справжня копія (називається "дублювання"). Побачити Вас лякають для більш широкого введення на цю тему.

PHP 5

Внутрішній покажчик масиву та HashPointer

Масиви в PHP 5 мають один виділений «Внутрішній покажчик масиву» (IAP), який належним чином підтримує модифікації. Кожного разу, коли елемент видаляється, буде перевіряти, чи точка IAP вказує на цей елемент. Якщо це так, він переміщується до наступного елемента.

Хоча foreach використовує IAP, існує додаткове ускладнення: існує лише один IAP, але один масив може бути частиною кількох циклів для кожного:

// 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) {
        // ...
    }
}

Для підтримки двох одночасних циклів з лише одним внутрішнім покажчиком масиву, для кожного виконує такі schenanigans: перед виконанням тіла циклу, foreach буде резервувати вказівник на поточний елемент та його хеш на кожному кроці HashPointer. Після запуску тіла циклу IAP буде повернуто до цього елемента, якщо він все ще існує. Якщо елемент, однак, був вилучений, ми просто використовуватимемо там, де зараз знаходиться IAP. Ця схема працює переважно-kinda-sortof, але є дуже багато дивної поведінки, яку ви можете вийти з неї, деякі з яких я продемонструю нижче.

Дублювання масиву

IAP є видимою функцією масиву (виставленого через current сімейство функцій), оскільки такі зміни в IAP вважають модифікаціями під семантикою copy-on-write. Це, на жаль, означає, що foreach у багатьох випадках примушене дублювати масив, який повторюється. Точні умови:

  1. Масив не є посиланням (is_ref = 0). Якщо це посилання, то це змінюється передбачалося розмножуватися, тому його не слід дублювати.
  2. Масив має 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).

(*) Збільшення рефронта тут звучить беззахисно, але порушує семантику copy-on-write (COW): це означає, що ми будемо змінювати IAP масиву refcount = 2, а COW вказує на те, що модифікації можуть виконуватись тільки на refcount = 1 значення. Це порушення призводить до змін користувачем змін поведінки (в той час як COW, як правило, є прозорими), оскільки зміна IAP на ітераційному масиві буде спостерігатися, але тільки до першої модифікації не-IAP в масиві. Замість цього три "дійсних" варіанти були б: a) завжди дублювати, b) не збільшувати refcount і, таким чином, дозволяючи довільно змінювати ітераційний масив у циклі, або c) взагалі не використовують 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. Ось чому приклади коду, що показують модифікацію під час ітерації, завжди будуть вимкнені далі елемент, а не поточний.

Приклади: ваші тестові випадки

Три аспекти, описані вище, повинні надати вам переважно повне уявлення про особливості виконання кожної програми, і ми можемо перейти до обговорення деяких прикладів.

Поведінка ваших тестів дуже просто пояснити:

  • У тестових випадках 1 і 2 $array починається з refcount = 1, так що він не буде дублюватися для foreach: тільки refcount збільшується. Коли тіло циклу згодом змінює масив (який має refcount = 2 в той момент), дублювання буде відбуватися в цьому пункті. Foreach продовжуватиме працювати над незміненими копіями $array.

  • У тестовому випадку 3 знову масив не дублюється, тому для будь-якого буде змінювати IAP $array змінна В кінці ітерації IAP має значення NULL (що означає, що ітерація виконана), яка each вказує на повернення false.

  • У тестових випадках 4 та 5 обидва each і reset є допоміжними функціями. The $array має refcount=2 коли воно передається їм, тому його потрібно дублювати. Як такий foreach буде знову працювати над окремим масивом.

Приклади: ефекти current в передній

Хорошим способом показати різні типи поведінки дублювання є спостереження за поведінкою current() функція всередині foreach петлі. Розглянемо цей приклад:

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

Тут ти повинен це знати current() є функцією by-ref (фактично: prefer-ref), навіть якщо це не змінює масив. Це має бути для того, щоб грати добре з усіма іншими функціями, як next які всі побічні. Перехід за пропуском передбачає, що масив повинен бути розділений і таким чином $array і foreach-масив буде іншим. Причина, яку ви отримуєте 2 замість 1 також згадувалося вище: foreach випереджає покажчик масиву раніше запустити код користувача, а не після. Таким чином, навіть якщо код знаходиться на першому елементі, для будь-якого вже висунуто покажчик на другий.

Тепер давайте спробуємо зробити невелику зміну:

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

Тут ми маємо case is_ref = 1, тому масив не копіюється (як і вище). Але тепер, коли це посилання, масив більше не потрібно дублювати при переході до побічного реф current() функція Таким чином current() і для будь-якої роботи на тому ж масиві. Ви все ще бачите поведінку "не одне", хоча через це foreachрозвиває покажчик.

Ви отримуєте таку саму поведінку при повторній ітерації:

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

Тут важлива частина полягає в тому, що кожна з них зробить $array is_ref = 1, коли він повторюється за посиланням, тому в основному ви маєте таку ж ситуацію, що і вище.

Інший невеликий варіант, на цей раз ми призначимо масив іншою змінною:

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

Ось рефронт $array це 2, коли цикл запускається, так що раз ми дійсно повинні робити дублікацію на початку. Таким чином $array і масив, який використовується для кожного, буде повністю відокремлений з самого початку. Ось чому ви отримуєте позицію IAP там, де це було до циклу (у цьому випадку це було на першій позиції).

Приклади: модифікація під час ітерації

Спроба облік модифікацій під час ітерації - це місце, де виникла наша перша проблема, тому вона розглядає деякі приклади цього випадку.

Розглянемо ці вкладені цикли в тому ж масиві (де ітерація by-ref використовується для того, щоб переконатися, що вона дійсно однакова):

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() і т. д. зазвичай не впливають назавжди. Наприклад, наступний код виконується так, ніби reset() взагалі не було:

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

Причиною є те, що поки reset() тимчасово змінює IAP, воно буде відновлено до поточного для кожного елемента після тіла циклу. Щоб змусити 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', і розподільнику відбувається повторне використання того ж місця пам'яті для зберігання елемента. Таким чином, назавжди закінчується прямий стрибок на знову вставлений елемент, тим самим скорочуючи цикл.

Підставляти повторений об'єкт під час циклу

Останній непарний випадок, який я хотів би відзначити, полягає в тому, що 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

Ітератори Hashtable

Якщо ви ще пам'ятаєте, основна проблема з ітерацією масиву полягає в тому, як обробляти видалення елементів середньої ітерації. PHP 5 використовував для цієї мети єдиний внутрішній покажчик масиву (IAP), який був дещо недооптимальним, оскільки один масив вказував повинен був бути розтягнутий, щоб підтримувати кілька одночасних циклів foreach і взаємодія з reset() і т.д. на вершині цього.

PHP 7 використовує інший підхід, а саме: підтримує створення довільної кількості зовнішніх, безпечних хештейбельних ітераторів. Ці ітератори повинні бути зареєстровані в масиві, звідки вони мають ту саму семантику, що й IAP: якщо елемент масиву буде видалено, всі ішачі хешування, що вказують на цей елемент, будуть перенаправлені до наступного елемента.

Це означає, що foreach більше не буде використовувати IAP зовсім. Попередній цикл абсолютно не впливає на результати current() і т. ін., і власна поведінка ніколи не буде впливом таких функцій, як reset() тощо.

Дублювання масиву

Інша важлива зміна між PHP 5 і PHP 7 стосується дублювання масиву. Тепер, коли IAP більше не використовується, ітерація масиву побічного значення буде робити лише приріст refcount (замість дублювання масиву) у всіх випадках. Якщо масив модифікується протягом першого циклу, то в цьому місці відбудеться дублювання (за версією copy-on-write), і 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 зберігають один і той самий вихід: ітерація масиву за значенням завжди продовжує працювати над початковими елементами. (У цьому випадку навіть поведінка перерахунку та дублювання однаково відрізняється між PHP 5 та PHP 7).

  • Тестовий регістр 3 змінюється: Foreach більше не використовує IAP, тому each() це не впливає на цикл. Він буде мати такий самий вихід до і після.

  • Тестові випадки 4 та 5 залишаються однаковими: each() і reset() буде дублювати масив перед тим, як змінити IAP, тоді як foreach все ще використовує оригінальний масив. (Не так, щоб зміна IAP мала значення, навіть якщо масив був поділений.)

Другий набір прикладів був пов'язаний з поведінкою current() під різними конфігураціями посилання / перерахунку. Це вже не має сенсу, як 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 вискочив прямо на новий елемент, тому що він виглядав як такий самий, як елемент видалення (через зіткнення хешу та покажчика). Оскільки ми ніколи більше не покладаємось на хеш-елемент, це вже не проблема.


1384
2018-02-13 13:21



@Баба це робить. Передача його до функції таке ж, як робити $foo = $array перед петлею;) - NikiC
Для тих з вас, хто не знає, що таке звал, будь ласка, зверніться до Сари Голмана blog.golemon.com/2007/01/youre-being-lied-to.html - shu zOMG chen
@unbeli Я використовую термінологію, яка використовується всередині PHP. The Buckets - це частина подвійного зв'язаного списку для зіткнень хешів, а також частина подвійного зв'язаного списку для замовлення;) - NikiC
@NikiC: чи дійсно потрібно іти в університеті? Ви всього 19 років, але ви виглядаєте набагато більш досвідченими - dynamic
Великий аншер. Я думаю, ти мав на увазі iterate($outerArr); і ні iterate($arr); десь - niahoo


У прикладі 3 ви не змінюєте масив. У всіх інших прикладах ви змінюєте вміст або вказівник внутрішнього масиву. Це важливо, коли справа доходить PHP масиви через семантику оператора присвоювання.

Оператор присвоювання для масивів у PHP працює більше як ледачий клон. Призначення однієї змінної іншим, що містить масив, буде клонувати масив, на відміну від більшості мов. Однак фактичне клонування не буде виконано, якщо це не потрібно. Це означає, що клон буде мати місце лише тоді, коли будь-яка змінна модифікується (copy-on-write).

Ось приклад:

$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, масив змінюється під час циклу, тоді як ця додаткова довідка є живим. Це викликає клон, і це пояснює, що відбувається тут!

Ось чудова стаття для іншого побічного ефекту цієї поведінки copy-on-write: Термінал оператора PHP: швидко чи ні?


97
2018-04-07 20:43



здається вашим правом, я зробив якийсь приклад, який демонструє: codepad.org/OCjtvu8r одна відмінність від вашого прикладу - вона не копіюється, якщо змінити значення, лише якщо змінити ключі. - zb'
Це дійсно пояснює все вищезазначене поведінку поведінки, і це може бути приємно проілюстроване закликом each() в кінці першого тестового випадку, де ми бачимо що покажчик масиву вихідного масиву вказує на другий елемент, оскільки масив був змінений під час першої ітерації. Це також, здається, демонструє це foreach переміщує вказівник масиву перед виконанням коду блоку циклу, який я не очікував - я б подумав, це зробить це в кінці. Велике спасибі, це добре очищає мене. - DaveRandom


Деякі вказівки слід пам'ятати при роботі з foreach():

а) foreach працює на проспективна копія від початкового масиву.     Це означає, що foreach () буде мати SHARED зберігання даних до або, якщо не prospected copy є     не створено foreach Notes / Коментарі користувача.

б) Що викликає a проспективна копія?     Прогнозована копія створена на основі політики copy-on-write, тобто коли завгодно     змінений масив для foreach (), створюється клон вихідного масиву.

в) Оригінальний масив і foreach () ітератор матимуть DISTINCT SENTINEL VARIABLES, тобто один для оригінального масиву та інший для будь-якого; перегляньте тестовий код нижче. SPL , Ітератори, і Масив Ітератор.

Стек переповнення питання Як переконатися, що значення скидається в циклі "foreach" у PHP? розглядає справи (3,4,5) Вашого запитання.

Наступний приклад показує, що кожен () і скидання () НЕ впливають 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

34
2018-04-07 21:03



Ваша відповідь не зовсім правильна. foreach працює на потенційній копії масиву, але це не робить фактичну копію, якщо це не потрібно. - linepogl
Ви хотіли б продемонструвати, як і коли ця потенційна копія створена за допомогою коду? Мій код демонструє це foreach копіює масив 100% часу. Я хочу знати. Дякую за коментарі - sakhunzai
Копіювання масиву коштує багато. Спробуйте підрахувати, скільки часу потрібно для повторення масиву з 100000 елементами, використовуючи їх for або foreach. Ви не побачите істотної різниці між двома з них, тому що фактична копія не відбувається. - linepogl
Тоді я б припустила, що є SHARED data storage зарезервовані до або без copy-on-write , але (з мого фрагмента коду) очевидно, що завжди буде два набору SENTINEL variables один для original array та інші для foreach. Спасибі, що має сенс - sakhunzai
"розвіданий"? Ви маєте на увазі "захищений"? - Peter Mortensen


ПРИМІТКА ДЛЯ PHP 7

Для поновлення на цю відповідь, як він придбав деяку популярність: Відповідь на це питання більше не застосовується в якості РНР 7. Як пояснив в "Зворотні несумісні зміни», В PHP 7 Еогеасп працює на копію масиву, так що будь-які зміни на самому масиві не відбивається на циклі Еогеасп. Детальніше за посиланням.

Пояснення (цитата з php.net):

Перша форма циклів над масивом, заданою масив_вираз. На кожен   ітерація, значення поточного елемента присвоюється значенням $ і   Внутрішній покажчик масиву просунутий одним (так на наступному   ітерація, ви подивитеся на наступний елемент).

Отже, у вашому першому прикладі у вас є лише один елемент масиву, а при переміщенні покажчика наступного елемента не існує, тому після того, як ви додаєте новий елемент для кожного кінця, оскільки він вже "вирішив", що він це як останній елемент.

У вашому другому прикладі, ви починаєте з двома елементами, і цикл Еогеасп нема на останній елемент так оцінює масив на наступній ітерації і, таким чином, розуміє, що існує новий елемент в масиві.

Я вірю, що це все є наслідком На кожній ітерації частина пояснення в документації, що, ймовірно, означає це 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 насправді працює? що пояснює внутрішні сторони за такою поведінкою.


22
2018-04-15 08:46



Чи ти прочитав решту відповіді? Зрозуміло, що для кожного вирішує, чи буде він співати інший раз раніше він навіть запускає в ньому код. - Damir Kasipovic
Ні, масив модифікується, але "занадто пізно", оскільки для будь-якого вже "думає", що він знаходиться на останньому елементі (який він є на початку ітерації) і більше не буде петлі. Де в другому прикладі, він не на останньому елементі на початку ітерації і знову оцінюється на початку наступної ітерації. Я намагаюся підготувати тестовий випадок. - Damir Kasipovic
@AlmaDo Подивіться на lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Він завжди встановлюється на наступний покажчик, коли він повторюється. Отже, коли він досягне останньої ітерації, він буде позначено як закінчений (через покажчик NULL). Коли ви потім додасте ключ у останній ітерації, для цього не помітите це. - bwoebi
@DKasipovic No Немає ніякого завершити і очистити пояснення там (принаймні на даний момент - можливо, я помиляюсь) - Alma Do
Насправді, здається, що @AlmaDo має недолік у розумінні власної логіки ... Ваша відповідь в порядку. - bwoebi


Відповідно до документації, наданої керівництвом PHP.

На кожній ітерації значення поточного елемента присвоюється $ v і внутрішньому
  Масивний покажчик висунуто одним (так що на наступній ітерації ви будете дивитись на наступний елемент).

Отже, за вашим першим прикладом:

$array = ['foo'=>1];
foreach($array as $k=>&$v)
{
   $array['bar']=2;
   echo($v);
}

$array Є лише один елемент, так як за кожним виконанням, 1 призначити $vі у нього немає іншого елемента для переміщення покажчика

Але у вашому другому прикладі:

$array = ['foo'=>1, 'bar'=>2];
foreach($array as $k=>&$v)
{
   $array['baz']=3;
   echo($v);
}

$array маємо два елементи, тому тепер $ масив оцінює нульові індекси і переміщує покажчик на один. Для першої ітерації циклу додано $array['baz']=3; як пройти за посиланням.


8
2018-04-15 09:32





Чудовий питання, тому що багато розробників, навіть досвідчених, плутаються з тим, як PHP обробляє масиви в попередніх циклах. У стандартній 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, виконуються членам вихідного набору $. Використання $ 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, залишаючи оригінал $ встановленим з 6 елементами, але ніколи не вступаючи в нескінченний цикл.

Однак, якби ми використовували $ item за посиланням, як я вже говорив раніше? Один символ додається до вищевказаного тесту:

$set = array("apple", "banana", "coconut");
foreach ( $set AS &$item ) {
    $set[] = ucfirst($item);
}
print_r($set);

Результати в нескінченному циклі. Зверніть увагу, що насправді це нескінченна ланцюг, вам доведеться або самостійно вбити сценарій, або зачекати, поки ваша операційна система буде вичерпана з пам'яті. Я додав в моєму скрипті наступний рядок, тому PHP дуже швидко закінчився пам'яттю, і я пропоную вам зробити те ж саме, якщо ви збираєтеся виконувати ці нескінченні циклові тести:

ini_set("memory_limit","1M");

Отже, у цьому попередньому прикладі з нескінченним циклом ми бачимо причину написання PHP для створення копії масиву для циклічності. Коли копія створюється і використовується тільки структурою самого контуру циклу, масив залишається статичним протягом всього виконання циклу, так що ви ніколи не будете стикатися з проблемами.


5
2018-04-21 08:44





PHP foreach цикл може бути використаний з Indexed arrays, Associative arrays і Object public variables.

У першому циклі перше, що робить 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


4
2017-11-13 14:08



Object public variables це неправильно або, на краще, ввести в оману. Ви не можете використовувати об'єкт у масиві без правильного інтерфейсу (наприклад, Traversible) і коли ви робите це foreach((array)$obj ... Ви насправді працюєте з простою матрицею, а не об'єктом більше. - Christian