52-使用雜湊表API

來源:互聯網
上載者:User

標籤:

52-使用雜湊表API

Zend把與HashTable有關的API分成了好幾類以便於我們尋找,這些API的傳回值大多都是常量SUCCESS或者FAILURE。

建立HashTable

下面在介紹函數原型的時候都使用了ht名稱,但是我們在編寫擴充的時候,一定不要使用這個名稱,因為一些PHP宏展開後會聲明這個名稱的變數,進而引發命名衝突。

建立並初始化一個HashTable非常簡單,只要使用zend_hash_init函數即可,它的定義如下:

int zend_hash_init(    HashTable *ht,    uint nSize,    hash_func_t pHashFunction,    dtor_func_t pDestructor,    zend_bool persistent);

*ht是指標,指向一個HashTable,我們即可以&一個已存在的HashTable變數,也可以通過emalloc()、pemalloc()等函數來直接申請一塊記憶體,不過最常用的方法還是用ALLOC_HASHTABLE(ht)宏來讓核心自動的替我們完成這項工作。ALLOC_HASHTABLE(ht)所做的工作相當於ht = emalloc(sizeof(HashTable));

nSize代表著這個HashTable可以擁有的元素的最大數量(HashTable能夠包含任意數量的元素,這個值只是為了提前申請好記憶體,提高效能,省的不停的進行rehash操作)。在我們添加新的元素時,這個值會根據情況決定是否自動成長,有趣的是,這個值永遠都是2的次方,如果你給它的值不是一個2的次方的形式,那它將自動調整成大於它的最小的2的次方值。它的計算方法就像這樣:nSize = pow(2, ceil(log(nSize, 2)));

pHashFunction是早期的Zend Engine中的一個參數,為了相容沒有去掉它,但它已經沒有用處了,所以我們直接賦成NULL就可以了。在原來,它其實是一個鉤子,用來讓使用者自己hook一個散列函數,替換php預設的DJBX33A演算法實現。

pDestructor也代表著一個回呼函數,當我們刪除或者修改HashTable中其中一個元素時候便會調用,它的函數原型必須是這樣的:void method_name(void *pElement);這裡的*pElement是一個指標,指向HashTable中那麼將要被刪除或者修改的那個資料,而資料的類型往往也是個指標。

persistent是最後一個參數,它的含義非常簡單。如果它為true,那麼這個HashTable將永遠存在於記憶體中,而不會在RSHUTDOWN階段自動被登出掉。此時第一個參數ht所指向的地址必須是通過pemalloc()函數申請的。

舉個例子,PHP核心在每個Request請求的頭部都調用了這個函數來初始化symbol_table。

zend_hash_init(&EG(symbol_table), 50, NULL, ZVAL_PTR_DTOR, 0);//#define ZVAL_PTR_DTOR (void (*)(void *)) zval_ptr_dtor_wrapper

因為50不是2的整數冪形式,所以它會在函數執行時被調成成64。

添加與修改

我們有四個常用的函數來完成這項操作,它們的原型分別如下:

