PHP 'foreach' thực sự hoạt động như thế nào?


2018

Hãy để tôi đặt tiền tố này bằng cách nói rằng tôi biết cái gì foreach, làm và cách sử dụng nó. Câu hỏi này liên quan đến cách thức hoạt động của nó dưới nắp ca-pô và tôi không muốn có bất kỳ câu trả lời nào dọc theo dòng chữ "đây là cách bạn lặp một mảng với foreach".


Trong một thời gian dài, tôi cho rằng nó foreachhoạt động với chính mảng đó. Sau đó, tôi tìm thấy nhiều tài liệu tham khảo về thực tế rằng nó hoạt động với một bản sao của mảng và từ đó tôi đã cho rằng đây là kết thúc của câu chuyện. Nhưng gần đây tôi đã có một cuộc thảo luận về vấn đề này, và sau một thử nghiệm nhỏ thấy rằng điều này thực tế không phải là sự thật 100%.

Hãy để tôi cho thấy những gì tôi có ý nghĩa. Đối với các trường hợp thử nghiệm sau, chúng tôi sẽ làm việc với mảng sau:

$array = array(1, 2, 3, 4, 5);

Trường hợp thử nghiệm 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 */

Điều này rõ ràng cho thấy rằng chúng tôi không làm việc trực tiếp với mảng nguồn - nếu không thì vòng lặp sẽ tiếp tục mãi mãi, vì chúng tôi liên tục đẩy các mục lên mảng trong vòng lặp. Nhưng chỉ để chắc chắn đây là trường hợp:

Trường hợp thử nghiệm 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 */

Điều này sao lưu kết luận ban đầu của chúng tôi, chúng tôi đang làm việc với một bản sao của mảng nguồn trong vòng lặp, nếu không chúng tôi sẽ thấy các giá trị được sửa đổi trong vòng lặp. Nhưng...

Nếu chúng ta xem trong hướng dẫn , chúng ta thấy tuyên bố này:

Khi foreach lần đầu tiên bắt đầu thực thi, con trỏ mảng bên trong sẽ tự động được đặt lại thành phần tử đầu tiên của mảng.

Phải ... điều này dường như gợi ý rằng foreachdựa vào con trỏ mảng của mảng nguồn. Nhưng chúng tôi đã chứng minh rằng chúng tôi không làm việc với mảng nguồn , phải không? Vâng, không hoàn toàn.

Trường hợp thử nghiệm 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)
*/

Vì vậy, mặc dù thực tế là chúng ta không làm việc trực tiếp với mảng nguồn, chúng ta đang làm việc trực tiếp với con trỏ mảng nguồn - thực tế là con trỏ nằm ở cuối mảng ở cuối vòng lặp cho thấy điều này. Ngoại trừ điều này không thể đúng - nếu đúng như vậy, thì trường hợp thử nghiệm 1 sẽ lặp lại mãi mãi.

Hướng dẫn PHP cũng nêu:

Vì foreach dựa vào con trỏ mảng bên trong, việc thay đổi nó trong vòng lặp có thể dẫn đến hành vi không mong muốn.

Chà, hãy tìm hiểu xem "hành vi bất ngờ" đó là gì (về mặt kỹ thuật, mọi hành vi đều bất ngờ vì tôi không còn biết phải trông đợi điều gì nữa).

Trường hợp thử nghiệm 4 :

foreach ($array as $key => $item) {
  echo "$item\n";
  each($array);
}

/* Output: 1 2 3 4 5 */

Trường hợp thử nghiệm 5 :

foreach ($array as $key => $item) {
  echo "$item\n";
  reset($array);
}

/* Output: 1 2 3 4 5 */

... không có gì bất ngờ ở đó, trên thực tế nó dường như hỗ trợ lý thuyết "bản sao nguồn".


Câu hỏi

Chuyện gì đang xảy ra ở đây? C-fu của tôi không đủ tốt để tôi có thể rút ra một kết luận đúng đắn chỉ bằng cách xem mã nguồn PHP, tôi sẽ đánh giá cao nếu ai đó có thể dịch nó sang tiếng Anh cho tôi.

Dường như với tôi, nó foreachhoạt động với một bản sao của mảng, nhưng đặt con trỏ mảng của mảng nguồn vào cuối mảng sau vòng lặp.

  • Điều này có đúng không và toàn bộ câu chuyện?
  • Nếu không, nó thực sự đang làm gì?
  • Có bất kỳ tình huống trong đó việc sử dụng các hàm điều chỉnh con trỏ mảng ( each(), reset()et al.) Trong quá trình foreachcó thể ảnh hưởng đến kết quả của vòng lặp không?

5
@DaveRandom Có một thẻ php-internals có thể đi cùng, nhưng tôi sẽ để nó cho bạn quyết định xem có bất kỳ thẻ nào trong 5 thẻ còn lại để thay thế không.
Michael Berkowski

5
trông giống như COW, không cần xử lý xóa
zb '

149
Lúc đầu tôi nghĩ »trời ạ, một câu hỏi mới khác. Đọc tài liệu về hm, hành vi không xác định rõ ràng «. Sau đó tôi đọc câu hỏi hoàn chỉnh, và tôi phải nói: Tôi thích nó. Bạn đã bỏ ra một số nỗ lực trong đó và viết tất cả các bản thử nghiệm. ps. Testcase 4 và 5 có giống nhau không?
knittl

21
Chỉ cần suy nghĩ về lý do tại sao nó có nghĩa là con trỏ mảng bị chạm: PHP cần đặt lại và di chuyển con trỏ mảng bên trong của mảng ban đầu cùng với bản sao, bởi vì người dùng có thể yêu cầu tham chiếu đến giá trị hiện tại ( foreach ($array as &$value)) - PHP cần biết vị trí hiện tại trong mảng ban đầu mặc dù nó thực sự lặp lại trên một bản sao.
Niko

