foreach
รองรับการวนซ้ำในสามค่าที่แตกต่างกัน:
ในต่อไปนี้ฉันจะพยายามอธิบายอย่างแม่นยำว่าการวนซ้ำทำงานในกรณีที่แตกต่างกันอย่างไร กรณีที่ง่ายที่สุดคือTraversable
อ็อบเจกต์, สำหรับสิ่งเหล่านี้ส่วนใหญ่foreach
คือ syntax น้ำตาลสำหรับโค้ดตามบรรทัดเหล่านี้:
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 มีหนึ่ง "internal array pointer" (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
ทำ shenanigans ต่อไปนี้: ก่อนที่จะมีการประมวลผลลูปร่างกาย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
นี่ฟังดูไร้เดียงสา แต่เป็นการละเมิดซีแมนทิกส์ 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
อยู่แล้วที่องค์ประกอบ นี่คือเหตุผลที่ตัวอย่างโค้ดแสดงการปรับเปลี่ยนในระหว่างการวนซ้ำจะunset
เป็นองค์ประกอบต่อไปเสมอแทนที่จะเป็นองค์ประกอบปัจจุบัน
ตัวอย่าง: กรณีทดสอบของคุณ
ทั้งสามด้านที่อธิบายไว้ข้างต้นควรให้ความประทับใจที่สมบูรณ์ที่สุดแก่คุณเกี่ยวกับความเหมือนของการforeach
ใช้งานและเราสามารถดำเนินการต่อไปเพื่อหารือเกี่ยวกับตัวอย่าง
พฤติกรรมของกรณีทดสอบของคุณอธิบายได้ง่าย ณ จุดนี้:
ในกรณีทดสอบ 1 และ 2 $array
เริ่มด้วย refcount = 1 ดังนั้นมันจะไม่ถูกทำซ้ำโดยforeach
: เฉพาะค่าที่refcount
เพิ่มขึ้น เมื่อร่างกายวนรอบในภายหลังปรับเปลี่ยนอาร์เรย์ (ซึ่งมี refcount = 2 ณ จุดนั้น) การทำซ้ำจะเกิดขึ้นที่จุดนั้น foreach $array
จะยังคงทำงานในสำเนาไม่มีการแก้ไขของ
ในกรณีทดสอบ 3 อาร์เรย์จะไม่ซ้ำกันอีกครั้งดังนั้นforeach
จะทำการแก้ไข IAP ของ$array
ตัวแปร ในตอนท้ายของการทำซ้ำที่ IAP เป็นโมฆะ (หมายถึงการทำซ้ำได้ทำ) ซึ่งแสดงให้เห็นโดยการกลับ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()
เป็นฟังก์ชั่นการอ้างอิง (จริง ๆ แล้ว: prefer-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 case ดังนั้นอาร์เรย์จะไม่ถูกคัดลอก (เหมือนด้านบน) แต่ตอนนี้มันเป็นข้อมูลอ้างอิงอาร์เรย์ไม่จำเป็นต้องทำซ้ำเมื่อส่งผ่านไปยังcurrent()
ฟังก์ชันการอ้างอิง ดังนั้นcurrent()
และforeach
ทำงานในอาเรย์เดียวกัน คุณยังคงเห็นพฤติกรรมแบบ off-by-one เนื่องจากวิธี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 */
นี่ refcount ของ$array
คือ 2 เมื่อวงเริ่มต้นดังนั้นเมื่อเราต้องทำซ้ำล่วงหน้าจริง ๆ ดังนั้น$array
และอาร์เรย์ที่ใช้โดย foreach จะถูกแยกออกจากจุดเริ่มต้นอย่างสมบูรณ์ นั่นเป็นเหตุผลที่คุณได้รับตำแหน่งของ IAP ไม่ว่าจะอยู่ที่ไหนก่อนที่จะวนรอบ (ในกรณีนี้มันอยู่ที่ตำแหน่งแรก)
ตัวอย่าง: การแก้ไขระหว่างการทำซ้ำ
การพยายามพิจารณาการแก้ไขในระหว่างการทำซ้ำเป็นสิ่งที่ปัญหาต่าง ๆ ที่เกิดขึ้นก่อนหน้าของเราดังนั้นจึงมีหน้าที่พิจารณาตัวอย่างบางส่วนสำหรับกรณีนี้
พิจารณาลูปซ้อนกันเหล่านี้ในอาร์เรย์เดียวกัน (โดยใช้การทำซ้ำโดยอ้างอิงเพื่อให้แน่ใจว่าเป็นลูปเดียวกันจริงๆ):
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
สำรอง + กลไกการเรียกคืนคือการเปลี่ยนแปลงที่ไปเมาหัวราน้ำผ่าน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'
และตัวจัดสรรเกิดขึ้นเพื่อใช้ตำแหน่งหน่วยความจำเดียวกันเพื่อเก็บองค์ประกอบ ดังนั้นก่อนจะกระโดดลงไปที่องค์ประกอบที่สอดเข้าไปใหม่โดยตรงดังนั้นจึงเป็นการตัดวงสั้น ๆ
การแทนที่เอนทิตีที่วนซ้ำระหว่างลูป
กรณีแปลก ๆ สุดท้ายที่ฉันอยากพูดถึงคือ 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 ใช้วิธีการที่แตกต่างกันคือสนับสนุนการสร้างตัวทำซ้ำ hashtable ที่ปลอดภัยภายนอก ตัววนซ้ำเหล่านี้จะต้องลงทะเบียนในอาร์เรย์จากจุดที่พวกเขามีความหมายเช่นเดียวกับ IAP: ถ้าองค์ประกอบอาร์เรย์ถูกลบออกตัววนซ้ำ hashtable ทั้งหมดที่ชี้ไปที่องค์ประกอบนั้นจะก้าวหน้าไปยังองค์ประกอบถัดไป
ซึ่งหมายความว่า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)
อย่างที่คุณเห็นลูปด้านนอกจะไม่ยกเลิกอีกต่อไปหลังจากการวนซ้ำครั้งแรก เหตุผลคือตอนนี้ลูปทั้งสองมีตัววนซ้ำ hashtable แยกกันอย่างสิ้นเชิงและไม่มีการปนเปื้อนข้ามของลูปทั้งสองผ่าน 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 ข้ามไปที่องค์ประกอบใหม่เพราะมัน "ดูเหมือน" เหมือนกับองค์ประกอบที่ถูกลบ (เนื่องจากการแฮชและตัวชี้ชนกัน) เนื่องจากเราไม่ต้องพึ่งพาองค์ประกอบแฮชอีกต่อไปแล้วสิ่งนี้จึงไม่มีปัญหาอีกต่อไป