PHP 'foreach' ใช้งานได้จริงอย่างไร


2018

ขอให้ฉันนำหน้าสิ่งนี้โดยบอกว่าฉันรู้ว่าอะไรforeachคืออะไรและใช้อย่างไร คำถามนี้เกี่ยวกับวิธีการทำงานภายใต้หมวกผู้หญิงรูปฝาชีและฉันไม่ต้องการคำตอบตามเส้นของ "นี่คือวิธีที่คุณวนลูปอาร์เรย์ด้วยforeach"


เป็นเวลานานฉันคิดว่ามันใช้ได้foreachกับอาเรย์เอง จากนั้นฉันได้พบการอ้างอิงจำนวนมากเกี่ยวกับความจริงที่ว่ามันใช้ได้กับสำเนาของอาเรย์และตั้งแต่นั้นฉันก็สันนิษฐานว่านี่เป็นจุดจบของเรื่อง แต่เมื่อไม่นานมานี้ฉันได้มีการหารือเกี่ยวกับเรื่องนี้และหลังจากการทดลองเล็กน้อยพบว่าสิ่งนี้ไม่ได้เกิดขึ้นจริง 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 เริ่มดำเนินการตัวชี้อาร์เรย์ภายในจะถูกรีเซ็ตเป็นองค์ประกอบแรกของอาร์เรย์โดยอัตโนมัติ

ใช่ ... นี่ดูเหมือนจะแนะนำว่า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ทำงานกับสำเนาของอาร์เรย์ แต่ตั้งค่าตัวชี้อาร์เรย์ของอาร์เรย์ต้นทางไปยังจุดสิ้นสุดของอาร์เรย์หลังจากวนรอบ

  • เรื่องนี้ถูกต้องและเรื่องราวทั้งหมดหรือไม่
  • ถ้าไม่มันกำลังทำอะไรอยู่?
  • มีสถานการณ์ใดบ้างที่ใช้ฟังก์ชั่นที่ปรับตัวชี้อาร์เรย์ ( each(), reset()และอื่น ๆ ) ในระหว่าง a foreachอาจส่งผลต่อผลลัพธ์ของลูป?

5
@DaveRandom มีแท็กphp-internals ที่น่าจะเป็นไปได้ แต่ฉันจะปล่อยให้คุณตัดสินใจเลือกว่าจะมีแท็ก 5 แท็กใดที่จะแทนที่
Michael Berkowski

5
ดูเหมือนว่า COW โดยไม่มีตัวจัดการลบ
zb '

149
ตอนแรกฉันคิดว่า»เอ้ยคำถามใหม่อีกอย่าง อ่านเอกสาร… hm, พฤติกรรมที่ไม่ได้กำหนดอย่างชัดเจน« จากนั้นฉันอ่านคำถามทั้งหมดและฉันต้องพูดว่า: ฉันชอบ คุณใช้ความพยายามและเขียนบททดสอบทั้งหมดแล้ว PS testcase 4 และ 5 เหมือนกันหรือไม่
knittl

21
แค่คิดว่าทำไมมันถึงทำให้รู้สึกว่าตัวชี้อาร์เรย์ได้รับการสัมผัส: PHP ต้องรีเซ็ตและย้ายตัวชี้อาร์เรย์ภายในของอาร์เรย์เดิมพร้อมกับสำเนาเพราะผู้ใช้อาจขออ้างอิงถึงค่าปัจจุบัน ( foreach ($array as &$value)) - PHP ต้องการทราบตำแหน่งปัจจุบันในอาเรย์ดั้งเดิมแม้ว่ามันจะวนซ้ำจริง ๆ
Niko

4
@Sean: IMHO เอกสาร PHP ค่อนข้างแย่มากที่อธิบายความแตกต่างของคุณสมบัติภาษาหลัก แต่ที่อาจจะเป็นเพราะกรณีเฉพาะกิจพิเศษจำนวนมากอบเป็นภาษา ...
โอลิเวอร์ Charlesworth

คำตอบ:


1660

foreach รองรับการวนซ้ำในสามค่าที่แตกต่างกัน:

  • อาร์เรย์
  • วัตถุปกติ
  • Traversable วัตถุ

