Câu hỏi PHP 'foreach' thực sự hoạt động như thế nào?


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


Trong một thời gian dài tôi giả định rằng foreach đã làm việc với chính mảng đó. Sau đó, tôi tìm thấy nhiều tham chiếu đến thực tế là nó hoạt động với sao chép của mảng, và tôi đã giả định đây là kết thúc của câu chuyện. Nhưng gần đây tôi đã 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 không đúng 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 cho thấy rõ ràng rằng chúng ta không làm việc trực tiếp với mảng nguồn - nếu không vòng lặp sẽ tiếp tục mãi mãi, vì chúng ta liên tục đẩy các mục vào 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 ta sẽ thấy các giá trị được sửa đổi trong vòng lặp. Nhưng...

Nếu chúng ta nhìn vào thủ công, chúng tôi tìm thấy tuyên bố này:

Khi foreach đầ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 foreach dựa vào con trỏ mảng của mảng nguồn. Nhưng chúng tôi vừa chứng minh rằng chúng tôi không làm việc với mảng nguồn, đú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 có, thì trường hợp thử nghiệm 1 sẽ lặp lại mãi mãi.

Hướng dẫn sử dụng PHP cũng nêu rõ:

Như foreach dựa vào con trỏ mảng nội bộ thay đổi nó trong vòng lặp có thể dẫn đến hành vi bất ngờ.

Vâng, chúng ta hãy tìm hiểu những gì "hành vi bất ngờ" là (về mặt kỹ thuật, bất kỳ hành vi là bất ngờ kể từ khi tôi không còn biết những gì mong đợi).

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 "sao chép 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ể trích xuất một kết luận thích hợp chỉ đơn giản bằng cách xem mã nguồn PHP, tôi sẽ đánh giá cao nếu một người nào đó có thể dịch nó sang tiếng Anh cho tôi.

Theo tôi thì foreach làm việc với một sao chép 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ó chính xác và toàn bộ câu chuyện không?
  • Nếu không, nó thực sự đang làm gì?
  • Có tình huống nào khi sử dụng các hàm điều chỉnh con trỏ mảng (each(), reset() et al.) trong một foreach có thể ảnh hưởng đến kết quả của vòng lặp?

1644
2018-04-07 19:33


gốc


@DaveRandom Có một php-internals thẻ này có lẽ nên đi với, nhưng tôi sẽ để nó cho bạn để quyết định nếu có 5 thẻ khác để thay thế. - Michael Berkowski
trông giống như COW, không xóa xử lý - zb'
Lúc đầu, tôi nghĩ »gosh, một câu hỏi mới. Đọc tài liệu… hm, hành vi rõ ràng không xác định «. 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 đã đặt khá nhiều nỗ lực vào nó và viết tất cả các testcases. ps. là testcase 4 và 5 giống nhau không? - knittl
Chỉ cần suy nghĩ về lý do tại sao nó làm cho cảm giác rằng con trỏ mảng được xúc động: PHP cần phải thiết lập lại và di chuyển con trỏ mảng nội bộ 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 một tham chiếu đến giá trị hiện tại (foreach ($array as &$value)) - PHP cần phải biết vị trí hiện tại trong mảng ban đầu mặc dù nó thực sự lặp qua một bản sao. - Niko
@ Sean: IMHO, tài liệu PHP thực sự khá xấu khi 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ác câu trả lời:


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

Trong phần tiếp theo, tôi sẽ cố gắng giải thích chính xác cách 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à Traversable đối tượng, như đối với những foreach về cơ bản 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 một API nội bộ mà về cơ bản chỉ phản ánh Iterator giao diện ở cấp độ C.