4
@Sean: IMHO, tài liệu PHP thực sự khá tệ trong việc mô tả các sắc thái của các tính năng ngôn ngữ cốt lõi. Nhưng đó là, có lẽ, bởi vì rất nhiều trường hợp đặc biệt đặc biệt được đưa vào ngôn ngữ ...
Oliver Charlesworth

Câu trả lời:


1660

foreach hỗ trợ lặp qua ba loại giá trị khác nhau:

  • Mảng
  • Đối tượng bình thường
  • Traversable các đối tượng

Sau đây, tôi sẽ cố gắng giải thích chính xác cách thức lặp lại hoạt động trong các trường hợp khác nhau. Cho đến nay, trường hợp đơn giản nhất là Traversablecác đối tượng, vì về foreachcơ bản, đây chỉ là đường cú pháp cho mã dọc theo các dòng này:

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();
    /* ... */
}

Đối với các lớp bên trong, các cuộc gọi phương thức thực tế được tránh bằng cách sử dụng API nội bộ về cơ bản chỉ phản chiếu Iteratorgiao diện ở cấp độ C.

Lặp lại các mảng và các đối tượng đơn giản là phức tạp hơn đáng kể. Trước hết, cần lưu ý rằng trong "mảng" PHP là các từ điển thực sự được sắp xếp theo thứ tự và chúng sẽ được duyệt theo thứ tự này (khớp với thứ tự chèn miễn là bạn không sử dụng thứ gì đó như sort). Điều này trái ngược với việc lặp theo thứ tự tự nhiên của các khóa (cách danh sách trong các ngôn ngữ khác thường hoạt động) hoặc không có thứ tự xác định nào cả (từ điển trong các ngôn ngữ khác thường hoạt động như thế nào).

Điều tương tự cũng áp dụng cho các đối tượng, vì các thuộc tính đối tượng có thể được xem như là một tên thuộc tính ánh xạ từ điển (có thứ tự) khác với các giá trị của chúng, cộng với một số xử lý khả năng hiển thị. Trong phần lớn các trường hợp, các thuộc tính đối tượng không thực sự được lưu trữ theo cách khá kém hiệu quả này. Tuy nhiên, nếu bạn bắt đầu lặp lại một đối tượng, biểu diễn được đóng gói thường được sử dụng sẽ được chuyển đổi thành một từ điển thực sự. Tại thời điểm đó, việc lặp lại các đối tượng đơn giản trở nên rất giống với việc lặp lại các mảng (đó là lý do tại sao tôi không thảo luận nhiều về việc lặp lại đối tượng đơn giản ở đây).

Càng xa càng tốt. Lặp lại từ điển không thể quá khó, phải không? Các vấn đề bắt đầu khi bạn nhận ra rằng một mảng / đối tượng có thể thay đổi trong quá trình lặp. Có nhiều cách điều này có thể xảy ra:

  • Nếu bạn lặp theo tham chiếu bằng cách sử dụng foreach ($arr as &$v)thì $arrđược chuyển thành tham chiếu và bạn có thể thay đổi nó trong quá trình lặp.
  • Trong PHP 5, điều tương tự cũng được áp dụng ngay cả khi bạn lặp theo giá trị, nhưng mảng là tham chiếu trước: $ref =& $arr; foreach ($ref as $v)
  • Các đối tượng có xử lý thông qua ngữ nghĩa, đối với hầu hết các mục đích thực tế có nghĩa là chúng hành xử giống như các tài liệu tham khảo. Vì vậy, các đối tượng luôn có thể được thay đổi trong quá trình lặp.

Vấn đề với việc cho phép sửa đổi trong quá trình lặp là trường hợp phần tử bạn hiện đang bị loại bỏ. Giả sử bạn sử dụng một con trỏ để theo dõi phần tử mảng nào bạn đang ở. Nếu phần tử này được giải phóng, bạn sẽ bị bỏ lại một con trỏ lơ lửng (thường dẫn đến một segfault).

Có nhiều cách khác nhau để giải quyết vấn đề này. PHP 5 và PHP 7 khác nhau đáng kể về vấn đề này và tôi sẽ mô tả cả hai hành vi sau đây. Tóm tắt là cách tiếp cận của PHP 5 khá ngu ngốc và dẫn đến tất cả các loại vấn đề cạnh kỳ lạ, trong khi cách tiếp cận liên quan hơn của PHP 7 dẫn đến hành vi nhất quán và dễ đoán hơn.

Như một sơ bộ cuối cùng, cần lưu ý rằng PHP sử dụng tính năng tham chiếu và sao chép khi ghi để quản lý bộ nhớ. Điều này có nghĩa là nếu bạn "sao chép" một giá trị, bạn thực sự chỉ cần sử dụng lại giá trị cũ và tăng số tham chiếu của nó (đếm lại). Chỉ khi bạn thực hiện một số loại sửa đổi, một bản sao thực sự (được gọi là "sao chép") sẽ được thực hiện. Xem Bạn đang bị lừa dối để giới thiệu rộng rãi hơn về chủ đề này.

PHP 5

Con trỏ mảng nội bộ và HashPulum

Mảng trong PHP 5 có một "con trỏ mảng bên trong" (IAP) chuyên dụng, hỗ trợ chính xác các sửa đổi: Bất cứ khi nào một phần tử được loại bỏ, sẽ có một kiểm tra xem IAP có trỏ đến phần tử này không. Nếu có, nó được chuyển sang phần tử tiếp theo thay thế.

Mặc dù foreachsử dụng IAP, có một biến chứng bổ sung: Chỉ có một IAP, nhưng một mảng có thể là một phần của nhiều foreachvòng lặp:

// 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) {
        // ...
    }
}