ในต่อไปนี้ฉันจะพยายามอธิบายอย่างแม่นยำว่าการวนซ้ำทำงานในกรณีที่แตกต่างกันอย่างไร กรณีที่ง่ายที่สุดคือ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ในหลายกรณีถูกบังคับให้ทำซ้ำอาร์เรย์ที่ทำซ้ำ เงื่อนไขที่แม่นยำคือ:

  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 จริงๆในตอนแรก)

(*) การเพิ่มที่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 เป็นโมฆะ (หมายถึงการทำซ้ำได้ทำ) ซึ่งแสดงให้เห็นโดยการกลับeachfalse

  • ในกรณีทดสอบ 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 จะสร้าง$arrayis_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 ข้ามไปที่องค์ประกอบใหม่เพราะมัน "ดูเหมือน" เหมือนกับองค์ประกอบที่ถูกลบ (เนื่องจากการแฮชและตัวชี้ชนกัน) เนื่องจากเราไม่ต้องพึ่งพาองค์ประกอบแฮชอีกต่อไปแล้วสิ่งนี้จึงไม่มีปัญหาอีกต่อไป


4
@Baba มันทำ ผ่านไปยังฟังก์ชั่นเหมือนกับการทำ$foo = $arrayก่อนลูป;)
NikiC

32
สำหรับบรรดาของคุณที่ไม่ทราบว่า zval คืออะไรโปรดดูบล็อก
shu zOMG chen

1
การแก้ไขเล็กน้อย: สิ่งที่คุณเรียกว่า Bucket ไม่ใช่สิ่งที่ปกติเรียกว่า Bucket ใน hashtable ปกติ Bucket เป็นชุดของรายการที่มีขนาดแฮช% เท่ากัน คุณดูเหมือนจะใช้มันสำหรับสิ่งที่ปกติเรียกว่ารายการ รายการที่เชื่อมโยงไม่ได้อยู่ในที่เก็บข้อมูล แต่ในรายการ
unbeli

12
@unbeli ฉันใช้คำศัพท์ที่ใช้ภายในโดย PHP Buckets เป็นส่วนหนึ่งของรายการที่เชื่อมโยงเป็นทวีคูณสำหรับชนกัญชาและยังเป็นส่วนหนึ่งของรายการที่เชื่อมโยงเป็นทวีคูณสำหรับการสั่งซื้อ;)
Nikic

4
เยี่ยมมาก ฉันคิดว่าคุณหมายถึงiterate($outerArr);และไม่ได้iterate($arr);อยู่ที่ไหนสักแห่ง
niahoo

116

ในตัวอย่าง 3 คุณไม่ต้องแก้ไขอาร์เรย์ ในตัวอย่างอื่น ๆ ทั้งหมดคุณแก้ไขเนื้อหาหรือตัวชี้อาร์เรย์ภายใน นี่เป็นสิ่งสำคัญเมื่อพูดถึงอาร์เรย์PHPเพราะความหมายของผู้ดำเนินการที่ได้รับมอบหมาย

ผู้ประกอบการที่ได้รับมอบหมายสำหรับอาร์เรย์ใน PHP ทำงานเหมือนโคลนที่ขี้เกียจ การกำหนดตัวแปรหนึ่งให้กับอีกตัวแปรหนึ่งที่มีอาเรย์นั้นจะทำการโคลนอาเรย์ต่างจากภาษาส่วนใหญ่ อย่างไรก็ตามการโคลนนิ่งที่แท้จริงจะไม่สามารถทำได้เว้นแต่จะมีความจำเป็น ซึ่งหมายความว่าโคลนจะเกิดขึ้นเฉพาะเมื่อมีการเปลี่ยนแปลงตัวแปรอย่างใดอย่างหนึ่ง (คัดลอกเมื่อเขียน)

นี่คือตัวอย่าง:

$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 อาร์เรย์จะถูกปรับเปลี่ยนระหว่างลูปขณะที่การอ้างอิงพิเศษนี้ยังมีชีวิต สิ่งนี้ก่อให้เกิดโคลนและอธิบายสิ่งที่เกิดขึ้นที่นี่!

นี่คือบทความที่ยอดเยี่ยมสำหรับผลข้างเคียงอื่นของพฤติกรรมการคัดลอกเมื่อเขียนนี้: ผู้ประกอบการ Ternary PHP: เร็วหรือไม่