Lặp lại các mảng và các đối tượng đơn giản phức tạp hơn nhiều. Trước hết, cần lưu ý rằng trong PHP "mảng" là từ điển được đặt hàng thực sự và chúng sẽ được chuyển 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 lại theo thứ tự tự nhiên của các khóa (danh sách các ngôn ngữ khác thường hoạt động như thế nào) hoặc không có thứ tự xác định nào cả (cá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ư một tên thuộc tính ánh xạ từ điển khác (thứ tự) với giá trị của chúng, cộng với một số xử lý 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 này không hiệu quả. Tuy nhiên nếu bạn bắt đầu lặp qua một đối tượng, đại 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. 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 về việc lặp lại đối tượng thuần túy nhiều ở đâ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 khi lặp lại. Có nhiều cách có thể xảy ra:

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

Vấn đề với việc cho phép sửa đổi trong khi lặp lại là trường hợp mà phần tử bạn hiện đang được xóa. Giả sử bạn sử dụng một con trỏ để theo dõi phần tử mảng nào bạn hiện đang ở. Nếu phần tử này bây giờ được giải phóng, bạn bị bỏ lại vớ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á câm và dẫn đến tất cả các loại vấn đề cạnh tranh lạ, trong khi cách tiếp cận liên quan đến PHP 7 càng dẫn đến hành vi có thể đoán trước và nhất quán hơn.

Như là một sơ bộ cuối cùng, cần lưu ý rằng PHP sử dụng tính toán tham chiếu và copy-on-write để 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ố lượng tham chiếu của nó (số lần truy cập). Chỉ một 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à "trùng lặp") sẽ được thực hiện. Xem Bạn đang bị nói dối để có một giới thiệu sâu rộng hơn về chủ đề này.

PHP 5

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

Mảng trong PHP 5 có một "con trỏ mảng nội bộ" chuyên dụng (IAP), hỗ trợ sửa đổi đúng cách: 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ỏ tới phần tử này hay không. Nếu có, thay vào đó nó được nâng cấp thành phần tử tiếp theo.

Trong khi foreach sử dụng IAP, có một biến chứng thêm: Chỉ có một IAP, nhưng một mảng có thể là một phần của nhiều vòng lặp 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) {
        // ...
    }
}

Để hỗ trợ hai vòng đồng thời chỉ với một con trỏ mảng bên trong, foreach thực hiện các lược đồ sau: Trước khi vòng lặp được thực hiện, foreach sẽ sao lưu một con trỏ tới phần tử hiện tại và hàm băm của nó vào HashPointer. Sau khi vòng lặp chạy, IAP sẽ được thiết lập trở lại thành phần 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ẽ chỉ sử dụng bất cứ nơi nào IAP hiện đang ở. Đề án này chủ yếu-kinda-sortof hoạt động, nhưng có rất nhiều hành vi kỳ lạ bạn có thể nhận được ra khỏi nó, một số trong đó tôi sẽ chứng minh dưới đây.

Sao chép mảng

IAP là một tính năng hiển thị của một mảng (được hiển thị thông qua current gia đình của các chức năng), vì những thay đổi như vậy đối với số IAP là các sửa đổi theo ngữ nghĩa sao chép trên ghi. Điều này không may có nghĩa là foreach trong nhiều trường hợp buộc phải nhân đôi 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à tham chiếu (is_ref = 0). Nếu đó là một tham chiếu, thì những thay đổi đối với nó là được cho là để nhân giống, vì vậy nó không nên được nhân đôi.
  2. Mảng có refcount> 1. Nếu refcount là 1, thì mảng không được chia sẻ và chúng tôi tự do sửa đổi nó trực tiếp.

Nếu mảng không được nhân đôi (is_ref = 0, refcount = 1), thì chỉ số refcount của nó sẽ được tăng lên (*). Ngoài ra, nếu foreach bằng tham chiếu được sử dụng, thì mảng (có khả năng nhân đôi) sẽ được chuyển thành tham chiếu.

Xem xét mã này làm ví dụ nơi xảy ra trùng lặp:

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

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

Đây, $arr sẽ được nhân đôi để ngăn thay đổi IAP $arr từ rò rỉ đến $outerArr. Trong điều kiện của các điều kiện trên, mảng không phải là một tham chiếu (is_ref = 0) và được sử dụng ở hai nơi (refcount = 2). Yêu cầu này là không may và một tạo phẩm của việc triển khai tối ưu (không có mối quan tâm về sửa đổi trong khi lặp lại ở đây, vì vậy chúng tôi không thực sự cần phải sử dụng IAP ở nơi đầu tiên).

