Pułapka referencji w foreach

Napi­szę o cieka­wym proble­mie, który przy odro­bi­nie pośpie­chu, nieuwagi, mody­fi­ka­cji czyje­goś kodu może dawać solidną dawkę WTF podczas testo­wa­nia (mnie dało). Żeby nie było, w doku­men­ta­cji fore­ach pisze jak byk: Refe­rence of a $value and the last array element remain even after the fore­ach loop. It is recom­men­ded to destroy it by unset().

Powiedzmy, że mamy tablicę jedno­wy­mia­rową, którą chcemy prze­ite­ro­wać w celu zmiany warto­ści. Nic prost­szego powiecie:

$array = ['arr_item1', 'arr_item2', 'arr_item3'];
 
foreach ($array as &$item) { // $item z referencją
  // modyfikuje elementy
}

Na tym etapie wszystko jest OK. Jedziemy sobie z kodem dalej.

// 150 linii niżej, kiedy zapomnieliśmy już o $item
 
foreach ($array as $item) { // $item BEZ referencji
  // robie cokolwiek z $item
}

W tym momen­cie poja­wia się WTF. Zawar­tość $array1 to arr_item1, arr_item2, arr_item2. Jeśli za drugim razem robimy jakąś agre­ga­cję danych w stylu wyli­cza­nie śred­niej czy sumy, błąd może przez długi czas pozo­stać niezau­wa­żony (mediana) lub wynik deli­kat­nie prze­ina­czony (śred­nia aryt­me­tyczna dla dużej liczby elementów).

Pyta­nie dlaczego tak się dzieje? To nie bug, to ficzer!
$item po zakoń­cze­niu pierw­szej itera­cji w dalszym ciągu trzyma refe­ren­cję do $array[2]. Wobec tego w czasie drugiej itera­cji przy­pi­suje kolejne elementy do $array[2]. Wszystko jest OK dopóki nie docie­ramy do ostat­niego elementu. Mamy tam do czynie­nia z instruk­cją w stylu „przy­pisz do $item[2] wartość $item[2]”. Jako iż $item[2] ma aktu­al­nie wartość z $item[1] to buch i mamy arr_item2.

Iteru­jąc po kilku różnych tabli­cach za pomocą tej samej zmien­nej zawsze powo­du­jemy nadpi­sa­nie ostat­niego elementu pierw­szej tablicy.

Jak się przed tym uchronić?

  • użyć unset($item) po fore­ach (najlep­sze wyjście)
  • unikal­nie nazy­wać zmienne $item (ryzy­kowny krok)
  • rozbić funk­cję wyko­rzy­stu­jącą itera­cje na kilka mniej­szych, praw­do­po­dob­nie robi kilka różnych rzeczy

Podobne wpisy:

  1. 10 trików w Smarty
  2. Hello, I’m Tmail?
  3. Wali­duj z Harrym Potterem
  4. PDO poprzez Depen­dency Injec­tion Conta­iner [cz. 2/2]

5 Comments

  • Krzysztof
    13 sierpnia 2013 - 07:59 | Permalink

    Jeden z przy­kła­dów przy­go­to­wu­ją­cych do ZCE :) Co do itera­cji po tablicy — może warto by zmie­niać tablicę przez $arr[$key] = $newVa­lue — bezpiecz­niej­sze rozwiązanie.

  • 13 sierpnia 2013 - 08:39 | Permalink

    Jeśli nie mamy tablic wielo­wy­mia­ro­wych to rzeczy­wi­ście mody­fi­ka­cja poprzez $arr[$key] = $newVa­lue jest najwygodniejsza.

  • V
    13 sierpnia 2013 - 15:48 | Permalink

    Nikt Ci jesz­cze nie powie­dział, że eferen­cje są baaaad? :) Nie rozwią­zują proble­mów, raczej je tworzą :)

  • 9 września 2013 - 10:13 | Permalink

    Refe­ren­cje są dobre, ja najczę­ściej używam je do funk­cji. Nie trzeba wtedy itero­wać $GLOBALS.

  • 29 grudnia 2013 - 08:56 | Permalink

    Kilka razy się nadzia­łem z tym „proble­mem” zapa­mię­ty­wa­nia ostat­niej refe­ren­cji, potem jakieś cuda się działy gdy używa­łem tej samej zmien­nej co w pętli ;)

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.

    Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>