Để hỗ trợ hai vòng lặp đồng thời chỉ với một con trỏ mảng bên trong, hãy foreachthực hiện các shenanigans sau: Trước khi thân vòng lặp được thực thi, foreachsẽ sao lưu một con trỏ tới phần tử hiện tại và hàm băm của nó thành một pereach HashPointer. Sau khi thân vòng lặp chạy, IAP sẽ được đặt lại thành phần tử này nếu nó vẫn tồn tại. Tuy nhiên, nếu phần tử đã bị xóa, chúng tôi sẽ sử dụng bất cứ nơi nào IAP hiện đang ở. Sơ đồ này hầu hết là các loại công việc, nhưng có rất nhiều hành vi kỳ lạ mà bạn có thể thoát khỏi nó, một số trong đó tôi sẽ trình bày dưới đây.

Sao chép mảng

IAP là một tính năng có thể nhìn thấy của một mảng (được thể hiện thông qua currenthọ các hàm), do đó các thay đổi đối với số IAP là các sửa đổi theo ngữ nghĩa sao chép khi ghi. Thật không may, điều này có nghĩa foreachlà trong nhiều trường hợp buộc phải sao chép mảng mà nó đang lặp lại. Các điều kiện chính xác là:

  1. Mảng không phải là một tham chiếu (is_Vf = 0). Nếu đó là một tài liệu tham khảo, thì những thay đổi đối với nó được cho là truyền bá, vì vậy nó không nên bị trùng lặp.
  2. Mảng có số lượng> 1. Nếu refcountlà 1, thì mảng không được chia sẻ và chúng tôi có thể tự do sửa đổi nó trực tiếp.

Nếu mảng không được nhân đôi (is_Vf = 0, refcount = 1), thì chỉ mảng của nó refcountsẽ được tăng (*). Ngoài ra, nếu foreachtham chiếu được sử dụng, thì mảng (có khả năng trùng lặp) sẽ được chuyển thành tham chiếu.

Hãy xem mã này như một ví dụ khi xảy ra sự trùng lặp:

function iterate($arr) {
    foreach ($arr as $v) {}
}

$outerArr = [0, 1, 2, 3, 4];
iterate($outerArr);

Tại đây, $arrsẽ được nhân đôi để ngăn chặn các thay đổi IAP trên $arrbị rò rỉ sang $outerArr. Xét về các điều kiện trên, mảng không phải là tham chiếu (is numf = 0) và được sử dụng ở hai nơi (refcount = 2). Yêu cầu này là không may và là một yếu tố của việc triển khai dưới mức tối ưu (không có vấn đề sửa đổi trong quá trình lặp ở đây, vì vậy chúng tôi không thực sự cần sử dụng IAP ở nơi đầu tiên).

(*) Tăng thêm refcountở đây nghe có vẻ vô hại, nhưng vi phạm ngữ nghĩa sao chép trên ghi (COW): Điều này có nghĩa là chúng tôi sẽ sửa đổi IAP của một refcount = 2, trong khi COW chỉ ra rằng sửa đổi chỉ có thể được thực hiện trên refcount = 1 giá trị. Vi phạm này dẫn đến thay đổi hành vi người dùng có thể nhìn thấy (trong khi COW thường trong suốt) vì thay đổi IAP trên mảng lặp sẽ có thể quan sát được - nhưng chỉ cho đến khi sửa đổi không phải IAP đầu tiên trên mảng. Thay vào đó, ba tùy chọn "hợp lệ" sẽ là a) luôn luôn trùng lặp, b) không tăng refcountvà do đó cho phép mảng lặp được sửa đổi tùy ý trong vòng lặp hoặc c) hoàn toàn không sử dụng IAP (PHP 7 giải pháp).

Thứ tự thăng tiến

Có một chi tiết triển khai cuối cùng mà bạn phải biết để hiểu đúng các mẫu mã dưới đây. Cách lặp "thông thường" thông qua một số cấu trúc dữ liệu sẽ trông giống như thế này trong mã giả:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    code();
    move_forward(arr);
}

Tuy nhiên foreach, là một bông tuyết khá đặc biệt, chọn làm mọi thứ hơi khác một chút:

reset(arr);
while (get_current_data(arr, &data) == SUCCESS) {
    move_forward(arr);
    code();
}

Cụ thể, con trỏ mảng đã được di chuyển về phía trước trước khi thân vòng lặp chạy. Điều này có nghĩa là trong khi thân vòng lặp đang hoạt động trên phần tử $i, IAP đã có sẵn ở phần tử $i+1. Đây là lý do tại sao các mẫu mã hiển thị sửa đổi trong quá trình lặp sẽ luôn unsetlà phần tử tiếp theo , thay vì phần tử hiện tại.

Ví dụ: Các trường hợp thử nghiệm của bạn

Ba khía cạnh được mô tả ở trên sẽ cung cấp cho bạn một ấn tượng gần như hoàn chỉnh về các đặc điểm riêng của việc foreachtriển khai và chúng ta có thể chuyển sang thảo luận về một số ví dụ.

Hành vi của các trường hợp thử nghiệm của bạn rất đơn giản để giải thích tại thời điểm này:

  • Trong trường hợp thử nghiệm 1 và 2 $arraybắt đầu với refcount = 1, do đó, nó sẽ không bị trùng lặp bởi foreach: Chỉ refcounttăng được. Khi thân vòng lặp sau đó sửa đổi mảng (có refcount = 2 tại điểm đó), sự trùng lặp sẽ xảy ra tại điểm đó. Foreach sẽ tiếp tục làm việc trên một bản sao chưa sửa đổi $array.

  • Trong trường hợp thử nghiệm 3, một lần nữa mảng không bị trùng lặp, do đó foreachsẽ sửa đổi IAP của $arraybiến. Khi kết thúc việc lặp lại, IAP là NULL (có nghĩa là việc lặp lại đã hoàn thành), eachbiểu thị bằng cách quay lại false.

  • Trong các trường hợp thử nghiệm 4 và 5 cả hai eachresetlà các hàm tham chiếu. Có $arraymột refcount=2khi nó được truyền cho họ, vì vậy nó phải được nhân đôi. Như vậy foreachsẽ làm việc trên một mảng riêng biệt một lần nữa.

