PHP Foreach ผ่านอ้างอิง: องค์ประกอบสุดท้ายที่ซ้ำกัน? (Bug?)


159

ฉันเพิ่งมีพฤติกรรมแปลก ๆ บางอย่างกับสคริปต์ PHP ง่าย ๆ ที่ฉันเขียน ฉันลดให้เหลือน้อยที่สุดเพื่อสร้างข้อผิดพลาด:

<?php

$arr = array("foo",
             "bar",
             "baz");

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

?>

ผลลัพธ์นี้:

Array
(
    [0] => foo
    [1] => bar
    [2] => baz
)
Array
(
    [0] => foo
    [1] => bar
    [2] => bar
)

นี่เป็นข้อบกพร่องหรือพฤติกรรมแปลก ๆ ที่ควรจะเกิดขึ้นหรือไม่?


ทำตามค่าอีกครั้งดูว่าเปลี่ยนเป็นครั้งที่ 3 หรือไม่
Shackrock

1
@Shackrock ดูเหมือนว่าจะไม่เปลี่ยนแปลงอีกต่อไปด้วยการวนซ้ำซ้ำตามค่า
regality

1
น่าสนใจถ้าคุณเปลี่ยนวนรอบที่สองเพื่อใช้อย่างอื่นที่ไม่ใช่ $ item แสดงว่าทำงานได้ตามที่คาดไว้
Steve Claridge

9
ยกเลิกการตั้งค่ารายการที่ส่วนท้ายของลูปเสมอ: foreach($x AS &$y){ ... unset($y); }- จริง ๆ แล้วบน php.net (ไม่รู้ว่าอยู่ที่ไหน) เพราะมันทำผิดพลาดมาก
Rudie

2
เป็นไปได้ซ้ำของPHP Pass โดยการอ้างอิงใน foreach
Felix Kling

คำตอบ:


170

หลังจากที่วง foreach แรกยังคงมีการอ้างอิงถึงค่าบางอย่างที่ยังถูกใช้โดย$item $arr[2]ดังนั้นแต่ละการเรียกแต่ละครั้งในลูปที่สองซึ่งไม่ได้เรียกโดยการอ้างอิงจะแทนที่ค่านั้นและ$arr[2]ด้วยค่าใหม่

ดังนั้นวน 1 ค่าและ$arr[2]กลายเป็น$arr[0]ซึ่งคือ 'foo'
วนรอบ 2 ค่าและ$arr[2]กลายเป็น$arr[1]ซึ่งคือ 'แถบ'
วน 3 ค่าและ$arr[2]กลายเป็น$arr[2]ซึ่ง 'แถบ' (เนื่องจากวนรอบ 2)

ค่า 'baz' จะหายไปเมื่อมีการโทรครั้งแรกของลูป foreach ที่สอง

การดีบักเอาต์พุต

สำหรับแต่ละการวนซ้ำของลูปเราจะแสดงค่า$itemรวมถึงพิมพ์อาร์เรย์$arrซ้ำ

เมื่อวงแรกวิ่งผ่านเราจะเห็นผลลัพธ์นี้:

foo
Array ( [0] => foo [1] => bar [2] => baz )

bar
Array ( [0] => foo [1] => bar [2] => baz )

baz
Array ( [0] => foo [1] => bar [2] => baz )

ในตอนท้ายของวงที่ยังคงชี้ไปที่สถานที่เดียวกันเป็น$item$arr[2]

เมื่อวงที่สองวิ่งผ่านเราจะเห็นผลลัพธ์นี้:

foo
Array ( [0] => foo [1] => bar [2] => foo )

bar
Array ( [0] => foo [1] => bar [2] => bar )

bar
Array ( [0] => foo [1] => bar [2] => bar )

คุณจะสังเกตเห็นว่าแต่ละครั้งที่อาร์เรย์ใส่ค่าใหม่เข้าไป$itemยังมีการอัปเดต$arr[3]ด้วยค่าเดียวกันเนื่องจากทั้งคู่ยังคงชี้ไปที่ตำแหน่งเดียวกัน เมื่อลูปมาถึงค่าที่สามของอาร์เรย์มันจะมีค่าbarเพราะมันเพิ่งถูกกำหนดโดยการวนซ้ำก่อนหน้าของลูปนั้น

มันเป็นข้อบกพร่องหรือไม่?

ไม่นี่คือลักษณะการทำงานของรายการอ้างอิงไม่ใช่ข้อบกพร่อง มันจะคล้ายกับการทำงานบางอย่างเช่น:

for ($i = 0; $i < count($arr); $i++) { $item = $arr[$i]; }

การวนรอบ foreach ไม่ได้พิเศษในลักษณะที่สามารถละเว้นรายการอ้างอิง มันแค่ตั้งค่าตัวแปรนั้นให้เป็นค่าใหม่ทุกครั้งที่คุณออกไปนอกวง


4
ฉันมีการแก้ไขความคล่องแคล่วเล็กน้อย $itemไม่ได้มีการอ้างอิงถึง$arr[2]ค่าที่มีอยู่โดยมีการอ้างอิงถึงมูลค่าที่อ้างถึงโดย$arr[2] $itemเพื่อแสดงให้เห็นถึงความแตกต่างคุณสามารถยกเลิกการตั้งค่า$arr[2]และ$itemจะไม่ได้รับผลกระทบและการเขียนที่$itemจะไม่ส่งผลกระทบต่อมัน
Paul Biggar