ดูเหมือนว่าถูกต้องของคุณฉันทำตัวอย่างที่แสดงให้เห็นว่า: codepad.org/OCjtvu8rข้อแตกต่างอย่างหนึ่งจากตัวอย่างของคุณ - มันไม่ได้คัดลอกหากคุณเปลี่ยนค่าเฉพาะเมื่อเปลี่ยนคีย์
zb '

นี่เป็นการอธิบายพฤติกรรมทั้งหมดที่แสดงด้านบนและสามารถแสดงได้อย่างชัดเจนโดยการเรียกeach()เมื่อสิ้นสุดการทดสอบครั้งแรกซึ่งเราเห็นว่าตัวชี้อาร์เรย์ของจุดเริ่มต้นดั้งเดิมชี้ไปที่องค์ประกอบที่สองเนื่องจากอาร์เรย์ถูกปรับเปลี่ยนระหว่าง การทำซ้ำครั้งแรก สิ่งนี้ดูเหมือนว่าจะแสดงให้เห็นว่าforeachย้ายตัวชี้อาร์เรย์ไปก่อนที่จะเรียกใช้งานโค้ดบล็อกของลูปซึ่งฉันไม่ได้คาดหวัง - ฉันคิดว่ามันน่าจะทำในตอนท้าย ขอบคุณมากนี่เป็นการลบล้างสำหรับฉันอย่างมาก
DaveRandom

49

บางจุดที่ควรทราบเมื่อทำงานกับforeach():

ก) foreachทำงานกับสำเนาที่คาดหวังของอาร์เรย์ดั้งเดิม มันหมายความว่าforeach()จะได้ร่วมกันจัดเก็บข้อมูลจนหรือเว้นแต่prospected copyจะไม่สร้างforeach หมายเหตุ / ความคิดเห็นของผู้ใช้

ข) เรียกสำเนามุ่งหน้า ? สำเนาที่คาดหมายจะถูกสร้างขึ้นตามนโยบายของcopy-on-writeซึ่งก็คือเมื่อใดก็ตามที่อาร์เรย์ที่ส่งผ่านไปforeach()ถูกเปลี่ยนจะมีการสร้างโคลนของอาร์เรย์ดั้งเดิมขึ้น

c) อาเรย์ดั้งเดิมและตัวforeach()วนซ้ำจะมีDISTINCT SENTINEL VARIABLESนั่นคือหนึ่งสำหรับอาเรย์ดั้งเดิมและอื่น ๆ สำหรับforeach; ดูรหัสทดสอบด้านล่าง SPL , Iteratorsและอาร์เรย์ Iterator

คำถาม Stack Overflow ทำอย่างไรเพื่อให้แน่ใจว่าค่าถูกรีเซ็ตในลูป '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

2
คำตอบของคุณไม่ถูกต้องนัก foreachดำเนินการกับสำเนาที่อาจเกิดขึ้นของอาร์เรย์ แต่จะไม่ทำสำเนาจริงยกเว้นว่าจำเป็น
linepogl

คุณต้องการสาธิตวิธีการและโอกาสในการคัดลอกที่สร้างขึ้นโดยใช้รหัสหรือไม่ รหัสของฉันแสดงให้เห็นว่าforeachกำลังคัดลอกอาร์เรย์ 100% ของเวลา ฉันอยากรู้ ขอบคุณสำหรับความคิดเห็นของคุณ
sakhunzai

การคัดลอกอาร์เรย์มีค่าใช้จ่ายสูง ลองนับเวลาที่ใช้ในการย้ำกับอาร์เรย์ 100000 องค์ประกอบในการใช้อย่างใดอย่างหนึ่งหรือfor foreachคุณจะไม่เห็นความแตกต่างอย่างมีนัยสำคัญระหว่างสองสิ่งนี้เนื่องจากไม่มีสำเนาจริงเกิดขึ้น
linepogl

แล้วฉันจะคิดว่ามีSHARED data storageลิขสิทธิ์หรือจนกว่าเว้นแต่copy-on-writeแต่ (จากข้อมูลโค้ดของฉัน) เห็นได้ชัดว่ามีจะเป็นสองชุดSENTINEL variablesหนึ่งสำหรับoriginal arrayและอื่น ๆ foreachสำหรับ ขอบคุณที่ทำให้รู้สึก
sakhunzai

