zval介绍-foreach问题(PHP)

zval介绍

先看如下代码

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

foreach ($arr as &$item) {
    var_dump($item);
}

echo "\n\n";

foreach ($arr as $item) {
    var_dump($item);
}

预期结果应该是

int(1)
int(2)
int(3)


int(1)
int(2)
int(3)
[Finished in 0.1s]

运行结果却是

int(1)
int(2)
int(3)


int(1)
int(2)
int(2)
[Finished in 0.1s]

代码改为

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

foreach ($arr as &$item) {
    var_dump($item);
}

unset($item);
echo "\n\n";

foreach ($arr as $item) {
    var_dump($item);
}

结果就正常了

but why???

说原因之前,先了解下ZVAL的大致结构:

“zval结构体中有四个字段,其含义分别为:

属性名 含义 默认值
refcount__gc 表示引用计数 1
is_ref__gc 表示是否为引用 0
value 存储变量的值 
type 变量具体的类型”

摘录来自: Reeze Xia. “TIPI: 深入理解PHP内核”。

利用xdebug扩展,可以看到refcount和is_ref的情况:

<?php

$a = "hello world";
xdebug_debug_zval('a');

然后在PHP7下运行:

a: (refcount=2, is_ref=0)='hello world'

在PHP5.5下运行:(我没试过,因为环境都是php7)

a: (refcount=1, is_ref=0)='hello world'

为什么7的refcount 为2呢?这里是php7的字面量也计算了一次次数

<?php

$a = "hello world";
$b = $a . $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

结果为:

a: (refcount=2, is_ref=0)='hello world'
b: (refcount=1, is_ref=0)='hello worldhello world'

加上引用符号的时候:

<?php

$a = "hello world";
$b = &$a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');

结果为:

a: (refcount=2, is_ref=1)='hello world'
b: (refcount=2, is_ref=1)='hello world'

foreach 内部的实现类似于:
foreach具体实现

foreach ($arr as $k => $item)

$item = $arr[$k];
foreach ($arr as $k => &$item)

$item = &$arr[$k];

了解这些之后,回头看下最初的问题

第一次foreach结束的时候, item并没有释放掉,item为arr[k]的引用

所以在第二次foreach的时候,item始终为数组最后一个元素的引用,循环的每次,都给数组最后一项进行了赋值操作

所以在倒数第二次的循环的时候,给最后一项赋值了倒数第二项的值。进行最后一次循环的时候,值为倒数第二项的值,最后一项的值已经丢掉了

在第二次进行循环的时候,给item进行一次unset操作,因为item是对arr[k]的引用,查看zval如下

item:(refcount=2, is_ref=1)int 1
item:(refcount=2, is_ref=1)int 2
item:(refcount=2, is_ref=1)int 3

进行unset操作只是对应的变量的refcount减1,并不会删除变量内容

所以第二次的结果就正常了

php的引用计数是对于内存进行的优化,给变量赋值操作的话,不进行拼接等操作,只是引用计数增加,并不会增加内存空间的占用

函数参数和返回值

对于只是内存操作的话,比如拼装where条件等(php7.1之后,支持类型限制)

function test_mem(array $arr) : array
{
	foreach ( $arr as $item ) {
	    var_dump($item);
	}
	return $arr;
}

如果有io操作,比如数据库操作,http调用操作(进一步可以使用try catch)
函数的返回值标识执行结果的成功与失败,函数的返回参数通过传进去一个空的变量,通过这个变量拿出来

function test_io(array &$resp, array $params) : bool
{
    $resp = [];
	foreach ( $params as $item ) {
	    var_dump($item);
	}
	return true;
}

php原生函数,ksort函数的参数就是引用传值

php也有类似go的返回类型

<?php
function test_go(array $params) : array
{
    $ret = [
        [],    // data
        '',    // msg
        false, // status
    ];
    foreach ( $params as $item ) {
        var_dump($item);
    }
    $ret['data'] = $params;
    $ret['msg']  = 'success';
    $ret['status'] = true;
    return $ret;
}

$data = [];
$msg = '';
$status = false;
$params = [1,2,3,4,5,6];
[$data, $msg, $status] = test_go($params);

进一步延申:

深入理解PHP7内核之zval
比如这种

struct _zval_struct {
     union {
          long lval;
          double dval;
          struct {
               char *val;
               int len;
          } str;
          HashTable *ht;
          zend_object_value obj;
          zend_ast *ast;
     } value;
     zend_uint refcount__gc;
     zend_uchar type;
     zend_uchar is_ref__gc;
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章