воскресенье, 10 марта 2013 г.

Ссылки внутри цикла foreach

Ниже приведён пример отлично работающего кода:
$list = array(1, 2, 3);
foreach ($list as &$item) {
    $item++;
}
print_r($list);
Запускаем, получаем логичный вывод:

Array
(
    [0] => 2
    [1] => 3
    [2] => 4
)

А теперь лёгким движением руки этот код превращается... превращается... в конструкцию со странным неочевидным функционалом.
$list = array(1, 2, 3);
foreach ($list as &$item) {
    $item++;
}
print_r($list);

foreach ($list as $item) {
    ;
}
print_r($list);
Мы всего лишь организовали второй цикл foreach по одному и тому массиву. И в этот раз print_r выдал совсем иные результаты.

Array
(
    [0] => 2
    [1] => 3
    [2] => 3
)

Что же произошло? Собственно, ничего такого, чтобы мы сами не просили сделать PHP. В первом цикле мы объявили ссылку &$item, которая после завершения работы цикла указывает на элемент массива $list[2]. Далее мы пробегаемся ещё раз по массиву, на каждом шаге присваивая переменной $item очередное значение. Т.к. в PHP область видимости переменных не ограничивается блоком составного оператора, то переменная $item во втором цикле - это та же самая переменная из первого цикла. Поэтому, одновременно с установкой значения переменной $item, это же значение присваивается и элементу $list[2].

Шаг 0: $item = $list[2] = $list[0] = 2
Шаг 1: $item = $list[2] = $list[1] = 3
Шаг 2: $item = $list[2] = $list[2] = 3

Никаких ошибок нет, но получить-то мы хотели несколько другой результат. Есть несколько вариантов, чтобы обезопасить себя от таких неожиданностей. Первый вариант - никогда не забываем принудительно чистить переменные. Вот этот код всегда работает так, как задумано.
$list = array(1, 2, 3);
foreach ($list as &$item) {
    $item++;
}
print_r($list);

unset($item); // !!!

foreach ($list as $item) {
    ;
}
print_r($list);
Второй вариант - для изменения элементов массива не используем ссылки, используем конструкцию $key => $value. С ней также нет никаких проблем.
$list = array(1, 2, 3);
foreach ($list as $key => $item) {
    $list[$key]++;
}
print_r($list);

foreach ($list as $key => $item) {
    ;
}
print_r($list);
Ну и самый правильный вариант, при котором мы не только избегаем описанной проблемы, но и многих других - максимально сокращаем область видимости переменных, выделяя логические части кода в отдельные функции. Этот вариант неплохо бы совмещать с одним из первых двух.
$list = array(1, 2, 3);

$incrementList = function ($list) {
    foreach ($list as $key => $item) {
        $list[$key]++;
    }
    return $list;
};
$list = $incrementList($list);
print_r($list);

$doList = function ($list) {
    foreach ($list as $key => $item) {
        ;
    }
    return $list;
};
$list = $doList($list);
print_r($list);
Здесь $item первого цикла и $item второго цикла - разные переменные.

Пишите на  PHP и прибудет с вами сила!