zoukankan      html  css  js  c++  java
  • PHP 内核:foreach 是如何工作的(一)

    foreach 是如何工作的?

    PHP 内核:foreach 是如何工作的(二)

    首先声明,我知道 foreach 是什么,也知道怎么去用它。但这个问题关心的是,内核中 foreach 是如何运行的,我不想回答关于 “如何使用 foreach 循环数组” 的任何问题。

    很长时间我都认为 foreach 是直接作用于数组本身,后来一些资料表明,它作用于数组的一个副本,那时我以为这就是真相了。但最近我又讨论了一下这件事,经过一些试验,发现我之前的想法并非完全正确。

     

    让我来展示一下我的观点。下面的测试用例中我们将使用以下数组:

     

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

    测试用例 1:

    foreach ($array as $item) {
      echo "$item
    ";
      $array[] = $item;
    }
    print_r($array);
    
    /* 循环中输出:       1 2 3 4 5
       循环后的$array: 1 2 3 4 5 1 2 3 4 5 */
    
    

    这很清晰的表明我们不直接使用数据源 - 否则循环会一直持续下去,因此我们可以在循环中不停的推送元素到数组中。为了保证正确请看下面的测试用例:

    测试用例 2:

    foreach ($array as $key => $item) {
     $array[$key + 1] = $item + 2;
      echo "$item
    ";
    }
    
    print_r($array);
    
    /* 循环中输出:    1 2 3 4 5
       循环后 $array: 1 3 4 5 6 7 */
    

    这印证了我们的初步结论,在循环中使用的是数组的副本,否则我们将看到在循环中改变后的值。 但是...

    如果我们查阅手册 手册,我们会发现下面这句话:

    当 foreach 首次开始执行时,数组的内部指针自动重置为数组的第一个元素。

     

    没错。。。这似乎表明 foreach 依赖源数组的指针。但是我们刚刚证明我们 没有使用源数组,对吧?好吧,不完全是。

    测试用例 3:

    // 将数组指针移动到一个上面确保它不会影响循环
    var_dump(each($array));
    
    foreach ($array as $item) {
      echo "$item
    ";
    }
    
    var_dump(each($array));
    
    /* 输出
      array(4) {
        [1]=>
        int(1)
        ["value"]=>
        int(1)
        [0]=>
        int(0)
        ["key"]=>
        int(0)
      }
      1
      2
      3
      4
      5
      bool(false)
    */
    
    

    因此,尽管我们不支持使用源数组,但是直接使用源数组指针 - 指针位于循环结束时的数组末尾证明了这一点。除非这不是真的 - 如果是,那么 测试用例 1 将永远循环。

    PHP 手册还说明:

    由于 foreach 依赖与内部数组指针,因此在循环内部改变它可能导致意外的行为。

    让我们找出那种 “意外行为” 是什么 (从技术上讲,任何行为都是意外的,因为我们也不知道将会发生什么)。

    测试用例 4:

    foreach ($array as $key => $item) {
      echo "$item
    ";
      each($array);
    }
    
    /* 输出: 1 2 3 4 5 */

    测试用例 5:

    foreach ($array as $key => $item) {
      echo "$item
    ";
      reset($array);
    }
    
    /* 输出: 1 2 3 4 5 */

    ... 意料之中,事实上它似乎支持 「复制源」 理论。

    问题

    这是怎么回事呢?我的 C-fu 还不够好,不能通过简单地查看 PHP 源代码就得出正确的结论,如果有人能把它翻译成英语,我将不胜感激。

    在我看来,foreach 使用数组的 copy ,但是在循环之后将源数组的数组指针设置为数组的末尾。

    • 这是真的吗?
    • 如果不是,正确的流程是什么样的呢?
    • 在 foreach 期间使用调整数组指针 (each()reset() 等) 的函数是否会影响循环的结果呢?

    解答

    foreach 支持三种不同类型值的迭代:

    在下面的讨论中,我将尝试准确的解释迭代在不同的场景中是如何工作的。到目前为止,最简单的例子是 Traversable 对象,因为这些 foreach 本质上只是以下代码的语法糖:

    foreach ($it as $k => $v) { /* ... */ }
    
    /* 转换为: */
    
    if ($it instanceof IteratorAggregate) {
        $it = $it->getIterator();
    }
    for ($it->rewind(); $it->valid(); $it->next()) {
        $v = $it->current();
        $k = $it->key();
        /* ... */
    }
    

    对于内部类,通过使用一个内部 API 来避免实际的方法调用,这个 API 本质上只是在 C 级对 Iterator 接口的映射。

    数组和普通对象的迭代要复杂的多。首先,应该注意,在 PHP 中,“数组” 实际上是有序的字典,它们将按照这个顺序遍历(只要不使用形如 sort 一类的函数对其排序,它就能按照插入的顺序遍历)。这与按照键的自然顺序迭代(其他语言中的列表通常是如何排序的呢?)或者无序(其他语言中的字典通常是如何工作的呢)是截然不同的。

    同样的情况也适用于对象,因为对象属性可以看做是另一个(有序的)字典,将属性名映射为对应的值,并加上一些可见性的操作处理。在大多数情况下,对象属性实际上并不是以这种低效的方式存储的。然而,如果你开始遍历一个对象,通常它将被打包转换为一个真正的字典。在这一点上,普通对象的迭代与数组的迭代非常相似(这就是为什么我在这没有过多讨论普通对象的迭代)。

    到目前为止,一切顺利。遍历字典应该不难,对吧?当你意识到数组 / 对象可以在迭代期间更改时,问题就出现了。发生这种情况有如下几种:

     

    • 如果你使用 foreach($arr as &$v) 通过引用迭代,那么 $arr 将会转为引用,你可以在迭代期间更改它。
    • 在 PHP 5 中,即使按值迭代也是如此,但数组之前是一个引用:$ref=&$arr;foreach($ref as $v)。
    • 对象具有处理传递语义的功能,对于大多数实际用途而言,它们的行为类似于引用。 因此,在迭代期间总是可以更改对象。

    在迭代期间允许修改的问题是删除当前所在元素的情况。假设你使用指针来跟踪你当前所在的数组元素。 如果现在释放了此元素,则会留下悬空指针(通常会导致 segfault 段错误)

    有不同的方法来解决这个问题。 PHP 5 和 PHP 7 在这方面有很大不同,我将在下面描述这两种情况。 总结是 PHP 5 的方法相当愚蠢并导致出现各种奇怪的边缘情况问题,而 PHP 7 更复杂的方法导致出现更可预测和一致的行为情况。

    初步得出结论,PHP 是使用引用计数和写时复制来管理内存。 这意味着如果你 “复制” 一个值,实际上只是复用其旧值并增加其引用计数(refcount)。 只有在执行某种修改后,才会执行其真正的副本(复制)。 请参阅 你被骗了,以获得有关此主题的更多的介绍。

  • 相关阅读:
    Linux 的文件软链接如何删除
    mysql-xtrabackup备份sh: xtrabackup_56: command not found与error while loading shared libraries: libssl.so.6: cannot open shared object file: No such file or directory
    How To Upgrade ASMLib Kernel Driver as Part of Kernel Upgrade? (文档 ID 1391807.1)
    [trouble] error connecting to master 'repl@192.168.1.107:3306'
    Troubleshooting 10g and 11.1 Clusterware Reboots (文档 ID 265769.1)
    "Last_IO_Error: Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs
    RMAN-06900 RMAN-06901 ORA-19921
    一则ORACLE进程都在但是无法进入实例的问题
    VirtualBox下Win7下CPU高占用的一次故障解决
    netcore之mysql中文乱码问题解决记录
  • 原文地址:https://www.cnblogs.com/a609251438/p/12924680.html
Copyright © 2011-2022 走看看