1
ใช่นั่นคือสำเนา "ที่คาดหมาย" คือสำเนา "ที่มีโอกาสเป็นไปได้" ซึ่งไม่ได้รับการป้องกันตามที่คุณแนะนำ
sakhunzai

33

หมายเหตุสำหรับ PHP 7

หากต้องการอัปเดตคำตอบนี้เนื่องจากได้รับความนิยม: คำตอบนี้ใช้ไม่ได้กับ PHP 7 ตามที่อธิบายไว้ใน " การเปลี่ยนแปลงย้อนหลังที่เข้ากันไม่ได้ " ใน PHP 7 foreach ทำงานบนสำเนาของอาร์เรย์ดังนั้นการเปลี่ยนแปลงใด ๆ ในอาร์เรย์ ไม่ปรากฏบนลูป foreach รายละเอียดเพิ่มเติมได้ที่ลิงค์

คำอธิบาย (อ้างจากphp.net ):

แบบฟอร์มแรกวนซ้ำเหนืออาร์เรย์ที่กำหนดโดย array_expression ในการวนซ้ำแต่ละครั้งค่าขององค์ประกอบปัจจุบันจะถูกกำหนดให้กับ $ value และตัวชี้อาร์เรย์ภายในจะถูกดำเนินการขั้นสูงโดยหนึ่ง (ดังนั้นในการทำซ้ำครั้งถัดไปคุณจะมองไปที่องค์ประกอบถัดไป)

ดังนั้นในตัวอย่างแรกของคุณคุณจะมีเพียงหนึ่งองค์ประกอบในอาเรย์และเมื่อตัวชี้ถูกย้ายองค์ประกอบถัดไปไม่มีอยู่ดังนั้นหลังจากคุณเพิ่มองค์ประกอบใหม่ foreach จบลงเพราะมัน "ตัดสินใจ" แล้วว่ามันเป็นองค์ประกอบสุดท้าย

ในตัวอย่างที่สองของคุณคุณเริ่มต้นด้วยสององค์ประกอบและ foreach loop ไม่ได้อยู่ที่องค์ประกอบสุดท้ายดังนั้นจึงประเมินอาร์เรย์ในการทำซ้ำต่อไปและทำให้ตระหนักว่ามีองค์ประกอบใหม่ในอาร์เรย์

ผมเชื่อว่านี่เป็นผลที่ตามมาทั้งหมดในแต่ละซ้ำส่วนหนึ่งของคำอธิบายในเอกสารซึ่งอาจหมายถึงว่าไม่ตรรกะทั้งหมดก่อนที่มันเรียกรหัสใน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 'foreach' ใช้งานได้จริงอย่างไร ซึ่งอธิบายถึง internals ที่อยู่เบื้องหลังพฤติกรรมนี้


7
คุณอ่านคำตอบที่เหลือดีแล้วเหรอ? มันทำให้รู้สึกสมบูรณ์แบบที่ foreach ตัดสินใจว่าจะวนซ้ำอีกครั้งก่อนที่มันจะรันโค้ดในมัน
dkasipovic

2
ไม่อาร์เรย์ได้รับการแก้ไข แต่ "สายเกินไป" เนื่องจากก่อนหน้านี้ "คิด" แล้วว่าเป็นองค์ประกอบสุดท้าย (ซึ่งเป็นจุดเริ่มต้นของการทำซ้ำ) และจะไม่วนซ้ำอีกต่อไป ในตัวอย่างที่สองมันไม่ได้อยู่ที่องค์ประกอบสุดท้ายเมื่อเริ่มการวนซ้ำและประเมินอีกครั้งเมื่อเริ่มต้นการทำซ้ำครั้งถัดไป ฉันกำลังพยายามเตรียมกรณีทดสอบ
dkasipovic

1
@AlmaDo ดูที่lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509มันถูกตั้งค่าเป็นตัวชี้ถัดไปเสมอเมื่อมันวนซ้ำ ดังนั้นเมื่อถึงการทำซ้ำครั้งล่าสุดมันจะถูกทำเครื่องหมายว่าเสร็จแล้ว (ผ่านตัวชี้ NULL) เมื่อคุณเพิ่มคีย์ในการทำซ้ำครั้งล่าสุด foreach จะไม่สังเกตเห็น
bwoebi

1
@DKasipovic ไม่ ไม่มีคำอธิบายที่สมบูรณ์ & ชัดเจน (อย่างน้อยตอนนี้ - อาจเป็นเพราะฉันผิด)
แอลมาทำ