Ví dụ: Tác dụng của currentforeach

Một cách tốt để thể hiện các hành vi trùng lặp khác nhau là quan sát hành vi của current()hàm bên trong một foreachvòng lặp. Xem xét ví dụ này:

foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 2 2 2 2 */

Ở đây bạn nên biết rằng đó current()là một hàm by-ref (thực ra là: prefer-ref), mặc dù nó không sửa đổi mảng. Nó phải là để chơi tốt với tất cả các chức năng khác giống như nexttất cả các phụ. Truyền qua tham chiếu ngụ ý rằng mảng phải được tách ra và do đó $arrayforeach-arraysẽ khác nhau. Lý do bạn nhận được 2thay vì 1cũng được đề cập ở trên: foreachtiến bộ con trỏ mảng trước khi chạy mã người dùng, không phải sau. Vì vậy, mặc dù mã nằm ở phần tử đầu tiên, nhưng foreachđã nâng con trỏ lên phần tử thứ hai.

Bây giờ hãy thử một sửa đổi nhỏ:

$ref = &$array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Ở đây chúng ta có trường hợp is_Vf = 1, vì vậy mảng không được sao chép (giống như trên). Nhưng bây giờ nó là một tham chiếu, mảng không còn phải được sao chép khi chuyển đến hàm by-ref current(). Do đó current()foreachlàm việc trên cùng một mảng. Tuy nhiên, bạn vẫn thấy hành vi tắt, do cách foreachtiến con trỏ.

Bạn nhận được hành vi tương tự khi thực hiện lặp đi lặp lại:

foreach ($array as &$val) {
    var_dump(current($array));
}
/* Output: 2 3 4 5 false */

Ở đây, phần quan trọng là foreach sẽ tạo $arraymột is numf = 1 khi nó được lặp lại bởi tham chiếu, vì vậy về cơ bản bạn có tình huống tương tự như trên.

Một biến thể nhỏ khác, lần này chúng ta sẽ gán mảng cho một biến khác:

$foo = $array;
foreach ($array as $val) {
    var_dump(current($array));
}
/* Output: 1 1 1 1 1 */

Ở đây số lần đếm $arraylà 2 khi vòng lặp được bắt đầu, vì vậy lần đầu tiên chúng ta thực sự phải thực hiện sao chép trước. Do đó $arrayvà mảng được sử dụng bởi foreach sẽ hoàn toàn tách biệt với phần đầu. Đó là lý do tại sao bạn có được vị trí của IAP ở bất cứ đâu trước vòng lặp (trong trường hợp này là ở vị trí đầu tiên).

Ví dụ: Sửa đổi trong quá trình lặp

Cố gắng tính đến các sửa đổi trong quá trình lặp là nơi tất cả các rắc rối foreach của chúng tôi bắt nguồn, vì vậy nó phục vụ để xem xét một số ví dụ cho trường hợp này.

Hãy xem xét các vòng lặp lồng nhau này trên cùng một mảng (trong đó phép lặp by-ref được sử dụng để đảm bảo rằng nó thực sự giống nhau):

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)

Phần dự kiến ​​ở đây là (1, 2)thiếu từ đầu ra vì phần tử 1đã bị xóa. Điều có lẽ bất ngờ là vòng lặp bên ngoài dừng lại sau phần tử đầu tiên. Tại sao vậy?

Lý do đằng sau điều này là hack vòng lặp lồng nhau được mô tả ở trên: Trước khi thân vòng lặp chạy, vị trí và hàm băm IAP hiện tại được sao lưu vào a HashPointer. Sau phần thân vòng lặp, nó sẽ được khôi phục, nhưng chỉ khi phần tử vẫn tồn tại, nếu không thì vị trí IAP hiện tại (bất kể nó có thể là gì) được sử dụng thay thế. Trong ví dụ trên đây chính xác là trường hợp: Phần tử hiện tại của vòng lặp bên ngoài đã bị xóa, vì vậy nó sẽ sử dụng IAP, đã được đánh dấu là đã hoàn thành bởi vòng lặp bên trong!

Một hậu quả khác của HashPointercơ chế sao lưu + khôi phục là những thay đổi đối với IAP thông qua reset()vv thường không ảnh hưởng foreach. Ví dụ, đoạn mã sau thực thi như reset()thể không có mặt ở tất cả:

$array = [1, 2, 3, 4, 5];
foreach ($array as &$value) {
    var_dump($value);
    reset($array);
}
// output: 1, 2, 3, 4, 5

Lý do là, trong khi reset()tạm thời sửa đổi IAP, nó sẽ được khôi phục thành phần tử foreach hiện tại sau thân vòng lặp. Để buộc reset()tạo hiệu ứng trên vòng lặp, bạn phải loại bỏ thêm phần tử hiện tại để cơ chế sao lưu / khôi phục không thành công:

$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

Nhưng, những ví dụ đó vẫn còn lành mạnh. Thú vui thực sự bắt đầu nếu bạn nhớ rằng HashPointerkhôi phục sử dụng một con trỏ đến phần tử và hàm băm của nó để xác định xem nó có còn tồn tại hay không. Nhưng: Băm có va chạm, và con trỏ có thể được sử dụng lại! Điều này có nghĩa là, với sự lựa chọn cẩn thận các phím mảng, chúng ta có thể foreachtin rằng một phần tử đã bị xóa vẫn tồn tại, vì vậy nó sẽ nhảy trực tiếp vào nó. Một ví dụ:

$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

Ở đây chúng ta thường mong đợi đầu ra 1, 1, 3, 4theo các quy tắc trước đó. Làm thế nào những gì xảy ra là 'FYFY'có cùng hàm băm với phần tử bị loại bỏ 'EzFY'và bộ cấp phát xảy ra để sử dụng lại cùng một vị trí bộ nhớ để lưu trữ phần tử. Vì vậy, foreach kết thúc trực tiếp nhảy đến phần tử mới được chèn, do đó cắt ngắn vòng lặp.

