count 函式原始碼分析

suhanyujie發表於2019-05-10
  • 本文首發https://github.com/suhanyujie/learn-computer/blob/master/src/function/array/count.md
  • 基於PHP 7.3.3
  • 由於不瞭解PHP的原始碼,用工具搜尋了半天 count ,這個關鍵字的結果太多,挨個看了一遍都沒看到 count 實現位置。
  • 去百度了一下,通過其中實現體中的 php_count_recursive 關鍵字,才找到 count 的實現。
  • 位於檔案 ext/standard/array.c 中 776 行,搜尋關鍵字 PHP_FUNCTION(count) 即可搜尋到。
  • 實現原始碼如下:
PHP_FUNCTION(count)
{
    zval *array;
    zend_long mode = COUNT_NORMAL;
    zend_long cnt;

    ZEND_PARSE_PARAMETERS_START(1, 2)
        Z_PARAM_ZVAL(array)
        Z_PARAM_OPTIONAL
        Z_PARAM_LONG(mode)
    ZEND_PARSE_PARAMETERS_END();

    switch (Z_TYPE_P(array)) {
        case IS_NULL:
            php_error_docref(NULL, E_WARNING, "Parameter must be an array or an object that implements Countable");
            RETURN_LONG(0);
            break;
        case IS_ARRAY:
            if (mode != COUNT_RECURSIVE) {
                cnt = zend_array_count(Z_ARRVAL_P(array));
            } else {
                cnt = php_count_recursive(Z_ARRVAL_P(array));
            }
            RETURN_LONG(cnt);
            break;
        case IS_OBJECT: {
            zval retval;
            /* first, we check if the handler is defined */
            if (Z_OBJ_HT_P(array)->count_elements) {
                RETVAL_LONG(1);
                if (SUCCESS == Z_OBJ_HT(*array)->count_elements(array, &Z_LVAL_P(return_value))) {
                    return;
                }
            }
            /* if not and the object implements Countable we call its count() method */
            if (instanceof_function(Z_OBJCE_P(array), zend_ce_countable)) {
                zend_call_method_with_0_params(array, NULL, NULL, "count", &retval);
                if (Z_TYPE(retval) != IS_UNDEF) {
                    RETVAL_LONG(zval_get_long(&retval));
                    zval_ptr_dtor(&retval);
                }
                return;
            }

            /* If There's no handler and it doesn't implement Countable then add a warning */
            php_error_docref(NULL, E_WARNING, "Parameter must be an array or an object that implements Countable");
            RETURN_LONG(1);
            break;
        }
        default:
            php_error_docref(NULL, E_WARNING, "Parameter must be an array or an object that implements Countable");
            RETURN_LONG(1);
            break;
    }
}

part 1 引數處理

  • 先看第一部分:
ZEND_PARSE_PARAMETERS_START(1, 2)
    Z_PARAM_ZVAL(array)
    Z_PARAM_OPTIONAL
    Z_PARAM_LONG(mode)
ZEND_PARSE_PARAMETERS_END();
  • 在舊版的PHP中,獲取引數的寫法是 (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "z|l", &array, &mode) == FAILURE) ,但在 7.3 的寫法中,使用的是 FAST ZPP 方式,也就是 ZEND_PARSE_PARAMETERS_* 相關的巨集
  • 引數部分 (1, 2) ,第1個參數列示最少引數時的引數個數,這裡的 1 表示呼叫 count 時,最少要有1個引數。第2個參數列示,引數最多時的引數個數,這裡的 2 表示最多有2個引數。

part 2 型別匹配

  • 使用 switch 匹配傳入的引數的型別
  • 可以看出,只有當引數1是陣列或者物件型別時,才回執行正常的邏輯

引數是陣列時

if (mode != COUNT_RECURSIVE) {
    cnt = zend_array_count(Z_ARRVAL_P(array));
} else {
    cnt = php_count_recursive(Z_ARRVAL_P(array));
}
RETURN_LONG(cnt);
  • 在不進行遞迴計算元素數量的情況下,最後呼叫的是 (ht)->nNumOfElements ,也就是返回陣列變數對應的結構體成員 nNumOfElements
  • 在進行遞迴統計的情況下,底層會遞迴呼叫 php_count_recursive 函式,進行統計單元數量。
  • zend_array_count
uint32_t num;
if (UNEXPECTED(HT_FLAGS(ht) & HASH_FLAG_HAS_EMPTY_IND)) {
    num = zend_array_recalc_elements(ht);
    if (UNEXPECTED(ht->nNumOfElements == num)) {
        HT_FLAGS(ht) &= ~HASH_FLAG_HAS_EMPTY_IND;
    }
}...
...
  • 其中的這一段邏輯是處理特殊情況下的元素數量統計,針對其中的 HASH_FLAG_HAS_EMPTY_IND ,它定義是 #define HASH_FLAG_HAS_EMPTY_IND (1<<5)
  • google 檢視了一下核心相關文件,有一下介紹

    This flag is set when a HashTable needs its element count to be recalculated. One hash table where this always needs to be performed is the executor globals symbol table (for the $GLOBALS PHP array). This is because this hash table holds elements of type IS_INDIRECT, which means the values they point to could be unset (see IS_UNDEF). The only way to get the true element count of such a hash table is to iterate through all of its elements and check specifically for this condition.

  • 大意是:當雜湊表需要重新計算其元素時設定這個標誌位。全域性的符號表(PHP中的 $GLOBALS 陣列)就是一個經常要執行這個操作的雜湊表。這是因為這個雜湊表包含 IS_INDIRECT 型別的元素,這意味著它們指向的值會被 unset (查閱 IS_UNDEF)。獲取這類雜湊表的真正元素計數的方法是遍歷它的所有元素並專門檢查這個這個標誌位。
  • 當你 unset 一個陣列單元之後,並且 gc 尚未對其進行回收,導致單元從某種意義上還是存在,只是其標誌位對其標識 unset ,此時進行 count 操作,需要去除這些陣列單元。