2
พฤติกรรมนี้มีความซับซ้อนที่จะเข้าใจและอาจนำไปสู่ปัญหา ฉันเก็บสิ่งนี้เป็นหนึ่งในรายการโปรดของฉันเพื่อแสดงให้นักเรียนเห็นว่าทำไมพวกเขาควรหลีกเลี่ยงสิ่งต่าง ๆ "โดยการอ้างอิง"
Olivier Pons

1
ทำไม$itemไม่ออกนอกขอบเขตเมื่อออกจากลูป foreach? ดูเหมือนว่าปัญหาการปิดหรือไม่
jocull

6
@jocull: ใน PHP, foreach, for, while, etc ไม่ได้สร้างขอบเขตของตัวเอง
animuson

1
@jocull, PHP ไม่มีตัวแปรโลคอล (บล็อก) หนึ่งในเหตุผลที่ทำให้ฉันรำคาญ
Qtax

29

$itemเป็นการอ้างอิงถึง$arr[2]และถูกเขียนทับโดยลูป foreach ที่สองตามที่ animuson ชี้ให้เห็น

foreach ($arr as &$item) { /* do nothing by reference */ }
print_r($arr);

unset($item); // This will fix the issue.

foreach ($arr as $item) { /* do nothing by value */ }
print_r($arr); // $arr has changed....why?

3

ในขณะนี้อาจไม่เป็นข้อบกพร่องอย่างเป็นทางการในความคิดของฉันมันเป็น ฉันคิดว่าปัญหาที่นี่คือเรามีความคาดหวังที่$itemจะออกนอกขอบเขตเมื่อออกจากลูปเหมือนในภาษาอื่น ๆ อย่างไรก็ตามดูเหมือนจะไม่เป็นเช่นนั้น ...

รหัสนี้ ...

$arr = array('one', 'two', 'three');
foreach($arr as $item){
    echo "$item\n";
}    
echo $item;

ให้ผลลัพธ์ ...

one
two
three
three

อย่างที่คนอื่นพูดแล้วคุณจะเขียนทับตัวแปรอ้างอิงใน$arr[2]วงที่สอง แต่เกิดขึ้นเพราะ$itemไม่เคยออกนอกขอบเขต พวกคุณคิดว่าอะไร ... บั๊ก?


4
1) ไม่ใช่ข้อผิดพลาด มันถูกเรียกใช้แล้วในคู่มือและถูกไล่ออกในรายงานข้อผิดพลาดจำนวนหนึ่งตามที่ตั้งใจไว้ 2) ไม่ได้จริงๆตอบคำถาม ...
BoltClock

มันจับฉันไม่ได้เพราะปัญหาขอบเขตฉันคาดว่า $ item จะยังคงอยู่หลังจาก foreach เริ่มต้น แต่ฉันไม่ได้ตระหนักว่า foreach ปรับปรุงตัวแปรแทนการแทนที่ เช่นเดียวกับการรัน unset ($ item) ก่อนลูปที่สอง โปรดทราบว่าการล้างไม่ได้ล้างค่า (และองค์ประกอบสุดท้ายในอาร์เรย์) เพียงแค่ลบตัวแปร
Programster

น่าเสียดายที่ PHP ไม่ได้สร้างขอบเขตใหม่สำหรับลูปหรือ{}บล็อกโดยทั่วไป นี่คือวิธีการทำงานของภาษา
Fabian Schmengler

0

คำอธิบายที่ง่ายขึ้นดูเหมือนว่ามาจาก Rasmus Lerdorf ผู้สร้างดั้งเดิมของ PHP: https://bugs.php.net/bug.php?id=71454


คำอธิบายที่คล้ายกันนี้ได้รับมาก่อนหน้าในเว็บไซต์นี้: stackoverflow.com/a/19829715
qdinar

คำอธิบายที่คล้ายกันนี้ได้รับมาก่อนหน้านี้ในbugs.php.net/bug.php?id=29992โดย rasmus@php.net
qdinar

0

พฤติกรรมที่ถูกต้องของ PHP น่าจะเป็นข้อผิดพลาดข้อสังเกตในฝ่ายตรงข้ามของฉัน หากตัวแปรอ้างอิงที่สร้างขึ้นในการวนรอบ foreach จะใช้นอกวงมันควรทำให้เกิดการแจ้งเตือน ตกหล่นกับพฤติกรรมนี้ได้ง่ายมากยากที่จะสังเกตเห็นเมื่อมันเกิดขึ้น และไม่มีนักพัฒนาคนใดอ่านหน้าเอกสารหน้า foreach มันไม่ใช่ความช่วยเหลือ

คุณควรunset()อ้างอิงหลังจากวนรอบของคุณเพื่อหลีกเลี่ยงปัญหาประเภทนี้ unset () ในการอ้างอิงจะลบการอ้างอิงโดยไม่ทำอันตรายข้อมูลต้นฉบับ


0

นั่นเป็นเพราะคุณใช้โดยคำสั่งอ้างอิง (&) ค่าสุดท้ายจะถูกแทนที่ด้วยลูปที่สองและทำให้อาร์เรย์ของคุณเสียหาย ทางออกที่ง่ายที่สุดคือการใช้ชื่อที่แตกต่างกันสำหรับวงที่สอง:

foreach ($arr as &$item) { ... }

foreach ($arr as $anotherItem) { ... }
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.