Thay thế thực thể lặp trong vòng lặp

Một trường hợp kỳ lạ cuối cùng mà tôi muốn đề cập, đó là PHP cho phép bạn thay thế thực thể lặp trong vòng lặp. Vì vậy, bạn có thể bắt đầu lặp lại trên một mảng và sau đó thay thế nó bằng một mảng khác giữa chừng. Hoặc bắt đầu lặp lại trên một mảng và sau đó thay thế nó bằng một đối tượng:

$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 */

Như bạn có thể thấy trong trường hợp này, PHP sẽ chỉ bắt đầu lặp lại thực thể khác từ đầu một khi sự thay thế đã xảy ra.

PHP 7

Trình lặp Hashtable

Nếu bạn vẫn còn nhớ, vấn đề chính của việc lặp mảng là làm thế nào để xử lý loại bỏ các phần tử giữa các lần lặp. PHP 5 đã sử dụng một con trỏ mảng bên trong (IAP) cho mục đích này, điều này hơi tối ưu, vì một con trỏ mảng phải được kéo dài để hỗ trợ nhiều vòng lặp foreach đồng thời tương tác với reset()v.v.

PHP 7 sử dụng một cách tiếp cận khác, cụ thể là, nó hỗ trợ tạo ra một số lượng các trình lặp có thể băm bên ngoài, an toàn tùy ý. Các trình lặp này phải được đăng ký trong mảng, từ đó chúng có cùng ngữ nghĩa với IAP: Nếu một phần tử mảng bị loại bỏ, tất cả các trình lặp có thể băm chỉ vào phần tử đó sẽ được chuyển sang phần tử tiếp theo.

Điều này có nghĩa rằng foreachsẽ không còn sử dụng IAP ở tất cả . Các foreachvòng lặp sẽ được hoàn toàn không ảnh hưởng đến kết quả current()vv và hành vi của mình sẽ không bao giờ bị ảnh hưởng bởi các chức năng như reset(), vv

Sao chép mảng

Một thay đổi quan trọng khác giữa PHP 5 và PHP 7 liên quan đến sao chép mảng. Bây giờ IAP không còn được sử dụng nữa, việc lặp mảng theo giá trị sẽ chỉ thực hiện một refcountbước tăng (thay vì sao chép mảng) trong mọi trường hợp. Nếu mảng được sửa đổi trong foreachvòng lặp, tại thời điểm đó sẽ xảy ra sự trùng lặp (theo bản sao khi ghi) và foreachsẽ tiếp tục hoạt động trên mảng cũ.

Trong hầu hết các trường hợp, thay đổi này là minh bạch và không có tác dụng nào khác ngoài hiệu suất tốt hơn. Tuy nhiên, có một trường hợp dẫn đến hành vi khác nhau, cụ thể là trường hợp mảng là tham chiếu trước:

$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 */

Lặp lại giá trị trước của mảng tham chiếu là trường hợp đặc biệt. Trong trường hợp này, không có sự trùng lặp xảy ra, vì vậy tất cả các sửa đổi của mảng trong quá trình lặp sẽ được phản ánh bởi vòng lặp. Trong PHP 7, trường hợp đặc biệt này không còn nữa: Lặp lại theo giá trị của một mảng sẽ luôn tiếp tục làm việc trên các phần tử gốc, bỏ qua mọi sửa đổi trong vòng lặp.

Tất nhiên, điều này không áp dụng cho phép lặp theo tham chiếu. Nếu bạn lặp lại theo tham chiếu, tất cả các sửa đổi sẽ được phản ánh bởi vòng lặp. Thật thú vị, điều tương tự cũng đúng với phép lặp theo giá trị của các đối tượng đơn giản:

$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 */

Điều này phản ánh ngữ nghĩa của các đối tượng (nghĩa là chúng hành xử giống như tham chiếu ngay cả trong bối cảnh giá trị).

Ví dụ

Hãy xem xét một vài ví dụ, bắt đầu với các trường hợp thử nghiệm của bạn:

  • Các trường hợp kiểm tra 1 và 2 giữ nguyên đầu ra: Lặp lại mảng giá trị luôn luôn làm việc trên các phần tử gốc. (Trong trường hợp này, refcountinghành vi chẵn và trùng lặp hoàn toàn giống nhau giữa PHP 5 và PHP 7).

  • Trường hợp thử nghiệm 3 thay đổi: Foreachkhông còn sử dụng IAP, do đó each()không bị ảnh hưởng bởi vòng lặp. Nó sẽ có cùng một đầu ra trước và sau.

  • Các trường hợp thử nghiệm 4 và 5 giữ nguyên: each()reset()sẽ nhân đôi mảng trước khi thay đổi IAP, trong khi foreachvẫn sử dụng mảng ban đầu. (Không phải thay đổi IAP sẽ có vấn đề, ngay cả khi mảng được chia sẻ.)

Nhóm ví dụ thứ hai có liên quan đến hành vi của current()các reference/refcountingcấu hình khác nhau . Điều này không còn có ý nghĩa, vì current()hoàn toàn không bị ảnh hưởng bởi vòng lặp, vì vậy giá trị trả về của nó luôn giữ nguyên.

Tuy nhiên, chúng tôi nhận được một số thay đổi thú vị khi xem xét sửa đổi trong quá trình lặp. Tôi hy vọng bạn sẽ tìm thấy saner hành vi mới. Ví dụ đầu tiên:

$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) 

Như bạn có thể thấy, vòng lặp bên ngoài không còn hủy bỏ sau lần lặp đầu tiên. Lý do là cả hai vòng lặp đều có các vòng lặp băm hoàn toàn riêng biệt và không còn bất kỳ sự lây nhiễm chéo nào của cả hai vòng thông qua IAP được chia sẻ.

