当前位置:首页 > PHP教程 > PHP高级教程

深入解析PHP中foreach语句控制数组循环的用法

foreach是php中很常用的一个用作数组循环的控制语句。
因为它的方便和易用,自然也就在后端隐藏着很复杂的具体实现方式(对用户透明)
今天,我们就来一起分析分析,foreach是如何实现数组(对象)的遍历的。
我们知道php是一个脚本语言,也就是说,用户编写的php代码最终都是会被php解释器解释执行,
特别的,对于php来说,所有的用户编写的php代码,都会被翻译成php的虚拟机ze的虚拟指令(opcodes)来执行,不论细节的话,就是说,我们所编写的任何php脚本,都会最终被翻译成一条条的指令,从而根据指令,由相应的c编写的函数来执行。

那么foreach会被翻译成什么样子呢?

foreach($arr as $key => $val){
   echo $key . '=>' . $val . "n";
}

在词法分析阶段,foreach会被识别为一个token:t_foreach,
在语法分析阶段,会被规则:

 unticked_statement: //没有被绑定ticks的语句
   //有省略
  |  t_foreach '(' variable t_as
    { zend_do_foreach_begin(&$1, &$2, &$3, &$4, 1 tsrmls_cc); }
    foreach_variable foreach_optional_arg ')' { zend_do_foreach_cont(&$1, &$2, &$4, &$6, &$7 tsrmls_cc); }
    foreach_statement { zend_do_foreach_end(&$1, &$4 tsrmls_cc); }
  |  t_foreach '(' expr_without_variable t_as
    { zend_do_foreach_begin(&$1, &$2, &$3, &$4, 0 tsrmls_cc); }
    variable foreach_optional_arg ')' { zend_check_writable_variable(&$6); zend_do_foreach_cont(&$1, &$2, &$4, &$6, &$7 tsrmls_cc); }
    foreach_statement { zend_do_foreach_end(&$1, &$4 tsrmls_cc); }
   //有省略
;

仔细分析这段语法规则,我们可以发现,对于:

foreach($arr as $key => $val){
echo $key . ‘=>' . $val .”n”;
}

会被分析为:

   t_foreach '(' variable t_as  { zend_do_foreach_begin('foreach', '(', $arr, 'as', 1 tsrmls_cc); }
  foreach_variable foreach_optional_arg(t_double_arrow foreach_variable)  ')' { zend_do_foreach_cont('foreach', '(', 'as', $key, $val tsrmls_cc); }
  foreach_satement {zend_do_foreach_end('foreach', 'as');}

然后,让我们来看看foreach_statement:
它其实就是一个代码块,体现了我们的 echo $key . ‘=>' . $val .”n”;
t_echo expr;