(*) Tăng số lượng refcount ở đây nghe có vẻ vô hại, nhưng vi phạm ngữ nghĩa copy-on-write (COW): Điều này có nghĩa là chúng ta sẽ sửa đổi IAP của một mảng refcount = 2, trong khi COW chỉ ra rằng các 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 có thể nhìn thấy người dùng (trong khi COW thường trong suốt), bởi vì thay đổi IAP trên mảng được lặp lại sẽ được quan sát - nhưng chỉ cho đến khi sửa đổ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 lặp lại, b) để không tăng số đếm và do đó cho phép mảng lặp được tùy ý sửa đổi trong vòng lặp, hoặc c) không sử dụng IAP ở tất cả ( giải pháp PHP 7).

Lệnh thăng tiến vị trí

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ã bên dưới. Cách "vòng lặp" bình 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 những việc hơi khác nhau:

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 cơ thể vòng lặp chạy. Điều này có nghĩa là khi thân vòng lặp đang hoạt động trên phần tử $i, IAP đã ở nguyên tố $i+1. Đây là lý do tại sao các mẫu mã hiển thị sửa đổi trong khi lặp lại sẽ luôn luôn bỏ đặt kế tiếp phần tử, thay vì phần tử hiện tại.

Ví dụ: Các trường hợp kiểm tra 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 chủ yếu là hoàn toàn về các idiosyncracies của việc thực hiện foreach và chúng tôi có thể chuyển sang thảo luận 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 các trường hợp thử nghiệm 1 và 2 $array bắt đầu với refcount = 1, vì vậy nó sẽ không bị trùng lặp bởi foreach: Chỉ có refcount được tăng lên. Khi cơ thể vòng lặp sau đó sửa đổi mảng (trong đó có refcount = 2 tại điểm đó), sự trùng lặp sẽ xảy ra tại thời điểm đó. Foreach sẽ tiếp tục làm việc trên một bản sao chưa được sửa đổi của $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 đó foreach sẽ sửa đổi IAP của $array biến. Vào cuối của iteration IAP là NULL (có nghĩa là iteration done), each biể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 each và reset là các hàm tham chiếu. Các $array có một refcount=2 khi nó được truyền cho họ, vì vậy nó phải được nhân đôi. Như vậy foreach sẽ làm việc trên một mảng riêng biệt một lần nữa.

Ví dụ: Ảnh hưởng của current trong foreach

Một cách hay để hiển thị các hành vi trùng lặp khác nhau là quan sát hành vi của current() chức năng bên trong vòng lặp foreach. 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à hàm by-ref (thực sự là: ưa thích-ref), mặc dù nó không sửa đổi mảng. Nó phải là để chơi tốt đẹp với tất cả các chức năng khác như next tất cả đều là do ref. By-tham chiếu đi qua ngụ ý rằng mảng phải được tách ra và do đó $array và mảng foreach sẽ khác. Lý do bạn nhận được 2 thay vì 1 cũng được đề cập ở trên: foreach tiến lên con trỏ mảng trước chạy mã người dùng, không phải sau. Vì vậy, mặc dù mã là ở phần tử đầu tiên, foreach đã nâng cao con trỏ đến phần tử thứ hai.

Bây giờ, hãy thử 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ó is_ref = 1 case, do đó 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 sang by-ref current() chức năng. Như vậy current() và foreach làm việc trên cùng một mảng. Bạn vẫn thấy hành vi off-by-one mặc dù, do cách foreachtiến lên con trỏ.

Bạn nhận được cùng một hành vi khi thực hiện việc lặp lại bởi-ref:

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

Ở đây phần quan trọng là foreach sẽ làm cho $array một is_ref = 1 khi nó được lặp lại theo tham chiếu, vì vậy về cơ bản bạn có cùng một tình huống như trên.

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

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

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