Một trường hợp cạnh kỳ lạ khác đã được sửa bây giờ, là hiệu ứng kỳ lạ bạn nhận được khi xóa và thêm các phần tử có cùng hàm băm:

$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

Trước đây, cơ chế khôi phục HashPulum đã nhảy ngay sang phần tử mới vì nó "trông" giống như phần tử bị loại bỏ (do va chạm băm và con trỏ). Vì chúng tôi không còn dựa vào hàm băm cho bất cứ điều gì, điều này không còn là vấn đề nữa.


4
@Baba Nó làm. Truyền nó cho một chức năng cũng giống như thực hiện $foo = $arraytrước vòng lặp;)
NikiC

32
Đối với những người không biết zval là gì, vui lòng tham khảo blog
shu zOMG chen

1
Hiệu chỉnh nhỏ: những gì bạn gọi là Buck không phải là cái thường được gọi là Nhóm trong một hashtable. Thông thường Xô là một tập hợp các mục có cùng kích thước% băm. Bạn dường như sử dụng nó cho những gì thường được gọi là một mục. Danh sách liên kết không phải trên xô, nhưng trên các mục.
unbeli

12
@unbeli Tôi đang sử dụng thuật ngữ được sử dụng bởi PHP. Các Bucketphần này là một phần của danh sách được liên kết đôi cho các va chạm băm và cũng là một phần của danh sách được liên kết đôi theo thứ tự;)
NikiC

4
Anwser tuyệt vời. Tôi nghĩ bạn có ý nghĩa iterate($outerArr);và không phải iterate($arr);ở đâu đó.
niahoo

116

Trong ví dụ 3 bạn không sửa đổi mảng. Trong tất cả các ví dụ khác, bạn sửa đổi nội dung hoặc con trỏ mảng bên trong. Điều này rất quan trọng khi nói đến mảng PHP vì ngữ nghĩa của toán tử gán.

Toán tử gán cho các mảng trong PHP hoạt động giống như một bản sao lười biếng. Việc gán một biến cho một biến khác có chứa một mảng sẽ sao chép mảng đó, không giống như hầu hết các ngôn ngữ. Tuy nhiên, việc nhân bản thực tế sẽ không được thực hiện trừ khi cần thiết. Điều này có nghĩa là bản sao sẽ chỉ diễn ra khi một trong hai biến được sửa đổi (copy-on-write).

Đây là một ví dụ:

$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.

Quay trở lại các trường hợp thử nghiệm của bạn, bạn có thể dễ dàng tưởng tượng rằng nó foreachtạo ra một số loại trình vòng lặp có tham chiếu đến mảng. Tham chiếu này hoạt động chính xác như biến $btrong ví dụ của tôi. Tuy nhiên, iterator cùng với tham chiếu chỉ sống trong vòng lặp và sau đó, cả hai đều bị loại bỏ. Bây giờ bạn có thể thấy rằng, trong tất cả các trường hợp ngoại trừ 3, mảng được sửa đổi trong vòng lặp, trong khi tham chiếu bổ sung này còn sống. Điều này kích hoạt một bản sao, và điều đó giải thích những gì đang xảy ra ở đây!

Đây là một bài viết tuyệt vời cho một tác dụng phụ khác của hành vi sao chép trên ghi này: Toán tử Ternary PHP: Nhanh hay không?


có vẻ đúng, bạn đã đưa ra một số ví dụ chứng minh rằng: codepad.org/OCjtvu8r một điểm khác biệt so với ví dụ của bạn - nó không sao chép nếu bạn thay đổi giá trị, chỉ khi thay đổi khóa.
zb '

Điều này thực sự giải thích tất cả các hành vi hiển thị ở trên và nó có thể được minh họa độc đáo bằng cách gọi each()vào cuối trường hợp thử nghiệm đầu tiên, trong đó chúng ta thấy rằng con trỏ mảng của mảng ban đầu trỏ đến phần tử thứ hai, vì mảng đã được sửa đổi trong khi lần lặp đầu tiên. Điều này dường như cũng chứng minh rằng foreachdi chuyển con trỏ mảng trước khi thực hiện khối mã của vòng lặp, điều mà tôi không mong đợi - tôi đã nghĩ rằng nó sẽ làm điều này vào cuối. Rất cám ơn, điều này xóa nó cho tôi độc đáo.
DaveRandom

49

Một số điểm cần lưu ý khi làm việc với foreach():

a) foreachhoạt động trên bản sao dự kiến của mảng ban đầu. Điều đó có nghĩa là foreach()sẽ có lưu trữ dữ liệu CHIA SẺ cho đến khi hoặc trừ khi prospected copykhông được tạo ra để ghi chú Ghi chú / Nhận xét của người dùng .

b) Điều gì kích hoạt một bản sao tiềm năng ? Một bản sao có triển vọng được tạo dựa trên chính sách của copy-on-write, nghĩa là, bất cứ khi nào một mảng được truyền đến foreach()được thay đổi, một bản sao của mảng ban đầu sẽ được tạo.

c) Mảng ban đầu và foreach()iterator sẽ có DISTINCT SENTINEL VARIABLES, nghĩa là, một cho mảng ban đầu và khác cho mảng ban đầu foreach; xem mã kiểm tra bên dưới SPL , IteratorsArray Iterator .

Câu hỏi về Stack Overflow Làm thế nào để đảm bảo giá trị được đặt lại trong vòng lặp 'foreach' trong PHP? giải quyết các trường hợp (3,4,5) câu hỏi của bạn.

Ví dụ sau đây cho thấy rằng mỗi () và reset () KHÔNG ảnh hưởng đến SENTINELcác biến (for example, the current index variable)của foreach()trình vòng lặp.

$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/>";

Đầu ra:

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
Câu trả lời của bạn không hoàn toàn chính xác. foreachhoạt động trên một bản sao tiềm năng của mảng, nhưng nó không tạo ra bản sao thực sự trừ khi cần thiết.
linepogl