4
ที่จริงดูเหมือนว่า @AlmaDo มีข้อบกพร่องในการทำความเข้าใจตรรกะของเขาเอง ... คำตอบของคุณดี
bwoebi

15

ตามเอกสารที่จัดทำโดยคู่มือ PHP

ในการวนซ้ำแต่ละครั้งมูลค่าขององค์ประกอบปัจจุบันจะถูกกำหนดให้กับ $ v และ
ตัวชี้อาร์เรย์ภายในจะถูกดำเนินการขั้นสูงโดยหนึ่ง (ดังนั้นในการทำซ้ำครั้งถัดไปคุณจะมองไปที่องค์ประกอบถัดไป)

ดังนั้นตามตัวอย่างแรกของคุณ:

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

$arrayมีองค์ประกอบเดียวเท่านั้นดังนั้นตามการดำเนินการ foreach, 1 กำหนดให้$vและมันไม่มีองค์ประกอบอื่น ๆ ที่จะย้ายตัวชี้

แต่ในตัวอย่างที่สองของคุณ:

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

$arrayมีสององค์ประกอบดังนั้นตอนนี้ $ array ประเมินดัชนีเป็นศูนย์และย้ายตัวชี้ไปทีละรายการ สำหรับการวนซ้ำครั้งแรกของการวนซ้ำเพิ่ม$array['baz']=3;เป็น pass โดยการอ้างอิง


13

คำถามยอดเยี่ยมเนื่องจากนักพัฒนาจำนวนมากถึงแม้จะมีประสบการณ์ก็ตามก็สับสนกับวิธีที่ 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 ตามที่ปรากฏในสำเนาของ $ 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 จะทำกับสมาชิกของ $ set ดั้งเดิม การใช้ $ item by reference ช่วยป้องกัน 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 ครั้งเท่านั้นและแต่ละครั้งที่ผนวกค่าอื่นเข้ากับส่วนท้ายของชุด $ เดิมทำให้ชุด $ เดิมมี 6 องค์ประกอบ แต่ไม่เคยเข้าสู่วงวนไม่สิ้นสุด

อย่างไรก็ตามจะเกิดอะไรขึ้นถ้าเราใช้ $ item โดยอ้างอิงตามที่ฉันพูดถึงมาก่อน เพิ่มอักขระเดี่ยวในการทดสอบด้านบน:

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

ผลลัพธ์ในการวนซ้ำไม่สิ้นสุด โปรดทราบว่านี่เป็นการวนซ้ำไม่สิ้นสุดคุณจะต้องฆ่าสคริปต์ด้วยตัวเองหรือรอให้ระบบปฏิบัติการของคุณหมดหน่วยความจำ ฉันเพิ่มบรรทัดต่อไปนี้ในสคริปต์ของฉันดังนั้น PHP จะหมดหน่วยความจำอย่างรวดเร็วฉันขอแนะนำให้คุณทำเช่นเดียวกันถ้าคุณจะใช้การทดสอบวนรอบไม่สิ้นสุด

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

ดังนั้นในตัวอย่างก่อนหน้านี้ที่มีวงวนไม่สิ้นสุดเราจะเห็นสาเหตุที่ PHP เขียนขึ้นเพื่อสร้างสำเนาของอาร์เรย์เพื่อวนซ้ำ เมื่อสำเนาถูกสร้างและใช้งานโดยโครงสร้างของลูปเท่านั้นเองอาร์เรย์จะคงที่ตลอดการประมวลผลของลูปดังนั้นคุณจะไม่พบปัญหาใด ๆ


7

PHP foreach ห่วงสามารถใช้กับIndexed arrays, และAssociative arraysObject public variables

ในลูป foreach สิ่งแรกที่ 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 indexesreferences

ที่มา: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples


1
Object public variablesเป็นสิ่งที่ผิดหรือทำให้เข้าใจผิดอย่างดีที่สุด คุณไม่สามารถใช้วัตถุในอาร์เรย์โดยไม่ต้องใช้อินเทอร์เฟซที่ถูกต้อง (เช่น Traversible) และเมื่อคุณforeach((array)$obj ...ทำงานกับอาร์เรย์อย่างง่ายไม่ใช่วัตถุอีกต่อไป
คริสเตียน
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.