PHP原始碼分析-函式array_merge的”BUG”

劉澤奇1990發表於2020-12-04

PHP原始碼分析-函式array_merge的”BUG”


首先來看段程式碼.


<?php
$a = [  '2'  => 'a'  ,  'k'   =>  'g'  ];
$b = [ '6'  =>   'h'   ,  'd'  =>  's'   ];

$c = array_merge( $a , $b );
$d = array_merge( $b , $a );

var_dump( $c , $d );

執行結果

	array(4) {
	[0]=>
	string(1) "a"
	["k"]=>
	string(1) "g"
	[1]=>
	string(1) "h"
	["d"]=>
	string(1) "s"
	}
	array(4) {
	[0]=>
	string(1) "h"
	["d"]=>
	string(1) "s"
	[1]=>
	string(1) "a"
	["k"]=>
	string(1) "g"
	}

可以看到a和h的鍵被重置了而且兩個的結果是不一樣的.也是就說array_merge是有序的,和我們的一般認知是有出入的,而這個更類似於append的操作.
(這裡只考慮數字鍵的重置問題,不考慮這個函式的其他問題)
為了弄清楚這個問題根本的原因還是得去看原始碼.
首先找到array_merge的原始碼實現

PHP_FUNCTION(array_merge)
{
	php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAM_PASSTHRU, 0, 0);
}
static inline void php_array_merge_or_replace_wrapper(INTERNAL_FUNCTION_PARAMETERS, int recursive, int replace) /* {{{ */
{
zval *args = NULL;
	zval *arg;
	int argc, i;

	ZEND_PARSE_PARAMETERS_START(1, -1)
		Z_PARAM_VARIADIC('+', args, argc)
	ZEND_PARSE_PARAMETERS_END();


	if (replace) {
		//省略無關的原始碼
}else{
	//必要的引數校驗和目的陣列初始化
zval *src_entry;
		HashTable *src, *dest;
		uint32_t count = 0;

		for (i = 0; i < argc; i++) {
			zval *arg = args + i;

			if (Z_TYPE_P(arg) != IS_ARRAY) {
				php_error_docref(NULL, E_WARNING, "Expected parameter %d to be an array, %s given", i + 1, zend_zval_type_name(arg));
				RETURN_NULL();
			}
			count += zend_hash_num_elements(Z_ARRVAL_P(arg));
		}

		arg = args;
		src  = Z_ARRVAL_P(arg);
		/* copy first array */
		array_init_size(return_value, count);
		dest = Z_ARRVAL_P(return_value);
if (HT_FLAGS(src) & HASH_FLAG_PACKED) {
//省略無關程式碼
}else{
	//完成目的陣列初始化並開始copy需要合併的陣列
zend_string *string_key;
zend_hash_real_init_mixed(dest);
ZEND_HASH_FOREACH_STR_KEY_VAL(src, string_key, src_entry) {
	if (UNEXPECTED(Z_ISREF_P(src_entry) &&
		Z_REFCOUNT_P(src_entry) == 1)) {
		src_entry = Z_REFVAL_P(src_entry);
	}
	Z_TRY_ADDREF_P(src_entry);
//這裡有區別的了,如果是key是字串會是一種操作key不是字串又是另一種操作,從而造成了數字key的重置.具體過程這裡就不展開說了,主要是和php的hash表在處理數字鍵的機制有關.
	if (EXPECTED(string_key)) {
		zend_hash_append(dest, string_key, src_entry);
	} else {
		zend_hash_next_index_insert_new(dest, src_entry);//這裡
		}
	} ZEND_HASH_FOREACH_END();
}
if (recursive) {
	//省略無關程式碼
} else {
	//重複上邊不包含初始化的過程
	for (i = 1; i < argc; i++) {
		arg = args + i;
		php_array_merge(dest, Z_ARRVAL_P(arg));
	}
}
}
}

關於ZEND_HASH_FOREACH_STR_KEY_VAL和ZEND_HASH_FOREACH_END兩個標記其實就是兩個巨集.
這兩個巨集展開如下

#define ZEND_HASH_FOREACH_STR_KEY_VAL(ht, _key, _val) \
	ZEND_HASH_FOREACH(ht, 0); \
	_key = _p->key; \
	_val = _z;
#define ZEND_HASH_FOREACH(_ht, indirect) do { \
		HashTable *__ht = (_ht); \
		Bucket *_p = __ht->arData; \
		Bucket *_end = _p + __ht->nNumUsed; \
		for (; _p != _end; _p++) { \
			zval *_z = &_p->val; \
			if (indirect && Z_TYPE_P(_z) == IS_INDIRECT) { \
				_z = Z_INDIRECT_P(_z); \
			} \
			if (UNEXPECTED(Z_TYPE_P(_z) == IS_UNDEF)) continue;

#define ZEND_HASH_FOREACH_END() \
		} \
	} while (0)

相關文章