Bạn có muốn chứng minh làm thế nào và khi nào bản sao tiềm năng đó được tạo thông qua mã? Mã của tôi chứng minh rằng foreachđang sao chép mảng 100% thời gian. Tôi háo hức muốn biết. Cảm ơn bạn đã bình luận
sakhunzai

Sao chép một mảng chi phí rất nhiều. Hãy thử đếm thời gian cần thiết để lặp lại một mảng với 100000 phần tử bằng cách sử dụng forhoặc foreach. Bạn sẽ không thấy bất kỳ sự khác biệt đáng kể nào giữa hai người họ, bởi vì một bản sao thực sự không diễn ra.
linepogl

Sau đó, tôi sẽ cho rằng có SHARED data storagedành riêng cho đến khi hoặc trừ khi copy-on-write, nhưng (từ đoạn mã của tôi), điều hiển nhiên là sẽ luôn có HAI bộ SENTINEL variablescho cái này original arrayvà cái kia cho foreach. Cảm ơn điều đó có ý nghĩa
sakhunzai

1
vâng, đó là bản sao "có triển vọng" tức là bản sao "tiềm năng". Tôi không được bảo vệ như bạn đề xuất
sakhunzai

33

LƯU Ý CHO PHP 7

Để cập nhật câu trả lời này vì nó đã trở nên phổ biến: Câu trả lời này không còn áp dụng kể từ PHP 7. Như đã giải thích trong " Thay đổi không tương thích ngược ", trong PHP 7 foreach hoạt động trên bản sao của mảng, do đó, mọi thay đổi trên chính mảng không được phản ánh trên vòng lặp foreach. Thêm chi tiết tại liên kết.

Giải thích (trích dẫn từ php.net ):

Các vòng lặp biểu mẫu đầu tiên trên mảng được cung cấp bởi Array_expression. Trên mỗi lần lặp, giá trị của phần tử hiện tại được gán cho giá trị $ và con trỏ mảng bên trong được nâng cao bởi một (vì vậy trong lần lặp tiếp theo, bạn sẽ xem xét phần tử tiếp theo).

Vì vậy, trong ví dụ đầu tiên của bạn, bạn chỉ có một phần tử trong mảng và khi con trỏ được di chuyển thì phần tử tiếp theo không tồn tại, vì vậy sau khi bạn thêm phần tử foreach mới kết thúc vì nó đã "quyết định" nó là phần tử cuối cùng.

Trong ví dụ thứ hai của bạn, bạn bắt đầu với hai phần tử và vòng lặp foreach không nằm ở phần tử cuối cùng để nó đánh giá mảng trên lần lặp tiếp theo và do đó nhận ra rằng có phần tử mới trong mảng.

Tôi tin rằng đây là tất cả hệ quả của Trên mỗi phần lặp của phần giải thích trong tài liệu, điều này có thể có nghĩa là foreachtất cả logic trước khi nó gọi mã {}.

Trường hợp thử nghiệm

Nếu bạn chạy này:

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

Bạn sẽ nhận được đầu ra này:

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

Điều đó có nghĩa là nó đã chấp nhận sửa đổi và đã trải qua vì nó đã được sửa đổi "kịp thời". Nhưng nếu bạn làm điều này:

<?
    $array = Array(
        'foo' => 1,
        'bar' => 2
    );
    foreach($array as $k=>&$v) {
        if ($k=='bar') {
            $array['baz']=3;
        }
        echo $v." ";
    }
    print_r($array);
?>

Bạn sẽ nhận được:

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

Điều đó có nghĩa là mảng đó đã được sửa đổi, nhưng vì chúng tôi đã sửa đổi nó khi phần tử foreachđã ở phần tử cuối cùng của mảng, nó "quyết định" không lặp lại nữa, và mặc dù chúng tôi đã thêm phần tử mới, chúng tôi đã thêm nó "quá muộn" và nó đã không được lặp qua.

Có thể đọc giải thích chi tiết tại PHP 'foreach' thực sự hoạt động như thế nào? trong đó giải thích nội bộ đằng sau hành vi này.


7
Vâng, bạn đã đọc phần còn lại của câu trả lời? Nó có ý nghĩa hoàn hảo rằng foreach quyết định nếu nó sẽ lặp lại lần khác trước khi nó thậm chí chạy mã trong đó.
dkasipovic

2
Không, mảng đã được sửa đổi, nhưng "quá muộn" vì foreach đã "nghĩ" rằng đó là phần tử cuối cùng (mà nó là lúc bắt đầu lặp lại) và sẽ không lặp lại nữa. Trong ví dụ thứ hai, nó không nằm ở phần tử cuối cùng khi bắt đầu lặp và đánh giá lại khi bắt đầu lần lặp tiếp theo. Tôi đang cố gắng chuẩn bị một trường hợp thử nghiệm.
dkasipovic

1
@AlmaDo Hãy xem lxr.php.net/xref/PHP_TRUNK/Zend/zend_vm_def.h#4509 Nó luôn được đặt thành con trỏ tiếp theo khi lặp lại. Vì vậy, khi nó đạt đến lần lặp cuối cùng, nó sẽ được đánh dấu là đã hoàn thành (thông qua con trỏ NULL). Khi bạn thêm một khóa trong lần lặp cuối cùng, foreach sẽ không nhận thấy nó.
bwoebi

1
@DKasipovic không. Không có lời giải thích đầy đủ và rõ ràng ở đó (ít nhất là bây giờ - có thể tôi sai)
Alma Do

4
Trên thực tế, có vẻ như @AlmaDo có một lỗ hổng trong việc hiểu logic của chính mình. Câu trả lời của bạn là tốt.
bwoebi

15

Theo tài liệu được cung cấp bởi hướng dẫn PHP.

Trên mỗi lần lặp, giá trị của phần tử hiện tại được gán cho $ v và
con trỏ mảng bên trong được nâng cao bởi một (vì vậy trong lần lặp tiếp theo, bạn sẽ xem xét phần tử tiếp theo).

