深刻理解PHP之foreach

语言基础

foreach 语法结构提供了遍历数组的简单方式。php

php5以前, foreach仅能用于数组 php5+, 利用foreach能够遍历对象git

foreach仅可以应用于数据和对象,若是尝试应用于其余数据类型的变量,或者未初始化的变量将发出错误信息。express

有两种语法:数组

/* 遍历给定的 array_expression 数据。每次循环中, 当前单元的值被赋给$value而且数组内部的指针向前移一步(所以下次循环中将会获得下一个单元) */
foreach (array_expression as $value) {
    // statement
}

foreach (array_expression as $value) :
    // statement
endforeach;
复制代码
/* 同上,只除了当前单元格的键名也会在每次循环中被赋给变量$key */
foreach (array_expression as $key => $value) {
    // statement
}

foreach (array_expression as $key => $value) :
    // statement
endforeach;
复制代码

还可以自定义遍历对象!bash

foreach开始执行时, 数组内部的指针会自动指向第一个单元. 这意味着不须要在foreach循环以前调用reset() 因为foreach依赖内部数组指针, 在循环中修改其值将可能致使意外的行为php7

能够很容易经过在 $value 以前加上 & 来修改数组元素. 此方法将以引用 赋值, 而不是拷贝一个值.fetch

<?php

$arr = [1, 2, 3, 4];
foreach($arr as &$value) {
    $value = $value * 2;
}

// $arr is now [2, 4, 6, 8]
unset($value); // 最后取消掉引用

复制代码

$value的引用仅在被遍历的数组能够被引用时才可用(例如是个变量)。ui

如下代码没法运行:spa

<?php
/*
  此段代码能够运行
  运行结果:
    1-2
    2-4
    3-6
    4-8
*/
foreach (array(1, 2, 3, 4) as &$value) {
    echo $value, '-';
    $value = $value * 2;
    echo $value, PHP_EOL;
}
复制代码

Warning: 数组最后一个元素的 $value 引用在 foreach 循环以后仍会保留。建议使用 unset() 来将其销毁。.net

Note: foreach 不支持用 @ 来抑制错误信息的能力

foreach 虽然简单, 不过它可能出现一些意外行为, 特别是代码涉及到引用的时候。

问题研究

问题一: 以下代码运行结果为什么不是 2/4/6 ?

<?php
$arr = [1, 2, 3];

foreach ($arr as $k => &$v) {
    $v = $v * 2;
}

foreach ($arr as $k => $v) {
    echo $v, PHP_EOL;
}

/* 输出: 2 4 4 */
复制代码

咱们能够认为 foreach($arr as &$v) 结构隐含了以下操做, 分别将数组当前的 赋值给 $k$v. 具体展开形如:

<?php
foreach ($arr as $k => $v) {
    $k = currentKey();
    $v = currentVal();
    // 继续运行用户代码
} 
复制代码

根据上述理论, 如今咱们从新来分析下第一个foreach:

循环 备注 $arr值
循环 1-1 因为$v是一个引用, 所以 $v = &$arr[0], $v = $v * 2 至关于 $arr[0] * 2 [2, 2, 3]
循环 1-2 $v = &$arr[1] [2, 4, 3]
循环 1-3 $v = &$arr[2] [2, 4, 6]
循环 2-1 隐含操做 $v = $arr[0] 被触发, 因为此时 $v 还是 $arr[2] 的引用, 至关于 $arr[2] = $arr[0] [2, 4, 2]
循环 2-2 $v = $arr[1], 即$arr[2] = $arr[1] [2, 4, 4]
循环 2-3 $v = $arr[2], 即$arr[2] = $arr[2] [2, 4, 4]

如何解决此类问题呢? PHP手册上有一段提醒:

Warning: 数组最后一个元素的 $value 引用在 foreach 循环以后仍会保留。建议使用 unset() 来将其销毁。

<?php
$arr = [1, 2, 3];

foreach ($arr as $k => &$v) {
    $v = $v * 2;
}
unset($v);
foreach ($arr as $k => $v) {
    echo $v, PHP_EOL;
}

/* 输出: 2 4 6 */
复制代码

从这个问题能够看出, 引用极可能会伴随反作用。若是不但愿无心识的修改致使数据内容变动, 最好及时unset掉这些引用。

问题二: 以下代码运行结果为什么不是 0=>a 1=>b 2=>c

<?php
$arr = ['a', 'b', 'c'];

foreach ($arr as $k => $v) {
    echo key($arr), "=>", current($arr), PHP_EOL;
}