显然,实现foreach的核心就是如下3个函数:

  • zend_do_foreach_begin
  • zend_do_foreach_cont
  • zend_do_foreach_end
  • 其中,zend_do_foreach_begin (代码太长,直接写伪码) 主要做了:
    1. 记录当前的opline行数(为以后跳转而记录)
    2. 对数组进行reset(讲内部指针指向第一个元素)
    3. 获取临时变量 ($val)
    4. 设置获取变量的opcode fe_fetch,结果存第3步的临时变量
    4. 记录获取变量的opcodes的行数

    而对于 zend_do_foreach_cont来说:
    1. 根据foreach_variable的u.ea.type来判断是否引用
    2. 根据是否引用来调整zend_do_foreach_begin中生成的fe_fetch方式
    3. 根据zend_do_foreach_begin中记录的取变量的opcodes的行数,来初始化循环(主要处理在循环内部的循环:do_begin_loop)

    最后zend_do_foreach_end:
    1. 根据zend_do_foreach_begin中记录的行数信息,设置zend_jmp opcodes
    2. 根据当前行数,设置循环体下一条opline, 用以跳出循环
    3. 结束循环(处理循环内循环:do_end_loop)
    4. 清理临时变量

    当然, 在zend_do_foreach_cont 和 zend_do_foreach_end之间 会在语法分析阶段被填充foreach_satement的语句代码。

    这样,就实现了foreach的opcodes line。
    比如对于我们开头的实例代码,最终生成的opcodes是:

    filename:    /home/huixinchen/foreach.php
    function name: (null)
    number of ops: 17
    compiled vars: !0 = $arr, !1 = $key, !2 = $val
    line   # op              fetch     ext return operands
    -------------------------------------------------------------------------------
      2   0 send_val                         1
         1 send_val                         100
         2 do_fcall                   2     'range'
         3 assign                          !0, $0
      3   4 fe_reset                     $2   !0, ->14
         5 fe_fetch                     $3   $2, ->14
         6 zend_op_data                   ~5
         7 assign                          !2, $3
         8 assign                          !1, ~5
      4   9 concat                      ~7   !1, '-'
        10 concat                      ~8   ~7, !2
        11 concat                      ~9   ~8, '%0a'
        12 echo                           ~9
      5  13 jmp                           ->5
        14 switch_free                       $2
      7  15 return                          1
        16* zend_handle_exception
    

    我们注意到fe_fetch的op2的操作数是14,也就是jmp后一条opline,也就是说,在获取完最后一个数组元素以后,fe_fetch失败的情况下,会跳到第14行opline,从而实现了循环的结束。
    而15行opline的op1的操作数是指向了fe_fetch,也就是无条件跳转到第5行opline,从而实现了循环。

    附录:

    void zend_do_foreach_begin(znode *foreach_token, znode *open_brackets_token, znode *array, znode *as_token, int variable tsrmls_dc)
    {
      zend_op *opline;
      zend_bool is_variable;
      zend_bool push_container = 0;
      zend_op dummy_opline;
     
      if (variable) {
         //是否是匿名数组
        if (zend_is_function_or_method_call(array)) {
            //是否是函数返回值
          is_variable = 0;
        } else {
          is_variable = 1;
        }
        /* 使用括号记录fe_reset的opline行数 */
        open_brackets_token->u.opline_num = get_next_op_number(cg(active_op_array));
        zend_do_end_variable_parse(bp_var_w, 0 tsrmls_cc); //获取数组/对象和zend_do_begin_variable_parse对应
        if (cg(active_op_array)->last > 0 &&
          cg(active_op_array)->opcodes[cg(active_op_array)->last-1].opcode == zend_fetch_obj_w) {
          /* only lock the container if we are fetching from a real container and not $this */
          if (cg(active_op_array)->opcodes[cg(active_op_array)->last-1].op1.op_type == is_var) {
            cg(active_op_array)->opcodes[cg(active_op_array)->last-1].extended_value |= zend_fetch_add_lock;
            push_container = 1;
          }
        }
      } else {
        is_variable = 0;
        open_brackets_token->u.opline_num = get_next_op_number(cg(active_op_array));
      }
     
      foreach_token->u.opline_num = get_next_op_number(cg(active_op_array)); //记录数组reset opline number
     
      opline = get_next_op(cg(active_op_array) tsrmls_cc); //生成reset数组opcode
     
      opline->opcode = zend_fe_reset;
      opline->result.op_type = is_var;
      opline->result.u.var = get_temporary_variable(cg(active_op_array));
      opline->op1 = *array;
      set_unused(opline->op2);
      opline->extended_value = is_variable ? zend_fe_reset_variable : 0;
     
      dummy_opline.result = opline->result;
      if (push_container) {
        dummy_opline.op1 = cg(active_op_array)->opcodes[cg(active_op_array)->last-2].op1;
      } else {
        znode tmp;
     
        tmp.op_type = is_unused;
        dummy_opline.op1 = tmp;
      }
      zend_stack_push(&cg(foreach_copy_stack), (void *) &dummy_opline, sizeof(zend_op)); 
     
      as_token->u.opline_num = get_next_op_number(cg(active_op_array)); //记录循环起始点
     
      opline = get_next_op(cg(active_op_array) tsrmls_cc);
      opline->opcode = zend_fe_fetch;
      opline->result.op_type = is_var;
      opline->result.u.var = get_temporary_variable(cg(active_op_array));
      opline->op1 = dummy_opline.result;  //被操作数组
      opline->extended_value = 0;
      set_unused(opline->op2);
     
      opline = get_next_op(cg(active_op_array) tsrmls_cc);
      opline->opcode = zend_op_data; //当使用key的时候附属操作数,当foreach中不包含key时忽略
      set_unused(opline->op1);
      set_unused(opline->op2);
      set_unused(opline->result);
    }
    void zend_do_foreach_cont(znode *foreach_token, const znode *open_brackets_token, const znode *as_token, znode *value, znode *key tsrmls_dc)
    {
      zend_op *opline;
      znode dummy, value_node;
      zend_bool assign_by_ref=0;
     
      opline = &cg(active_op_array)->opcodes[as_token->u.opline_num]; //获取fe_fetch opline
      if (key->op_type != is_unused) {
        znode *tmp;//交换key和val
     
        tmp = key;
        key = value;
        value = tmp;
     
        opline->extended_value |= zend_fe_fetch_with_key; //表明需要同时获取key和val
      }
     
      if ((key->op_type != is_unused) && (key->u.ea.type & zend_parsed_reference_variable)) {
         //key不能以引用方式获取
        zend_error(e_compile_error, "key element cannot be a reference");
      }
     
      if (value->u.ea.type & zend_parsed_reference_variable) {
         //以引用方式获取值
        assign_by_ref = 1;
        if (!(opline-1)->extended_value) {
            //根据fe_fetch的上一条opline也就是获取数组的扩展值来判断数组是否是匿名数组
          zend_error(e_compile_error, "cannot create references to elements of a temporary array expression");
        }
     
        opline->extended_value |= zend_fe_fetch_byref; //指明按引用取
        cg(active_op_array)->opcodes[foreach_token->u.opline_num].extended_value |= zend_fe_reset_reference; //重置原数组
      } else {
        zend_op *foreach_copy;
        zend_op *fetch = &cg(active_op_array)->opcodes[foreach_token->u.opline_num];
        zend_op *end = &cg(active_op_array)->opcodes[open_brackets_token->u.opline_num];
     
        /* change "write context" into "read context" */
        fetch->extended_value = 0; /* reset zend_fe_reset_variable */
        while (fetch != end) {
          --fetch;
          if (fetch->opcode == zend_fetch_dim_w && fetch->op2.op_type == is_unused) {
            zend_error(e_compile_error, "cannot use [] for reading");
          }
          fetch->opcode -= 3; /* fetch_w -> fetch_r */
        }
     
        /* prevent double switch_free */
        zend_stack_top(&cg(foreach_copy_stack), (void **) &foreach_copy);
        foreach_copy->op1.op_type = is_unused;
      }
     
      value_node = opline->result; 
     
      if (assign_by_ref) {
        zend_do_end_variable_parse(value, bp_var_w, 0 tsrmls_cc); //获取值(引用)
        zend_do_assign_ref(null, value, &value_node tsrmls_cc);//指明value node的type是is_var
      } else {
        zend_do_assign(&dummy, value, &value_node tsrmls_cc); //获取copy值
        zend_do_free(&dummy tsrmls_cc);
      }
     
      if (key->op_type != is_unused) {
        znode key_node;
     
        opline = &cg(active_op_array)->opcodes[as_token->u.opline_num+1];
        opline->result.op_type = is_tmp_var;
        opline->result.u.ea.type = 0;
        opline->result.u.opline_num = get_temporary_variable(cg(active_op_array));
        key_node = opline->result;
     
        zend_do_assign(&dummy, key, &key_node tsrmls_cc);
        zend_do_free(&dummy tsrmls_cc);
      }
     
      do_begin_loop(tsrmls_c);
      inc_bpc(cg(active_op_array));
    }
    void zend_do_foreach_end(znode *foreach_token, znode *as_token tsrmls_dc)
    {
      zend_op *container_ptr;
      zend_op *opline = get_next_op(cg(active_op_array) tsrmls_cc); //生成jmp opcode
     
      opline->opcode = zend_jmp;
      opline->op1.u.opline_num = as_token->u.opline_num; //设置jmp到fe_fetch opline行
      set_unused(opline->op1);
      set_unused(opline->op2);
     
      cg(active_op_array)->opcodes[foreach_token->u.opline_num].op2.u.opline_num = get_next_op_number(cg(active_op_array)); //设置跳出循环的opline行
      cg(active_op_array)->opcodes[as_token->u.opline_num].op2.u.opline_num = get_next_op_number(cg(active_op_array)); //同上
     
      do_end_loop(as_token->u.opline_num, 1 tsrmls_cc); //为循环嵌套而设置
     
      zend_stack_top(&cg(foreach_copy_stack), (void **) &container_ptr);
      generate_free_foreach_copy(container_ptr tsrmls_cc);
      zend_stack_del_top(&cg(foreach_copy_stack));
     
      dec_bpc(cg(active_op_array)); //为php interactive模式而设置
    }
    
    

    同时还要注意的是,foreach在使用中是值还是传引用的问题。
    php 中遍历一个array时可以使用for或foreach,foreach的语法为:foreach ($arr as $k => $v)。遍历数组,把index赋给$k,数组的值赋给$v,那么此处的赋值是传值还是传引用呢。先看下面的例子:

    $arr = array(
      array('id' => 1, 'name' => 'name1'),
      array('id' => 2, 'name' => 'name2'),
    );
    
    foreach ($arr as $obj) {
      $obj['id'] = $obj['id'];
      $obj['name'] = $obj['name'] . '-modify';
    }
    
    print_r($arr); //输出的结果
    array(
      [0] => array (
        [id] => 1
        [name] => name1
      )
      [1] => array(
        [id] => 2
        [name] => name2
      )
    )
    
    

    观察可以发现在foreach循环中对$arr操作并没有影响到$arr的元素,所以这里的赋值是传值而不是传引用。那如果需要修改$arr中元素的值该怎么办呢?可以在变量前面加一个”&”符号,例如:

    foreach ($arr as &$obj) {
      $obj['id'] = $obj['id'];
      $obj['name'] = $obj['name'] . '-modify';
    }
    

    再看另外一个例子,array里面存放的是object,

    $arr = array(
      (object)(array('id' => 1, 'name' => 'name1')),
      (object)(array('id' => 2, 'name' => 'name2')),
    );
    
    foreach ($arr as $obj) {
      $obj->name = $obj->name . '-modify'; 
    }
    
    print_r($arr); //输出的结果
    
    array
    (
      [0] => stdclass object
        (
          [id] => 1
          [name] => name1-modify
        )
    
      [1] => stdclass object
        (
          [id] => 2
          [name] => name2-modify
        )
    
    )
    
    

    此时可以看到原始数组中的object对象已经修改了,所以这里的赋值又是传引用而不是传值

    综合上述,得出的结论:如果数组里面存放的是普通类型的元素就是采用传值的方式,存放对象类型元素采用的方式为传地址。


    【说明】本文章由站长整理发布,文章内容不代表本站观点,如文中有侵权行为,请与本站客服联系(QQ:)!