PHP - each 與 foreach 操作 pointer 的版本地雷

零、前言

這陣子在進行手邊 PHP 專案的版本升級,從 PHP 5.x 升級到 PHP 7.x ,由於專案裡大量的使用了 while(list($key, $value) = each($items)) {} 這樣的語法,而在 PHP 7.2 之後,官方直接標注不建議使用,因此升級的過程中,順帶轉換為 foreach 結構,在過程中踩到一些 PHP 5.x 升級到 PHP 7.x 之後的特性雷,因此寫下這篇作為紀錄。

PS. 現行 PHP 7.2 當未關閉警告的情況下,會提醒 PHP: each - Manual 不建議使用

1
2
Deprecated: The each() function is deprecated. 
This message will be suppressed on further calls in /xxxx.php on line xx

壹、測試資料

首先,測試資料的部分,全程使用底下的陣列內容。

1
$items = [1, 2, 3, 4, 5, 6];

在 Migrating from PHP 5.6.x to PHP 7.0.x 的文件 PHP: Backward incompatible changes - Manual 中,提到 foreach no longer changes the internal array pointer 在 PHP 7 之後 foreach 不會再改變 array 的內部指標。如下驗證測試碼,current 可以取得目前 items 這個 array 的指標位置,當超出範圍則顯示為 boolean 的 false 否則為 integer:

1
2
3
4
5
6
7
8
$items = [1, 2, 3, 4, 5, 6];

echo "PHP version: " . phpversion() . PHP_EOL;
echo "----" . PHP_EOL;

foreach ($items as &$val) {
var_dump(current($items));
}
  • PHP 7
1
2
3
4
5
6
7
8
PHP version: 7.1.8
----
int(1)
int(1)
int(1)
int(1)
int(1)
int(1)
  • PHP 5
1
2
3
4
5
6
7
8
PHP version: 5.6.30
----
int(2)
int(3)
int(4)
int(5)
int(6)
bool(false)

貳、單獨獨立使用

一、關於 foreach 實作

平常我們會如此的使用 foreach 做迴圈內容的操作,這邊為單純整個測試及紀錄,就不變動 array 的內容。

  • 程式碼:
1
2
3
4
5
6
7
8
9
$items = [1, 2, 3, 4, 5, 6];

echo "PHP version: " . phpversion() . PHP_EOL;
echo "----" . PHP_EOL;

echo "foreach ";
foreach ($items as $foreach_key => $foreach_value) {
echo $foreach_value . " ";
}

輸出:

1
2
3
PHP version: 5.6.30
----
foreach 1 2 3 4 5 6

二、關於 while each

  • 程式碼:
1
2
3
4
5
6
7
8
9
$items = [1, 2, 3, 4, 5, 6];

echo "PHP version: " . phpversion() . PHP_EOL;
echo "----" . PHP_EOL;

echo "while each ";
while(list($list_key, $list_value) = each($items)) {
echo $list_value . " ";
}

輸出:

1
2
3
PHP version: 5.6.30
----
while each 1 2 3 4 5 6

目前為止無論是 each 或是 foreach 的操作,不論是 PHP 5.6 或是 PHP 7.1 兩者的輸出內容是一致的。

參、當混合使用時

先 foreach 後使用 each 迴圈:

  • 程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$items = [1, 2, 3, 4, 5, 6];

echo "PHP version: " . phpversion() . PHP_EOL;

echo "----" . PHP_EOL;

echo "foreach ";
foreach ($items as $foreach_key => $foreach_value) {
echo $foreach_value . " ";
}
echo PHP_EOL;

echo "----" . PHP_EOL;

echo "while each ";
while(list($list_key, $list_value) = each($items)) {
echo $list_value . " ";
}
echo PHP_EOL;
  • PHP 7
1
2
3
4
5
PHP version: 7.1.8
----
foreach 1 2 3 4 5 6
----
while each 1 2 3 4 5 6
  • PHP 5
1
2
3
4
5
PHP version: 5.6.30
----
foreach 1 2 3 4 5 6
----
while each

從上面的結果看到,PHP 5.6 的執行結果,在進行完 foreach 迴圈之後,再做一次同一陣列的 while each 迴圈,已經不會再印出任何內容。 each 迴圈在操作時,可以透過 PHP: current - Manual 來取得目前陣列的 pointer 指到 array 的哪一個 element,稍作修改,使上面的程式印出陣列目前所在的 pointer 位置。

先 foreach 後使用 each 迴圈 (增加顯示 pointer 位置):

  • 程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$items = [1, 2, 3, 4, 5, 6];

echo "PHP version: " . phpversion() . PHP_EOL;

echo "----" . PHP_EOL;

echo "foreach ";
foreach ($items as $foreach_key => $foreach_value) {
echo $foreach_value . " ";
}
echo PHP_EOL;
echo "current pos = " . current($items) . PHP_EOL;

echo "----" . PHP_EOL;

echo "while each ";
while(list($list_key, $list_value) = each($items)) {
echo $list_value . " ";
}
echo PHP_EOL;
echo "current pos = " . current($items) . PHP_EOL;
  • PHP 7