int zend_hash_add(    HashTable *ht,      //待操作的ht    char *arKey,            //索引,如"my_key"    uint nKeyLen,       //字串索引的長度,如6    void **pData,       //要插入的資料,注意它是void **類型的。int *p,i=1;p=&i,pData=&p;。    uint nDataSize,    void *pDest         //如果操作成功,則pDest=*pData;);int zend_hash_update(    HashTable *ht,    char *arKey,    uint nKeyLen,    void *pData,    uint nDataSize,    void **pDest);int zend_hash_index_update(    HashTable *ht,    ulong h,    void *pData,    uint nDataSize,    void **pDest);int zend_hash_next_index_insert(    HashTable *ht,    void *pData,    uint nDataSize,    void **pDest);

前兩個函數使用者添加帶字串索引的資料到HashTable中,就像我們在PHP中使用的那樣:$foo[‘bar’] = ‘baz’;用C來完成便是:

zend_hash_add(fooHashTbl, "bar", sizeof("bar"), &barZval, sizeof(zval*), NULL);

zend_hash_add()和zend_hash_update()唯一的區別就是如果這個key已經存在了,那麼zend_hash_add()將返回FAILURE,而不會修改原有資料。

接下來的兩個函數用於像HT中添加數字索引的資料,zend_hash_next_index_insert()函數則不需要索引值參數,而是自己直接計算出下一個數字索引值。

但是如果我們想擷取下一個元素的數字索引值,也是有辦法的,可以使用zend_hash_next_free_element()函數:

ulong nextid = zend_hash_next_free_element(ht);zend_hash_index_update(ht, nextid, &data, sizeof(data), NULL);

所有這些函數中,如果pDest不為NULL,核心便會修改其值為被操作的那個元素的地址。在下面的代碼中這個參數也有同樣的功能。

尋找

因為HashTable中有兩種類型的索引值,所以需要兩個函數來執行find操作。

int zend_hash_find(HashTable *ht, char *arKey, uint nKeyLength,void **pData);int zend_hash_index_find(HashTable *ht, ulong h, void **pData);

第一種就是我們處理PHP語言中字串索引數組時使用的,第二種是我們處理PHP語言中數字索引數組使用的。

void hash_sample(HashTable *ht, sample_data *data1){    sample_data *data2;    ulong targetID = zend_hash_next_free_element(ht);    if (zend_hash_index_update(ht, targetID,            data1, sizeof(sample_data), NULL) == FAILURE) {            /* Should never happen */            return;    }    if(zend_hash_index_find(ht, targetID, (void **)&data2) == FAILURE) {        /* Very unlikely since we just added this element */        return;    }    /* data1 != data2, however *data1 == *data2 */}

除了讀取,我們還需要檢測某個key是否存在:

int zend_hash_exists(HashTable *ht, char *arKey, uint nKeyLen);int zend_hash_index_exists(HashTable *ht, ulong h);

這兩個函數返回SUCCESS或者FAILURE,分別代表著是否存在:

if( zend_hash_exists(EG(active_symbol_table),"foo", sizeof("foo")) == SUCCESS ){    /* $foo is set */}else{    /* $foo does not exist */}
提速
ulong zend_get_hash_value(char *arKey, uint nKeyLen);

當我們需要對同一個字串的key進行許多操作時候,比如先檢測又沒,然後插入,然後修改等等,這時我們便可以使用zend_get_hash_value函數來對我們的操作進行加速!這個函數的傳回值可以和quick系列函數使用,達到加速的目的(就是不再重複計算這個字串的散列值,而直接使用已準備好的)!

int zend_hash_quick_add(    HashTable *ht,    char *arKey,    uint nKeyLen,    ulong hashval,    void *pData,    uint nDataSize,    void **pDest);int zend_hash_quick_update(    HashTable *ht,    char *arKey,    uint nKeyLen,    ulong hashval,    void *pData,    uint nDataSize,    void **pDest);int zend_hash_quick_find(    HashTable *ht,    char *arKey,    uint nKeyLen,    ulong hashval,    void **pData);int zend_hash_quick_exists(    HashTable *ht,    char *arKey,    uint nKeyLen,    ulong hashval);

雖然很意外,但你還是要接受沒有zend_hash_quick_del()這個函數呢。quick類函數會在下面這種場合中用到:

void php_sample_hash_copy(HashTable *hta, HashTable *htb,char *arKey, uint nKeyLen TSRMLS_DC){    ulong hashval = zend_get_hash_value(arKey, nKeyLen);    zval **copyval;    if (zend_hash_quick_find(hta, arKey, nKeyLen,hashval, (void**)?val) == FAILURE)    {        //標明不存在這個索引        return;    }    //這個zval已經被其它的Hashtable使用了,這裡我們進行引用計數操作。    (*copyval)->refcount__gc++;    zend_hash_quick_update(htb, arKey, nKeyLen, hashval,copyval, sizeof(zval*), NULL);}
複製與合并

在PHP語言中,我們經常需要進行數組間的Copy與Merge操作,所以php語言中的數組在C語言中的實現HashTable也肯定會經常碰到這種情況。為了簡化這一類操作,核心中早已準備好了相應的API供我們使用。

void zend_hash_copy(    HashTable *target,    HashTable *source,    copy_ctor_func_t pCopyConstructor,    void *tmp,    uint size);
  • *source中的所有元素都會通過pCopyConstructor函數Copy到*target中去,我們還是以PHP語言中的數組舉例,pCopyConstructor這個hook使得我們可以在copy變數的時候對他們的ref_count進行加一操作。target中原有的與source中索引位置的資料會被替換掉,而其它的元素則會被保留,原封不動。
  • tmp參數是為了相容PHP4.0.3以前版本的,現在賦值為NULL即可。
  • size參數代表每個元素的大小,對於PHP語言中的數組來說,這裡的便是sizeof(zval*)了。

    void zend_hash_merge(
    HashTable *target,
    HashTable *source,
    copy_ctor_func_t pCopyConstructor,
    void *tmp,
    uint size,
    int overwrite
    );

zend_hash_merge()與zend_hash_copy唯一的不同便是多了個int類型的overwrite參數,當其值非0的時候,兩個函數的工作是完全一樣的;如果overwrite參數為0,則zend_hash_merge函數就不會對target中已有索引的值進行替換了。

typedef zend_bool (*merge_checker_func_t)(HashTable *target_ht,void *source_data, zend_hash_key *hash_key, void *pParam);void zend_hash_merge_ex(    HashTable *target,    HashTable *source,    copy_ctor_func_t pCopyConstructor,     uint size,    merge_checker_func_t pMergeSource,    void *pParam);

這個函數又繁瑣了些,與zend_hash_copy相比,其多了兩個參數,多出來的pMergeSoure回呼函數允許我們選擇性的進行merge,而不是全都merge。

遍曆

在PHP語言中,我們有很多方法來遍曆一個數組,對於數組的本質HashTable,我們也有很多辦法來對其進行遍曆操作。首先最簡單的一種辦法便是使用一種與PHP語言中forech語句功能類似的函數——zend_hash_apply,它接收一個回呼函數,並將HashTable的每一個元素都傳遞給它。

typedef int (*apply_func_t)(void *pDest TSRMLS_DC);void zend_hash_apply(HashTable *ht,apply_func_t apply_func TSRMLS_DC);

下面是另外一種遍曆函數:

typedef int (*apply_func_arg_t)(void *pDest,void *argument TSRMLS_DC);void zend_hash_apply_with_argument(HashTable *ht,apply_func_arg_t apply_func, void *data TSRMLS_DC);

通過上面的函數可以在執行遍曆時向回呼函數傳遞任意數量的值,這在一些diy操作中非常有用。

上述函數對傳給它們的回呼函數的傳回值有一個共同的約定,詳細介紹下下表:

回呼函數的傳回值

名稱 備忘
ZEND_HASH_APPLY_KEEP 結束當前請求,進入下一個迴圈。與PHP語言forech語句中的一次迴圈執行完畢或者遇到continue關鍵字的作用一樣。
ZEND_HASH_APPLY_STOP 跳出,與PHP語言forech語句中的break關鍵字的作用一樣。
ZEND_HASH_APPLY_REMOVE 刪除當前的元素,然後繼續處理下一個。相當於在PHP語言中:unset(foo[key]);continue;

我們來一下PHP語言中的forech迴圈:

<?phpforeach($arr as $val) {    echo "The value is: $val\n";}?>

那我們的回呼函數在C語言中應該這樣寫:

int php_sample_print_zval(zval **val TSRMLS_DC){    //重新copy一個zval,防止破壞原資料    zval tmpcopy = **val;    zval_copy_ctor(&tmpcopy);    //轉換為字串    INIT_PZVAL(&tmpcopy);    convert_to_string(&tmpcopy);    //開始輸出    php_printf("The value is: ");    PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));    php_printf("\n");    //毀屍滅跡    zval_dtor(&tmpcopy);    //返回,繼續遍曆下一個~    return ZEND_HASH_APPLY_KEEP;}

遍曆我們的HashTable:

//產生一個名為arrht、元素為zval*類型的HashTablezend_hash_apply(arrht, php_sample_print_zval TSRMLS_CC);

儲存在HashTable中的元素並不是真正的最終變數,而是指向它的一個指標。我們的上面的遍曆函數接收的是一個zval**類型的參數。

typedef int (*apply_func_args_t)(void *pDest,int num_args, va_list args, zend_hash_key *hash_key);void zend_hash_apply_with_arguments(HashTable *ht,apply_func_args_t apply_func, int numargs, ...);

為了能在遍曆時同時接收索引的值,我們必須使用第三種形式的zend_hash_apply!就像PHP語言中這樣的功能:

<?phpforeach($arr as $key => $val){    echo "The value of $key is: $val\n";}?>

為了配合zend_hash_apply_with_arguments()函數,我們需要對我們的遍曆執行函數做一下小小的改動,使其接受索引作為一個參數:

int php_sample_print_zval_and_key(zval **val,int num_args,va_list args,zend_hash_key *hash_key){    //重新copy一個zval,防止破壞原資料    zval tmpcopy = **val;    /* tsrm_ls is needed by output functions */    TSRMLS_FETCH();    zval_copy_ctor(&tmpcopy);    INIT_PZVAL(&tmpcopy);    //轉換為字串    convert_to_string(&tmpcopy);    //執行輸出    php_printf("The value of ");    if (hash_key->nKeyLength)    {        //如果是字串類型的key        PHPWRITE(hash_key->arKey, hash_key->nKeyLength);    }    else    {        //如果是數字類型的key        php_printf("%ld", hash_key->h);    }    php_printf(" is: ");    PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));    php_printf("\n");    //毀屍滅跡    zval_dtor(&tmpcopy);    /* continue; */    return ZEND_HASH_APPLY_KEEP;}

執行遍曆:

zend_hash_apply_with_arguments(arrht,php_sample_print_zval_and_key, 0);

這個函數通過C語言中的可變參數特性來接收參數。This particular example required no arguments to be passed; for information on extracting variable argument lists from va_list args, see the POSIX documentation pages for va_start(), va_arg(), and va_end().

當我們檢查這個hash_key是字串類型還是數字類型時,是通過nKeyLength屬性來檢測的,而不是arKey屬性。這是因為核心有時候會留在arKey屬性裡些髒資料,但nKeyLength屬性是安全的,可以安全的使用。甚至對於Null 字元串索引,它也照樣能處理。比如:$foo[”] =”Bar”;索引的值是NULL字元,但它的長度卻是包括最後這個NULL字元的,所以為1。

向前遍曆HashTable

有時我們希望不用回呼函數也能遍曆一個數組的資料,為了實現這個功能,核心特意的為每個HashTable加了個屬性:The internal pointer(內部指標)。我們還是以PHP語言中的數組舉例,有以下函數來處理它所對應的那個HashTable的內部指標:reset(), key(), current(), next(), prev(), each(), and end()。

<?php    $arr = array(‘a‘=>1, ‘b‘=>2, ‘c‘=>3);    reset($arr);    while (list($key, $val) = each($arr)) {        /* Do something with $key and $val */    }    reset($arr);    $firstkey = key($arr);    $firstval = current($arr);    $bval = next($arr);    $cval = next($arr);?>

ZEND核心中有一組操作HashTable的功能與以上函數功能類似的函數:

/* reset() */void zend_hash_internal_pointer_reset(HashTable *ht);/* key() */int zend_hash_get_current_key(HashTable *ht,char **strIdx, unit *strIdxLen,ulong *numIdx, zend_bool duplicate);/* current() */int zend_hash_get_current_data(HashTable *ht, void **pData);/* next()/each() */int zend_hash_move_forward(HashTable *ht);/* prev() */int zend_hash_move_backwards(HashTable *ht);/* end() */void zend_hash_internal_pointer_end(HashTable *ht);/* 其他的...... */int zend_hash_get_current_key_type(HashTable *ht);int zend_hash_has_more_elements(HashTable *ht);

PHP語言中的next()、prev()、end()函數在移動完指標之後,都通過調用zend_hash_get_current_data()函數來擷取當前所指的元素並返回。而each()雖然和next()很像,卻是使用zend_hash_get_current_key()函數的傳回值來作為它的傳回值。

現在我們用另外一種方法來實現上面的forech:

void php_sample_print_var_hash(HashTable *arrht){    for(        zend_hash_internal_pointer_reset(arrht);        zend_hash_has_more_elements(arrht) == SUCCESS;        zend_hash_move_forward(arrht))    {        char *key;        uint keylen;        ulong idx;        int type;        zval **ppzval, tmpcopy;        type = zend_hash_get_current_key_ex(arrht, &key, &keylen,&idx, 0, NULL);        if (zend_hash_get_current_data(arrht, (void**)&ppzval) == FAILURE)        {            /* Should never actually fail             * since the key is known to exist. */            continue;        }        //重新copy一個zval,防止破壞原資料        tmpcopy = **ppzval;        zval_copy_ctor(&tmpcopy);        INIT_PZVAL(&tmpcopy);        convert_to_string(&tmpcopy);        /* Output */        php_printf("The value of ");        if (type == HASH_KEY_IS_STRING)        {            /* String Key / Associative */            PHPWRITE(key, keylen);        } else {            /* Numeric Key */            php_printf("%ld", idx);        }        php_printf(" is: ");        PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));        php_printf("\n");        /* Toss out old copy */        zval_dtor(&tmpcopy);    }}

上面的代碼你應該都能看懂了,唯一還沒接觸到的可能是zend_hash_get_current_key()函數的傳回值。

在我們遍曆一個HashTable時,一般是很難陷入死迴圈的。

void php_sample_print_var_hash(HashTable *arrht){    HashPosition pos;    for(zend_hash_internal_pointer_reset_ex(arrht, &pos);    zend_hash_has_more_elements_ex(arrht, &pos) == SUCCESS;    zend_hash_move_forward_ex(arrht, &pos)) {        char *key;        uint keylen;        ulong idx;        int type;        zval **ppzval, tmpcopy;        type = zend_hash_get_current_key_ex(arrht,                                &key, &keylen,                                &idx, 0, &pos);        if (zend_hash_get_current_data_ex(arrht,                    (void**)&ppzval, &pos) == FAILURE) {            /* Should never actually fail             * since the key is known to exist. */            continue;        }        /* Duplicate the zval so that         * the original‘s contents are not destroyed */        tmpcopy = **ppzval;        zval_copy_ctor(&tmpcopy);        /* Reset refcount & Convert */        INIT_PZVAL(&tmpcopy);        convert_to_string(&tmpcopy);        /* Output */        php_printf("The value of ");        if (type == HASH_KEY_IS_STRING) {            /* String Key / Associative */            PHPWRITE(key, keylen);        } else {            /* Numeric Key */            php_printf("%ld", idx);        }        php_printf(" is: ");        PHPWRITE(Z_STRVAL(tmpcopy), Z_STRLEN(tmpcopy));        php_printf("\n");        /* Toss out old copy */        zval_dtor(&tmpcopy);    }}<p>With these very slight additions, the HashTable‘s true internal pointer is preserved in whatever state it was initially in on entering the function. When it comes to working with internal pointers of userspace variable HashTables (that is, arrays), this extra step will very likely make the difference between whether the scripter‘s code works as expected.</p><h3>刪除</h3><p>核心中一共預置了四個刪除HashTable元素的函數,頭兩個是使用者刪除某個確定索引的資料:</p><code c>int zend_hash_del(HashTable *ht, char *arKey, uint nKeyLen);int zend_hash_index_del(HashTable *ht, ulong h);

它們兩個分別用來刪除字串索引和數字索引的資料,操作完成後都返回SUCCESS或者FAILURE表示成功or失敗。回顧一下最上面的敘述,當一個元素被刪除時,會啟用HashTable的destructor回呼函數。

void zend_hash_clean(HashTable *ht);void zend_hash_destroy(HashTable *ht);

前者用於將HashTable中的元素全部刪除,而後者是將這個HashTable自身也毀滅掉。

現在讓我們來完整的回顧一下HashTable的建立、添加、刪除操作。

int sample_strvec_handler(int argc, char **argv TSRMLS_DC){    HashTable *ht;    //分配記憶體    ALLOC_HASHTABLE(ht);    //初始化    if (zend_hash_init(ht, argc, NULL,ZVAL_PTR_DTOR, 0) == FAILURE) {        FREE_HASHTABLE(ht);        return FAILURE;    }    //填充資料    while (argc) {        zval *value;        MAKE_STD_ZVAL(value);        ZVAL_STRING(value, argv[argc], 1);        argv++;        if (zend_hash_next_index_insert(ht, (void**)&value,                            sizeof(zval*)) == FAILURE) {            /* Silently skip failed additions */            zval_ptr_dtor(&value);        }    }    //完成工作    process_hashtable(ht);    //毀屍滅跡    zend_hash_destroy(ht);    //釋放ht 為什麼不在destroy裡free呢,求解釋!    FREE_HASHTABLE(ht);    return SUCCESS;}

排序、比較

針對HashTable操作的Zend Api中有很多都需要回呼函數。首先讓我們來處理一下對HashTable中元素大小比較的問題:

typedef int (*compare_func_t)(void *a, void *b TSRMLS_DC);

這很像PHP語言中usort函數需要的函數,它將比較兩個值*a與*b,如果*a>*b,則返回1,相等則返回0,否則返回-1。下面是zend_hash_minmax函數的聲明,它就需要我們上面聲明的那個類型的函數作為回呼函數: int zend_hash_minmax(HashTable *ht, compare_func_t compar,int flag, void **pData TSRMLS_DC); 這個函數的功能我們從它的名稱中便能肯定,它用來比較HashTable中的元素大小。如果flag==0則返回最小值,否則返回最大值!

下面讓我們來利用這個函數來對使用者端定義的所有函數根據函數名找到最大值與最小值(大小寫不敏感~)。

//先定義一個比較函數,作為zend_hash_minmax的回呼函數。int fname_compare(zend_function *a, zend_function *b TSRMLS_DC){    return strcasecmp(a->common.function_name, b->common.function_name);}void php_sample_funcname_sort(TSRMLS_D){    zend_function *fe;    if (zend_hash_minmax(EG(function_table), fname_compare,0, (void **)&fe) == SUCCESS)    {        php_printf("Min function: %s\n", fe->common.function_name);    }    if (zend_hash_minmax(EG(function_table), fname_compare,1, (void **)&fe) == SUCCESS)    {        php_printf("Max function: %s\n", fe->common.function_name);    }}

zend_hash_compare()也許要回呼函數,它的功能是將HashTable看作一個整體與另一個HashTable做比較,如果前者大於後者返回1,相等返回0,否則返回-1。

int zend_hash_compare(HashTable *hta, HashTable *htb,compare_func_t compar, zend_bool ordered TSRMLS_DC);

預設情況下它往往是先判斷各個HashTable元素的個數,個數多的最大! 如果兩者的元素一樣多,然後就比較它們各自的第一個元素。另外一個重要的需要回呼函數的API便是排序函數,它需要的回呼函數形式是這樣的:

typedef void (*sort_func_t)(void **Buckets, size_t numBuckets,size_t sizBucket, compare_func_t comp TSRMLS_DC);

最後一個參數如果為處,則會拋棄HashTable中原有的索引-鍵關係,將對排列號的新值賦予新的數字索引值。PHP語言中的sort函數實現如下:

zend_hash_sort(target_hash, zend_qsort,array_data_compare, 1 TSRMLS_CC);

array_data_compare是一個返回compare_func_t類型資料的函數,它將按照HashTable中zval*值的大小進行排序。

52-使用雜湊表API

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.