foreach 是 PHP 遍歷陣列最常用的結構,但涉及引用(&$v)時隱藏著許多反直覺的行為。本文透過四個經典案例,結合 opcode 層面分析,徹底釐清 foreach 的運作機制。
$arr = array(1,2,3);
foreach ($arr as $k => &$v) { $v = $v * 2; }
// $arr 變為 [2,4,6]
foreach ($arr as $k => $v) { echo "$k => $v "; }
// 輸出:0 => 2 1 => 4 2 => 4 (預期 2 => 6)
foreach 結束後,$v 仍保留對 $arr[2] 的引用。foreach 隱含賦值 $v = currentVal(),等同 $arr[2] = $arr[0],導致最後一個元素被覆蓋。unset($v); // 釋放引用
foreach ($arr as $k => $v) { echo "$k => $v "; }
// 輸出:0 => 2 1 => 4 2 => 6
最佳實踐: 使用引用遍歷後,立即 unset($v)。
$arr = array('a','b','c');
foreach ($arr as $k => $v) {
echo key($arr), '=>', current($arr); // 輸出三次 1=>b
}
FE_RESET 將陣列內部指針指向第一個元素,並將指針位置存入 zend_execute_data->Ts。FE_FETCH 每次迭代前將內部指針移至下一個元素。key($arr)/current($arr) 時,PHP 執行 SEPARATE_ZVAL_TO_MAKE_IS_REF 複製陣列(Copy-on-Write),導致操作的是副本,其內部指針永遠停在第二個元素(b)。$arr = array('a','b','c');
foreach ($arr as $k => &$v) {
echo key($arr), '=>', current($arr); // 輸出 1=>b 2=>c =>
}
FE_RESET 會設定 is_ref=1,不觸發陣列複製。key()/current() 操作的是同一個陣列,內部指針隨 FE_FETCH 推進,故輸出正常推進。key()/current() 回傳 NULL/false,故無輸出。$arr = array(1,2,3);
$tmp = $arr;
foreach ($tmp as $k => &$v) { $v *= 2; }
var_dump($arr, $tmp); // $arr 不變,$tmp 變為 [2,4,6]
$tmp = $arr 只是增加陣列的 refcount,不複製資料。FE_RESET 發現陣列 refcount > 1 且需寫入(引用遍歷),觸發 SEPARATE_ZVAL_IF_NOT_REF 複製陣列。$tmp 指向新複製的陣列,$arr 保持原樣,互不影響。foreach ($a as &$v) 結束後必須 unset($v)。key()/current()/next() 等操作內部指針的函數,否則會觸發陣列複製導致邏輯錯誤。is_ref=1),值遍歷在需寫入時複製。掌握這些細節,能避免 90% 的 foreach 相關 Bug,寫出更健壯的 PHP 代碼。