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;
};
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章