Vì vậy, theo ví dụ đầu tiên của bạn:

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

$arraychỉ có một phần tử duy nhất, vì vậy theo thực thi foreach, 1 gán cho $vvà nó không có bất kỳ phần tử nào khác để di chuyển con trỏ

Nhưng trong ví dụ thứ hai của bạn:

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

$arraycó hai phần tử, vì vậy bây giờ $ mảng đánh giá các chỉ số 0 và di chuyển con trỏ theo một. Đối với lần lặp đầu tiên của vòng lặp, được thêm $array['baz']=3;dưới dạng tham chiếu.


13

Câu hỏi tuyệt vời, bởi vì nhiều nhà phát triển, ngay cả những người có kinh nghiệm, bị nhầm lẫn bởi cách PHP xử lý các mảng trong các vòng lặp foreach. Trong vòng lặp foreach tiêu chuẩn, PHP tạo một bản sao của mảng được sử dụng trong vòng lặp. Bản sao được loại bỏ ngay sau khi vòng lặp kết thúc. Điều này là minh bạch trong hoạt động của một vòng lặp foreach đơn giản. Ví dụ:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    echo "{$item}\n";
}

Kết quả này:

apple
banana
coconut

Vì vậy, bản sao được tạo nhưng nhà phát triển không thông báo, vì mảng ban đầu không được tham chiếu trong vòng lặp hoặc sau khi vòng lặp kết thúc. Tuy nhiên, khi bạn cố gắng sửa đổi các mục trong một vòng lặp, bạn thấy rằng chúng không được sửa đổi khi bạn hoàn thành:

$set = array("apple", "banana", "coconut");
foreach ( $set AS $item ) {
    $item = strrev ($item);
}

print_r($set);

Kết quả này:

Array
(
    [0] => apple
    [1] => banana
    [2] => coconut
)

Mọi thay đổi từ bản gốc không thể là thông báo, thực tế không có thay đổi nào so với bản gốc, mặc dù bạn đã gán rõ ràng một giá trị cho mục $. Điều này là do bạn đang hoạt động trên $ item khi nó xuất hiện trong bản sao của $ set đang hoạt động. Bạn có thể ghi đè lên điều này bằng cách lấy $ item bằng cách tham chiếu, như vậy:

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

Kết quả này:

Array
(
    [0] => elppa
    [1] => ananab
    [2] => tunococ
)

Vì vậy, điều hiển nhiên và có thể quan sát được, khi $ item được vận hành dựa trên tham chiếu, các thay đổi được thực hiện cho $ item được thực hiện cho các thành viên của tập $ gốc. Sử dụng $ item theo tham chiếu cũng ngăn PHP tạo bản sao mảng. Để kiểm tra điều này, trước tiên chúng tôi sẽ hiển thị một tập lệnh nhanh thể hiện bản sao:

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

Kết quả này:

Array
(
    [0] => apple
    [1] => banana
    [2] => coconut
    [3] => Apple
    [4] => Banana
    [5] => Coconut
)

Như được hiển thị trong ví dụ, PHP đã sao chép $ set và sử dụng nó để lặp lại, nhưng khi $ set được sử dụng bên trong vòng lặp, PHP đã thêm các biến vào mảng ban đầu, chứ không phải mảng được sao chép. Về cơ bản, PHP chỉ sử dụng mảng được sao chép để thực hiện vòng lặp và gán $ item. Do đó, vòng lặp ở trên chỉ thực hiện 3 lần và mỗi lần nó nối thêm một giá trị khác vào cuối tập $ gốc, để lại tập $ gốc ban đầu có 6 phần tử, nhưng không bao giờ đi vào vòng lặp vô hạn.

Tuy nhiên, điều gì sẽ xảy ra nếu chúng ta đã sử dụng $ item theo tham chiếu, như tôi đã đề cập trước đây? Một ký tự đơn được thêm vào bài kiểm tra trên:

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

Kết quả trong một vòng lặp vô hạn. Lưu ý đây thực sự là một vòng lặp vô hạn, bạn sẽ phải tự giết tập lệnh hoặc đợi HĐH hết bộ nhớ. Tôi đã thêm dòng sau vào tập lệnh của mình để PHP sẽ hết bộ nhớ rất nhanh, tôi khuyên bạn nên làm tương tự nếu bạn sẽ chạy các bài kiểm tra vòng lặp vô hạn này:

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

Vì vậy, trong ví dụ trước với vòng lặp vô hạn này, chúng ta thấy lý do tại sao PHP được viết để tạo một bản sao của mảng để lặp lại. Khi một bản sao được tạo và chỉ được sử dụng bởi cấu trúc của chính vòng lặp, mảng sẽ giữ trạng thái tĩnh trong suốt quá trình thực hiện vòng lặp, do đó bạn sẽ không bao giờ gặp phải sự cố.


7

Vòng lặp foreach PHP có thể được sử dụng với Indexed arrays, Associative arraysObject public variables.

Trong vòng lặp foreach, điều đầu tiên php làm là nó tạo ra một bản sao của mảng sẽ được lặp lại. PHP sau đó lặp lại qua copymảng mới này chứ không phải là bản gốc. Điều này được thể hiện trong ví dụ dưới đây:

<?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).

Bên cạnh đó, php cũng cho phép sử dụng iterated values as a reference to the original array value. Điều này được thể hiện dưới đây:

<?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

Lưu ý: Nó không cho phép original array indexesđược sử dụng như references.

Nguồn: http://dwellupper.io/post/47/under Hiểu-php-forum-rop-with-Nexus


1
Object public variableslà sai hoặc tốt nhất là sai lệch. Bạn không thể sử dụng một đối tượng trong một mảng mà không có giao diện chính xác (ví dụ: Traversible) và khi bạn foreach((array)$obj ...thực sự làm việc với một mảng đơn giản, không phải là một đối tượng nữa.
Christian
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.