Ví dụ: Sửa đổi trong khi lặp lại

Cố gắng tính đến các sửa đổi trong quá trình lặp là nơi mà tất cả các vấn đề của chúng tôi đều có nguồn gốc, vì vậy nó sẽ 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 đó lặp lại bởi-ref được sử dụng để đảm bảo 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 mong đợi ở đây là (1, 2) bị thiếu từ đầu ra, bởi vì phần tử 1 đã xóa bỏ. Có lẽ điều 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 đây là hack vòng lặp lồng nhau được mô tả ở trên: Trước khi vòng lặp chạy, vị trí IAP hiện tại và băm được sao lưu vào HashPointer. Sau khi vòng lặp cơ thể nó sẽ được phục hồi, nhưng chỉ khi phần tử vẫn cò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 đã được loại bỏ, do đó, nó sẽ sử dụng IAP, mà đã được đánh dấu là xong bởi vòng lặp bên trong!

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

$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 foreach hiện tại sau vòng lặp. Ép buộc reset() để tạo hiệu ứng trên vòng lặp, bạn phải loại bỏ bổ sung 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. Niềm vui thực sự bắt đầu nếu bạn nhớ rằng HashPointer phục hồi sử dụng một con trỏ đến phần tử và giá trị băm của nó để xác định xem nó có còn tồn tại hay không. Nhưng: Hashes có va chạm, và con trỏ có thể được tái sử dụng! Điều này có nghĩa rằng, với một sự lựa chọn cẩn thận của các phím mảng, chúng ta có thể làm cho foreach tin rằng một phần tử đã bị xóa vẫn cò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, 4 theo các quy tắc trước đó. Điều gì xảy ra là 'FYFY' có cùng giá trị băm như phần tử đã xóa 'EzFY'và trình 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 vào phần tử mới được chèn vào, do đó cắt ngắn vòng lặp.

Thay thế thực thể được lặp lại trong vòng lặp

Một trường hợp kỳ lạ cuối cùng mà tôi muốn đề cập đến, đó là PHP cho phép bạn thay thế thực thể được lặp lại trong vòng lặp. Vì vậy, bạn có thể bắt đầu lặp trên một mảng và sau đó thay thế nó bằng một mảng khác nử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ể kia ngay từ đầu khi sự thay thế đã xảy ra.

PHP 7

Trình biến đổi Hashtable

Nếu bạn vẫn còn nhớ, vấn đề chính với lặp mảng là làm thế nào để xử lý việc loại bỏ các phần tử giữa lần lặp lại. PHP 5 đã sử dụng một con trỏ mảng nội bộ duy nhất (IAP) cho mục đích này, mà phần nào là 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() vv trên hết.

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 tùy ý của các trình lặp vòng lặp có thể bắt đầu, an toàn bên ngoài. Các vòng lặp này phải được đăng ký trong mảng, từ điểm trên chúng có cùng ngữ nghĩa giống như IAP: Nếu một phần tử mảng bị loại bỏ, tất cả các trình vòng lặp có thể bắt đầu trỏ tới phần tử đó sẽ được nâng cấp thành phần tử tiếp theo.

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

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 sự 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ỉ làm tăng số đếm (thay vì sao chép mảng) trong mọi trường hợp. Nếu mảng được sửa đổi trong vòng lặp foreach, tại thời điểm đó một sự trùng lặp sẽ xảy ra (theo copy-on-write) và foreach sẽ tiếp tục làm việc 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ó hiệu ứng nào khác ngoài hiệu suất tốt hơn. Tuy nhiên có một lần mà nó dẫn đến hành vi khác nhau, cụ thể là trường hợp mảng được 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 */

Việc lặp lại giá trị trước đây của mảng tham chiếu là các trường hợp đặc biệt. Trong trường hợp này, không có sự trùng lặp nào 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 biến mất: Một lần lặp lại giá trị của một mảng sẽ luôn luôn tiếp tục làm việc trên các yếu tố ban đầu, bỏ qua bất kỳ sửa đổi nào trong vòng lặp.

