• 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)。 只有在执行某种修改后,才会执行其真正的副本(复制)。 请参阅 你被骗了,以获得有关此主题的更多的介绍。

  • 相关阅读:
    Truck History(poj 1789)
    Highways poj 2485
    117. Populating Next Right Pointers in Each Node II
    116. Populating Next Right Pointers in Each Node
    115. Distinct Subsequences
    114. Flatten Binary Tree to Linked List
    113. Path Sum II
    109. Convert Sorted List to Binary Search Tree
    106. Construct Binary Tree from Inorder and Postorder Traversal
    105. Construct Binary Tree from Preorder and Inorder Traversal
  • 原文地址:https://www.cnblogs.com/a609251438/p/12924680.html
Copyright © 2020-2023  润新知