引數是物件時

  • 先判斷檢查物件是否定義了 handler 。 Z_OBJ_HT_P(array)->count_elements
  • Z_OBJ_HT_P(array) 的作用是返回物件中的 value 的 handler table
  • count_elements 是物件相關結構體 _zend_object_handlers 中的一個成員
  • handler table 的定義中,它被定義為底層的行為。
  • 根據 php 官方文件,在引入 zend 標準物件之後,它們預設有以下這些項:
typedef struct _zend_object_handlers {
    /* general object functions */
    zend_object_add_ref_t              add_ref;
    zend_object_del_ref_t              del_ref;
    zend_object_clone_obj_t            clone_obj;
    /* individual object functions */
    zend_object_read_property_t        read_property;
    zend_object_write_property_t       write_property;
    zend_object_read_dimension_t       read_dimension;
    zend_object_write_dimension_t      write_dimension;
    zend_object_get_property_ptr_ptr_t get_property_ptr_ptr;
    zend_object_get_t                  get;
    zend_object_set_t                  set;
    zend_object_has_property_t         has_property;
    zend_object_unset_property_t       unset_property;
    zend_object_has_dimension_t        has_dimension;
    zend_object_unset_dimension_t      unset_dimension;
    zend_object_get_properties_t       get_properties;
    zend_object_get_method_t           get_method;
    zend_object_call_method_t          call_method;
    zend_object_get_constructor_t      get_constructor;
    zend_object_get_class_entry_t      get_class_entry;
    zend_object_get_class_name_t       get_class_name;
    zend_object_compare_t              compare_objects;
    zend_object_cast_t                 cast_object;
    zend_object_count_elements_t       count_elements;
    zend_object_get_debug_info_t       get_debug_info;
    zend_object_get_closure_t          get_closure;
} zend_object_handlers;
  • 除非特別指定,否則其中的引數被認為是非空指標。
  • 不脫離主題,我們回到 count_elements 上來,它的函式簽名是: int (*count_elements)(zval *object, long *count TSRMLS_DC)
  • 對它的描述大概如下:

    • 呼叫此函式可以確定某個可計數物件的計數。計數是非負數。
    • 物件有類似陣列的訪問元素的功能,並在未來可能會實現,這樣他們的行為就更像是陣列了。
    • 這個 handler 不常被 zend 引擎使用,而是由 count 和其他擴充套件使用。
    • 這個程式在向 *count 寫入一個非負數,並且如果傳遞的物件是可計數的,返回 SUCCESS,否則返回失敗。
    • 如果物件是不是可計數的,則 count_elements 可能為空,即使實現了 count_elements ,也會總是返回失敗。
  • 如果物件是可計數的,但沒有定義 count_elements 。隨後,會判斷改物件是否實現 Countable
  • 如果實現,則進行呼叫物件中實現的 count() 方法
  • 如果既沒有定義 count_elements ,也沒有實現 Countable ,則會報錯處理。

例項

  • 對物件進行 count 操作倒是用的少,不妨試試看:
<?php
class ThirdTypeA 
{
    public $data = [
        'merchantId'=>1,
        'key'=>'testxxkey32Xsdadxaqqwey',
    ];

    public function count()
    {
        return count($this->data);
    }
}

$ins = new ThirdTypeA;
$res = count($ins);
var_dump($res);
  • 此時返回 1,並且PHP提示了一個 Warning:
PHP Warning:  count(): Parameter must be an array or an object that implements Countable in /xxxxx/countExample.php on line 16
int(1)
  • 這個 1 並不是計數的結果,而是異常時的 code ,是符合原始碼中的邏輯:
php_error_docref(NULL, E_WARNING, "Parameter must be an array or an object that implements Countable");
RETURN_LONG(1);
  • 改進一下,同樣的程式碼,只是在宣告類的時候,顯示的實現 Countable 介面: class ThirdTypeA implements Countable
  • Countable 介面類中很簡單,只有1個 count 方法:
interface Countable {

    /**
     * Count elements of an object
     * @link https://php.net/manual/en/countable.count.php
     * @return int The custom count as an integer.
     * </p>
     * <p>
     * The return value is cast to an integer.
     * @since 5.1.0
     */
    public function count();
}
  • 因而在 implements Countable 時,需要實現方法 count
public function count()
{
    return count($this->data);
}
  • 此時,執行PHP檔案,顯示結果:
int(2)
  • 綜上,count 函式不經可以針對陣列使用,而且可以針對物件進行使用,使用時,要實現 Countable 介面。
  • 當你 unset 掉一個陣列單元時,再 count ,此時得到的結果也會是符合預期的,因為底層做了識別和處理。

參考資料

相關文章