Điều này, tất nhiên, không áp dụng cho lặp lại tham chiếu. Nếu bạn lặp lại bằng tham chiếu, tất cả các sửa đổi sẽ được phản ánh bởi vòng lặp. Điều thú vị là, điều này cũng đúng với việc lặp lại 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 theo cách xử lý (nghĩa là chúng hoạt động giống như tham chiếu ngay cả trong các ngữ cảnh theo 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 kiểm tra của bạn:

  • Các trường hợp thử nghiệm 1 và 2 giữ lại cùng một đầu ra: Lặp lại mảng giá trị luôn luôn tiếp tục làm việc trên các phần tử gốc. (Trong trường hợp này, ngay cả việc đếm ngược và hành vi trùng lặp cũng giống hệt nhau giữa PHP 5 và PHP 7).

  • Trường hợp thử nghiệm 3 thay đổi: Foreach không còn sử dụng IAP nữa, vì vậy 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 vẫn giữ nguyên: each() và reset() sẽ lặp lại mảng trước khi thay đổi IAP, trong khi foreach vẫn sử dụng mảng ban đầu. (Không phải là sự thay đổi IAP sẽ có vấn đề, ngay cả khi mảng được chia sẻ.)

Tập hợp các ví dụ thứ hai liên quan đến hành vi của current() dưới các cấu hình tham chiếu / đếm ngược khác nhau. Điều này không còn ý nghĩa, như current() hoàn toàn không bị ảnh hưởng bởi vòng lặp, do đó 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 khi lặp lại. Tôi hy vọng bạn sẽ tìm thấy những hành vi mới lành mạnh hơn. 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 sau lần lặp đầu tiên nữa. Lý do là cả hai vòng lặp bây giờ có các trình lặp có thể bắt đầu 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 lặp thông qua một IAP được chia sẻ.

Một trường hợp cạnh kỳ lạ khác đã được khắc phục ngay bây giờ, là hiệu ứng kỳ lạ mà bạn nhận được khi bạn xóa và thêm các phần tử xảy ra có cùng một 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 HashPointer đã nhảy ngay đến phần tử mới, bởi vì nó "trông" giống như phần tử remove (do va chạm băm và con trỏ). Vì chúng ta không còn dựa vào phần tử băm cho bất cứ điều gì, điều này không còn là vấn đề nữa.


1384
2018-02-13 13:21



@Baba Nó có. Chuyển nó đến một hàm cũng giống như làm $foo = $array trước vòng lặp;) - NikiC
Đối với những người không biết zval là gì, hãy tham khảo Sara Goleman's blog.golemon.com/2007/01/youre-being-lied-to.html - shu zOMG chen
@unbeli Tôi đang sử dụng thuật ngữ được sử dụng nội bộ bởi PHP. Các Buckets là một phần của danh sách được liên kết kép 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 kép cho đơn đặt hàng;) - NikiC
@ NikiC: bạn có thực sự cần phải đi học đại học không? Bạn chỉ mới 19 tuổi nhưng bạn có nhiều kinh nghiệm hơn - dynamic
Great anwser. Tôi nghĩ bạn có nghĩa là iterate($outerArr); và không iterate($arr); một vài nơi. - niahoo


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 nội bộ. Điều này rất quan trọng khi nói đến PHP mảng 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. 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, nhân bản thực tế sẽ không được thực hiện trừ khi nó là 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 foreach tạo một loại trình lặp nào có tham chiếu đến mảng. Tham chiếu này hoạt động chính xác như biến $b trong ví dụ của tôi. Tuy nhiên, trình vòng lặp cùng với tham chiếu trực tiếp chỉ trong vòng lặp và sau đó, chúng đều bị loại bỏ. Bây giờ bạn có thể thấy rằng, trong mọi trường hợp nhưng 3, mảng được sửa đổi trong vòng lặp, trong khi tham chiếu bổ sung này vẫn 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 copy-on-write này: PHP Ternary Operator: Nhanh hay không?