1
2
3
4
5
6
7
PHP version: 7.1.8
----
foreach 1 2 3 4 5 6
current pos = 1
----
while each 1 2 3 4 5 6
current pos =
  • PHP 5
1
2
3
4
5
6
7
PHP version: 5.6.30
----
foreach 1 2 3 4 5 6
current pos =
----
while each
current pos =

發現,在 PHP 7 版本中,pointer 並沒有轉變,而 PHP 5.x 版本則會隨著 foreach 操作,連帶變動到 each 使用的 pointer 指標。

先 foreach 後使用 each 迴圈 (foreach 到一半直接 break 掉):

  • 程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$items = [1, 2, 3, 4, 5, 6];

echo "PHP version: " . phpversion() . PHP_EOL;

echo "----" . PHP_EOL;

echo "foreach ";
foreach ($items as $foreach_key => $foreach_value) {
echo $foreach_value . " ";
if ($foreach_value > 2) {
break;
}
}
echo PHP_EOL;
echo "current pos = " . current($items) . PHP_EOL;

echo "----" . PHP_EOL;

echo "while each ";
while(list($list_key, $list_value) = each($items)) {
echo $list_value . " ";
}
echo PHP_EOL;
echo "current pos = " . current($items) . PHP_EOL;
  • PHP 7
1
2
3
4
5
6
7
PHP version: 7.1.8
----
foreach 1 2 3
current pos = 1
----
while each 1 2 3 4 5 6
current pos =
  • PHP 5
1
2
3
4
5
6
7
PHP version: 5.6.30
----
foreach 1 2 3
current pos = 4
----
while each 4 5 6
current pos =

特性如同上一個小實驗,在 PHP 7 版本中,pointer 並沒有轉變,而 PHP 5 版本則會隨著 foreach 操作,連帶變動到 each 使用的 pointer 指標。

多次 each 搭配 foreach 操作時

先一次 each 操作,而後 foreach,接著使用 while each 迴圈,程式碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$items = [1, 2, 3, 4, 5, 6];

echo "PHP version: " . phpversion() . PHP_EOL;

echo "----" . PHP_EOL;

echo "each ";
list($each_key, $each_value) = each($items);
echo $each_value;
echo PHP_EOL . "current pos = " . current($items) . PHP_EOL;

echo "----" . PHP_EOL;

echo "foreach ";
foreach ($items as $foreach_key => $foreach_value) {
echo $foreach_value . " ";
if ($foreach_value > 2) {
break;
}
}
echo PHP_EOL;
echo "current pos = " . current($items) . PHP_EOL;

echo "----" . PHP_EOL;

echo "while each ";
while(list($list_key, $list_value) = each($items)) {
echo $list_value . " ";
}
echo PHP_EOL;
echo "current pos = " . current($items) . PHP_EOL;
  • PHP 7
1
2
3
4
5
6
7
8
9
10
PHP version: 7.1.8
----
each 1
current pos = 2
----
foreach 1 2 3
current pos = 2
----
while each 2 3 4 5 6
current pos =
  • PHP 5
1
2
3
4
5
6
7
8
9
10
PHP version: 5.6.30
----
each 1
current pos = 2
----
foreach 1 2 3
current pos = 4
----
while each 4 5 6
current pos =

這邊是第二個需要特別注意的地方,在 PHP 7 版本中,因為不再操作 array 內部的 pointer,因此就算在 foreach 操作前做了一次 each 操作,也不影響 foreach 的初始值,且來到 while each 迴圈中時,會從陣列的第二個位置開始。但…在 PHP 5 中,foreach 會重置 pointer,而後在結束 foreach 迴圈離開後, while each 直接接 foreach 結束之後的 pointer 繼續進行。

總結

  1. PHP 5 年代,會因為 foreach 迴圈而重置 array 的 pointer 但 PHP 7 不會。
  2. 升級到 PHP 7 之前,如果之前有做多次 foreach 或 each 的操作,則要謹記 pointer 操作的問題,必要時,可以使用 reset() 進行 array 的 pointer 重置。
  3. 升級 PHP 至 PHP 7 版本,必須隨時注意,是否是有意義的在操作 array pointer,否則會因為 php array 操作特性改變,變得很難找出錯誤的原因。
  4. 當有使用 foreach by-reference 操作到 array 的內容時,那下一個地雷將會是這個:foreach by-reference has improved iteration behaviour ,在 PHP 5 時,會操作到的指標僅陣列的原始範圍,但在 PHP 7 開始,則會動態的調整需要走訪的陣列範圍,如官方的範例:
1
2
3
4
5
$array = [0];
foreach ($array as &$val) {
var_dump($val);
$array[1] = 1;
}
  • PHP 7
1
2
int(0)
int(1)
  • PHP 5
1
int(0)
作者: 墨嗓 (陳佑竹)
連結: https://mouson.im/Notes/PHP/php-each-and-foreach-cursor/

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器