foreach ($arr as $k => &$v) {
    echo key($arr), "=>", current($arr), PHP_EOL;
}
/* #php5.6 1=>b 1=>b 1=>b 1=>b 2=>c => #php7 0=>a 0=>a 0=>a 0=>a 0=>a 0=>a */
复制代码

按照手册中的说法, key和current分别是获取数据中当前元素的键值。 那为什么 key($arr) 一直是0,current($arr) 一直是'a'呢?

先用vld查看编译后的 opcode:

➜  demo /usr/local/Cellar/php/7.2.7/bin/php -dvld.active=1 a.php
Finding entry points
Branch analysis from position: 0
Jump found. (Code = 77) Position 1 = 2, Position 2 = 15
Branch analysis from position: 2
Jump found. (Code = 78) Position 1 = 3, Position 2 = 15
Branch analysis from position: 3
Jump found. (Code = 42) Position 1 = 2
Branch analysis from position: 2
Branch analysis from position: 15
Jump found. (Code = 62) Position 1 = -2
Branch analysis from position: 15
filename:       /Users/jianyong/demo/a.php
function name:  (null)
number of ops:  17
compiled vars:  !0 = $arr, !1 = $v, !2 = $k
line     #* E I O op fetch ext return operands
-------------------------------------------------------------------------------------
   2     0  E >   ASSIGN                                                   !0, <array>
   4     1      > FE_RESET_R                                       $4      !0, ->15
         2    > > FE_FETCH_R                                       ~5      $4, !1, ->15
         3    >   ASSIGN                                                   !2, ~5
   5     4        INIT_FCALL                                               'key'
         5        SEND_VAR                                                 !0
         6        DO_ICALL                                         $7
         7        ECHO                                                     $7
         8        ECHO                                                     '%3D%3E'
         9        INIT_FCALL                                               'current'
        10        SEND_VAR                                                 !0
        11        DO_ICALL                                         $8
        12        ECHO                                                     $8
        13        ECHO                                                     '%0A'
        14      > JMP                                                      ->2
        15    >   FE_FREE                                                  $4
   7    16      > RETURN                                                   1

branch: # 0; line: 2- 4; sop: 0; eop: 1; out1: 2; out2: 15
branch: # 2; line: 4- 4; sop: 2; eop: 2; out1: 3; out2: 15
branch: # 3; line: 4- 5; sop: 3; eop: 14; out1: 2
branch: # 15; line: 5- 7; sop: 15; eop: 16; out1: -2
path #1: 0, 2, 3, 2, 15,
path #2: 0, 2, 15,
path #3: 0, 15,
0=>a
0=>a
0=>a
复制代码

PHP7新特性之foreach

  • [x] foreach 循环对数组内部指针再也不起做用, 在PHP7以前, 当数据经过foreach迭代时, 数组指针会移动。
<?php
$array = [0, 1, 2];
foreach ($array as &$val) {
    var_dump(current($array));
}
复制代码
版本 结果 说明
PHP5 int(1) int(2) bool(false) 数组指针会移动
PHP7 int(0) int(0) int(0) 数据指针再也不移动
  • [x] 按照值进行循环时, 对数组的修改是不会影响循环。

foreach按照值进行循环的时候(by-value), foreach是对该数组的一个拷贝进行操做. 因此在循环过程当中修改不影响循环结果

<?php
$arr = [0, 1, 2];
$ref = &$arr;

foreach ($arr as $val) {
    var_dump($val);
    unset($arr[1]);
}
复制代码
版本 结果 说明
PHP5 int(0) int(2) 会将unset的数据跳过
PHP7 int(0) int(1) int(2) 对数组的改动不影响循环
  • [x] 按照引用进行循环的时候, 对数组的修改会影响循环
<?php
$arr = [0, 1, 2];
$ref = &$arr;

foreach ($arr as &$val) {
    var_dump($val);
    unset($arr[1]);
}
复制代码
版本 结果
PHP5 int(0) int(2)
PHP7 int(0) int(2)
  • [x] 对简单对象plain(non-Traversable)的循环

在简单对象的循环, 不论是按照值循环仍是引用循环, 和按照引用对数组循环的行为是同样的, 不过对位置的管理会更加精确

  • [x] 对迭代对象(Traversable objects)对象行为和以前一致

stackoverflow 上面的解释, Traversable objects is one that implements Iterator or IteratorAggregate interface

若是一个对象实现了 Iterator 或者 IteratorAggregate 接口, 便可称之为迭代对象

参考