97
2018-04-07 20:43



dường như quyền của bạn, tôi đã thực hiện một số ví dụ chứng minh rằng: codepad.org/OCjtvu8r một sự 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 các phím thay đổi. - 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 của trường hợp thử nghiệm đầu tiên, nơi chúng tôi thấy 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 lần lặp đầu tiên. Điều này cũng có vẻ chứng minh rằng foreach di chuyển con trỏ mảng trên trước khi thực hiện khối mã của vòng lặp, mà tôi đã không mong đợi - tôi đã có thể nghĩ rằng nó sẽ làm điều này ở cuối. Rất cám ơn, điều này làm tôi thấy rõ ràng. - DaveRandom


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

a) foreach hoạt động trên bản sao có triển vọng của mảng ban đầu.     Điều đó có nghĩa là foreach () sẽ có SHARED lưu trữ dữ liệu cho đến khi hoặc trừ khi prospected copy Là     chưa tạo foreach Ghi chú / Nhận xét của người dùng.

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

c) mảng ban đầu và foreach () iterator sẽ có DISTINCT SENTINEL VARIABLES, đó là, một cho mảng ban đầu và một cho foreach; xem mã thử nghiệm bên dưới. SPL , IteratorsArray Iterator.

Câu hỏi tràn ngăn xếp 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 cho thấy rằng mỗi () và đặt lại () KHÔNG ảnh hưởng đến SENTINEL biến (for example, the current index variable) của trình lặp 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/>";

Đầ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

34
2018-04-07 21:03



Câu trả lời của bạn không hoàn toàn chính xác. foreach hoạ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 tế trừ khi nó là 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 ra 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 tốn rất nhiều. Hãy thử đếm thời gian cần để lặp lại một mảng với 100000 phần tử bằng cách sử dụng for hoặc là foreach. Bạn sẽ không thấy bất kỳ sự khác biệt đáng kể giữa hai người trong số họ, bởi vì một bản sao thực tế không diễn ra. - linepogl
Sau đó, tôi sẽ giả định rằng có SHARED data storage dành riêng cho đến hoặc trừ khi copy-on-write , nhưng (từ đoạn mã của tôi) hiển nhiên rằng sẽ luôn có HAI bộ SENTINEL variables một cho original array và khác cho foreach. Cảm ơn điều đó có ý nghĩa - sakhunzai
"có triển vọng"? Bạn có nghĩa là "bảo vệ"? - Peter Mortensen


LƯU Ý CHO PHP 7

Để cập nhật câu trả lời này vì nó đã đạt được mức độ phổ biến: Câu trả lời này không còn áp dụng như PHP 7. Như đã giải thích trong phần "Các 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 đó, bất kỳ thay đổi nào trên mảng đó cũng không được phản ánh trên vòng lặp foreach.

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

