SplFixedArray不是“正?!钡念?/h2>

最近在SegmentFault答了一個關(guān)于SplFixedArray的問題,重新整理成本文。

現(xiàn)象

<?php

$arrA = SplFixedArray ::fromArray(array(true));
$arrB = SplFixedArray ::fromArray(array(false));

//json_encode($arrB);

$equal = ($arrA == $arrB);
var_export($equal);

注釋掉json_encode($arrB)時,$equaltrue,去掉注釋,$equalfalse。

這個現(xiàn)象在PHP 5.3.0 - PHP 7.1.0里都存在。

PHP是如果比較對象相等的?

按直覺,兩個SplFixedArray對象里的數(shù)組內(nèi)容是不同的,不應(yīng)該出現(xiàn)$equaltrue的情況。我們看一下PHP源代碼中比較對象相等的代碼,我加了點注釋:

// Zend/zend_object_handlers.c
// 注意:調(diào)用zend_std_compare_objects前已經(jīng)判定了o1和o2地址不同

static int zend_std_compare_objects(zval *o1, zval *o2) /* {{{ */
{
    zend_object *zobj1, *zobj2;

    zobj1 = Z_OBJ_P(o1);
    zobj2 = Z_OBJ_P(o2);

    if (zobj1->ce != zobj2->ce) {  // 如果是不同類的對象,一定不相等
        return 1; /* different classes */
    }

    if (!zobj1->properties && !zobj2->properties) { // Step 1: 如果兩個對象沒有動態(tài)添加屬性
        zval *p1, *p2, *end;

        if (!zobj1->ce->default_properties_count) { // Step 2: 如果類定義(Class Entry)里沒有定義成員變量
            return 0;  // Step 3: 相等
        }

        // Step 4: 對比類定義的成員變量
        p1 = zobj1->properties_table;
        p2 = zobj2->properties_table;
        end = p1 + zobj1->ce->default_properties_count;
        Z_OBJ_PROTECT_RECURSION(o1);
        Z_OBJ_PROTECT_RECURSION(o2);
        do {
            ...
        } while (p1 != end);
        Z_OBJ_UNPROTECT_RECURSION(o1);
        Z_OBJ_UNPROTECT_RECURSION(o2);
        return 0;
    } else {
        // Step 4:重建properties
        if (!zobj1->properties) {
            rebuild_object_properties(zobj1);
        }
        if (!zobj2->properties) {
            rebuild_object_properties(zobj2);
        }
        // Step 5:對比properties
        return zend_compare_symbol_tables(zobj1->properties, zobj2->properties);
    }
}

ce表示Class Entry,保存類的定義,properties_table是對象的成員變量,properties是對象屬性(包括了成員變量),兩者是有區(qū)別的:

class A {
    public $a;
    public $b = 2;
}

$a1 = new A();
$a2 = new A();
$a2->a = 1;
$a2->c = 2;  // 添加了c

執(zhí)行完上面代碼后,$a1$a2的properties_table都有兩個元素(a, b),$a1properties是空的,而$a2的是有3元素的。
即動態(tài)添加屬性時,會把properties_table的成員變量到properties里,然后在添加到properties。

根據(jù)上面的代碼,總結(jié)對象的比較規(guī)則:

  1. 如果兩個對象是不同類型,不相等
  2. 如果兩個對象都沒有動態(tài)添加屬性(properties為空),比較兩者的成員變量(properties_table)
  3. 如果其中一個對象有動態(tài)添加屬性(properties不為空),如果另一個沒有的則添加(rebuild_object_properties會復制properties_table),然后比較兩者的屬性(properties

回到第一部分SplFixedArray的測試代碼,調(diào)試時發(fā)現(xiàn),沒有json_encode($arrB)時,$arrBproperties是空的,表示沒有動態(tài)添加屬性,而SplFixedArray類也沒定義成員變量,
所以走代碼中的Step 1 -> Step 2 -> Step 3,直接返回0表示相等。
而調(diào)用了json_encode($arrB)之后,$arrBproperties就不為空了,比較流程就變成:Step 1 -> Step 4 -> Step 5,這個時候就會比較對象的屬性。

到這里,我們可以確定:

  1. SplFixedArray對象本來是沒有成員變量、沒有動態(tài)添加的屬性,==比較都返回true
  2. SplFixedArray對象在json_encode后有了動態(tài)添加的屬性,==比較對象的屬性

json_encode為什么會動態(tài)添加屬性?

json_encode的代碼,其中是這一句:myht = Z_OBJPROP_P(val)Z_OBJPROP_P的定義:

#define Z_OBJPROP_P(zval_p) Z_OBJPROP(*(zval_p))

#define Z_OBJDEBUG(zval,tmp) (Z_OBJ_HANDLER((zval),get_debug_info)?Z_OBJ_HANDLER((zval),get_debug_info)(&(zval),&tmp):(tmp=0,Z_OBJ_HANDLER((zval),get_properties)?Z_OBJPROP(zval):NULL))

簡單來說就是調(diào)用對象的object handler里的里的get_debug_info或者get_properties,SplFixedArrayget_properties是這樣的:

static HashTable* spl_fixedarray_object_get_properties(zval *obj) /* {{{{ */
{
    spl_fixedarray_object *intern  = Z_SPLFIXEDARRAY_P(obj);
    HashTable *ht = zend_std_get_properties(obj);
    zend_long  i = 0;

    if (intern->array) {
        ... 復制數(shù)組到ht
    }

    return ht;
}

其中調(diào)用了zend_std_get_properties

ZEND_API HashTable *zend_std_get_properties(zval *object) /* {{{ */
{
    zend_object *zobj;
    zobj = Z_OBJ_P(object);
    if (!zobj->properties) {
        rebuild_object_properties(zobj);
    }
    return zobj->properties;
}

其中又調(diào)用了rebuild_object_properties,創(chuàng)建了properties!

類似的,var_dump也會又類似的獲取和創(chuàng)建對象屬性的流程。

結(jié)果

  1. SplFixedArray對象不能通過==進行比較
  2. 使用get_properties的函數(shù)(var_dump、json_encode……)會導致SplFixedArray復制底層的C數(shù)組到PHP的數(shù)組,導致內(nèi)存占用增大

可能的修復方式

  1. SplFixedArrayobject handler要定義compare_objects,實現(xiàn)正確的比較
  2. SplFixedArrayget_properties不要調(diào)用zend_std_get_properties,而是直接返回一個HashTable,之后讓gc清理掉,避免一直占用內(nèi)存。

但是,還沒測試過,不知實際可不可行。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容