Biểu mẫu đầu tiên lặp qua mảng được array_expression đưa ra. Trên mỗi   lặp lại, giá trị của phần tử hiện tại được gán cho giá trị $ và   con trỏ mảng nội bộ được nâng cao bởi một con trỏ (tiếp theo là   lặp lại, 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, phần tử tiếp theo không tồn tại, vì vậy sau khi bạn thêm phần tử mới foreach kết thúc vì nó đã "quyết định" rằng 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 phải là 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ả các hệ quả của Trên mỗi lần lặp một phần của giải thích trong tài liệu, điều này có thể có nghĩa là foreach làm tất cả logic trước khi nó gọi mã trong {}.

Kiểm tra trường hợp

Nếu bạn chạy điều 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 kết quả này:

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

Điều đó có nghĩa là nó chấp nhận sự sửa đổi và trải qua nó bởi vì nó đã được sửa đổi "đúng lúc". 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 kể từ khi chúng tôi sửa đổi nó khi 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 ta đã thêm phần tử mới, chúng ta đã thêm nó "quá muộn" và nó không được lặp lại.

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


22
2018-04-15 08:46



Bạn đã đọc phần còn lại của câu trả lời chưa? Nó làm cho cảm giác hoàn hảo rằng foreach quyết định nếu nó sẽ lặp lại một thời gian khác trước nó thậm chí còn chạy mã trong đó. - Damir Kasipovic
Không, mảng được sửa đổi, nhưng "quá muộn" vì foreach đã "nghĩ" rằng nó ở 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 phải là phần tử cuối cùng khi bắt đầu lặp lại và đánh giá lại lần nữa khi bắt đầu lặp lại tiếp theo. Tôi đang cố gắng chuẩn bị một trường hợp thử nghiệm. - Damir Kasipovic
@AlmaDo Nhìn vào 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 nó 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 lại cuối cùng, foreach sẽ không nhận thấy nó. - bwoebi
@DKasipovic no. Không có hoàn chỉnh và rõ ràng lời giải thích ở đó (ít nhất là bây giờ - có thể tôi sai) - Alma Do
Trên thực tế có vẻ như @AlmaDo có một lỗ hổng trong việc hiểu logic của riêng mình ... Câu trả lời của bạn là tốt. - bwoebi


Theo tài liệu được cung cấp bởi hướng dẫn sử dụng 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à nội bộ
  mảng con trỏ đượ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);
}

$array chỉ 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ỳ yếu 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);
}

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


8
2018-04-15 09:32





Câu hỏi hay, bởi vì nhiều nhà phát triển, thậm chí là những người có kinh nghiệm, bị lẫn lộn bởi cách PHP xử lý các mảng trong vòng lặp foreach. Trong vòng lặp foreach 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 sẽ bị hủy ngay lập tức sau khi kết thúc vòng lặp. Đ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ả đầu ra này:

apple
banana
coconut

Vì vậy, bản sao được tạo nhưng nhà phát triển không nhận thấy, vì mảng ban đầu không được tham chiếu trong vòng lặp hoặc sau khi kết thúc vòng lặp. 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ả đầu ra này:

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

Bất kỳ thay đổi nào từ bản gốc không thể là thông báo, thực sự không có thay đổi nào so với bản gốc, mặc dù bạn đã chỉ định rõ ràng một giá trị cho $ item. Điều này là do bạn đang hoạt động trên mục $ vì nó xuất hiện trong bản sao của $ set đang được làm việc. 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ư sau:

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

Kết quả đầu ra này:

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

Vì vậy, nó là hiển nhiên và quan sát được, khi $ item được vận hành theo tham chiếu, các thay đổi được thực hiện cho $ item được tạo cho các thành viên của tập $ gốc. Sử dụng $ item bằng cách tham chiếu cũng ngăn không cho 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ả đầu ra 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, 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 giá trị $ item. Bởi vì điều này, 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 với 6 phần tử, nhưng không bao giờ nhập một vòng lặp vô hạn.

Tuy nhiên, nếu chúng ta đã sử dụng $ item bằng cách tham chiếu, như tôi đã đề cập trước đây thì sao? 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ự mình giết chính kịch bản hoặc đợi hệ điều hành của bạn 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ớ nhanh chóng, tôi đề nghị bạn làm như vậy nếu bạn định chạy các 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, 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 bản sao được tạo ra và chỉ được sử dụng bởi cấu trúc của vòng lặp xây dựng chính nó, mảng vẫn 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 vấn đề.


5
2018-04-21 08:44





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

Trong vòng lặp foreach, điều đầu tiên mà php thực hiện là nó tạo ra một bản sao của mảng được lặp lại. PHP sau đó lặp lại thông tin mới này copy của mảng chứ không phải 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 cho phép sử dụng iterated values as a reference to the original array value cũng. Đ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

Chú thích: Nó không cho phép original array indexes được sử dụng như references.

Nguồn: http://dwellupper.io/post/47/understanding-php-foreach-loop-with-examples


4
2017-11-13 14:08



Object public variables sai hoặc gây hiểu lầm tốt nhất. 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 làm foreach((array)$obj ... bạn đang trong thực tế 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