为什么array_merge_recursive不是递归的?

3

我最近发现我的应用程序中有一个错误,原因是array_merge_recursive的行为出乎意料。让我们看一下这个简单的例子:

$array1 = [
    1 => [
        1 => 100,
        2 => 200,
    ],
    2 => [
        3 => 1000,
    ],
    3 => [
        1 => 500
    ]
];
$array2 = [
    3 => [
        1 => 500
    ]
];
array_merge_recursive($array1, $array2);
//returns: array:4 [ 0 => //...

我原本期望得到一个包含3个元素的数组:键为1、2和3。但是,这个函数返回了一个包含键为0、1、2和3的数组。因此,返回了4个元素,而我只期望了3个。当我将数字替换为它们的字母等价物(a、b、c)时,它返回了一个仅包含3个元素的数组:a、b和c。

$array1 = [
    'a' => [
        1 => 100,
        2 => 200,
    ],
    'b' => [
        3 => 1000,
    ],
    'c' => [
        1 => 500
    ]
];
$array2 = [
    'c' => [
        1 => 500
    ]
];
array_merge_recursive($array1, $array2);
//returns: array:3 [ 'a' => //...

这对我来说是意外行为,但至少它已经被记录:

http://php.net/manual/zh/function.array-merge-recursive.php

如果输入数组有相同的字符串键名,则这些键名的值将合并成一个数组,并且这是递归完成的,因此,如果其中一个值本身就是一个数组,则该函数将把它与另一个数组中的相应条目合并。然而,如果这些数组具有相同的数字键名,则后面的值将不会覆盖原始值,而是附加到后面。

文档没有很清楚地说明“附加”是什么意思。事实证明,具有数字键的$array1元素将被视为索引元素,因此它们将失去当前的键:返回的数组以0开始。当在数组中同时使用数字和字符串键时,这将导致奇怪的结果,但是如果你使用这种不良实践,请不要责怪PHP。在我的情况下,问题通过使用array_replace_recursive得到解决,它完成了预期的操作。(在那个函数中,“replace”表示替换现有的值,否则附加;给函数命名真的很难!)

问题1:是否递归?

但问题并没有结束。我认为array_*_resursive应该是一个递归函数:

递归是一种函数调用,其中函数调用自身。这样的函数也称为递归函数。结构递归是一种问题解决方法,其中问题的解决方案取决于相同问题的较小实例的解决方案。

事实证明它不是。虽然$array1$array2都是关联数组,但上面示例中的$array1['c']$array2['c']都是带有一个元素的索引数组:[1 => 500]。让我们合并它们:

array_merge_recursive($array1['c'], $array2['c']);
//output: array:2 [0 => 500, 1 => 500]

这是预期输出,因为两个数组都有一个数字键(1),所以第二个数组将附加到第一个数组末尾。新��组的第一个键为0。但让我们回到第一个示例:

array_merge_recursive($array1, $array2);
// output:
// array:3 [
//  "a" => array:2 [
//    1 => 100
//    2 => 200
//  ]
//  "b" => array:1 [
//    3 => 1000
//  ]
//  "c" => array:2 [
//    1 => 500 //<-- why not 0 => 500?
//    2 => 500
//  ]
//]

$array2['c'][1]被附加到$array1['c']中,但它的键是1和2。在之前的例子中,不是0和1。处理整数键时,主数组和其子数组的处理方式不同。

问题2:字符串键和整数键有很大的区别。

在编写这个问题时,我发现了另一件事情。当用字符串键替换子数组中的数字键时,情况变得更加混乱:

$array1 = [
    'c' => [
        'a' => 500
    ]
];
$array2 = [
    'c' => [
        'a' => 500
    ]
];
array_merge_recursive($array1, $array2);
// output:
// array:1 [
//  "c" => array:1 [
//    "a" => array:2 [
//      0 => 500
//      1 => 500
//    ]
//  ]
//]

使用字符串键会将 (int) 500 转换为 array(500),而使用整数键则不会。
有人能解释一下这种行为吗?

2
PHP数组以及与之相关的所有函数都不过是香蕉... https://dev59.com/u5_ha4cB1Zd3GeqP7PG1 - philipp
2个回答

1
如果我们退一步观察只有一个数组时array_merge*()函数的行为,就可以看到它如何不同地处理关联数组和索引数组:
$array1 = [
    'k' => [
        1 => 100,
        2 => 200,
    ],
    2 => [
        3 => 1000,
    ],
    'f' => 'gf',
    3 => [
        1 => 500
    ],
    '99' => 'hi',
    5 => 'g'
];

var_dump( array_merge_recursive( $array1 ) );

输出:

array(6) {
  ["k"]=>
  array(2) {
    [1]=>
    int(100)
    [2]=>
    int(200)
  }
  [0]=>
  array(1) {
    [3]=>
    int(1000)
  }
  ["f"]=>
  string(2) "gf"
  [1]=>
  array(1) {
    [1]=>
    int(500)
  }
  [2]=>
  string(2) "hi"
  [3]=>
  string(1) "g"
}

正如您所看到的,它获取了所有数字键并忽略了它们的实际值,并按照遇到的顺序将它们返回给您。我想这个函数有意为之,以在底层C代码中保持清晰(或效率)。
回到您的两个数组示例,它获取了$array1的值,对其进行排序,然后附加了$array2
无论这种行为是否合理都是完全不同的讨论...

你是对的,这就是实际行为。但为什么忽略了 $array 的(数值)键的实际值,而 $array['k']$array[1] 的(同样是数值)键却没有被忽略?这样听起来并不像递归。 - Stephan Vierkant
@StephanVierkant 你说得对,理论上这些子数组应该重新编号...或者主数组不应该重新编号吗? - MonkeyZeus
2
@StephanVierkant PHP 7似乎在array_merge_recursive()方面出了一些问题。我提交了一个错误报告,请参见https://bugs.php.net/bug.php?id=76505 - MonkeyZeus

-1

您应该阅读提供的链接,其中明确说明(重点是我的):

如果输入数组具有相同的字符串键,则这些键的值将合并到数组中,并且这样做是递归的,因此,如果一个值本身就是一个数组,则函数将与另一个数组中的相应条目合并。 但是,如果数组具有相同的数字键,则后面的值不会覆盖原始值,而是将被追加到末尾


1
这并不解释为什么合并具有数字键的两个数组将导致以0开头的数组,但如果其中一个值本身就是一个数组,则原始元素将保留其键。 - Stephan Vierkant
我给你的回答点了踩,因为它并没有回答问题。我的问题中有完全相同的引用,并且强调了几乎相同的词语。 - Stephan Vierkant
@StephanVierkant,那么您应该将您的问题和投诉提交给维护PHP的团队。array_merge_recursive是PHP语言的一部分。SO不是质疑PHP维护者如何实现语言的正确论坛。 - Mr Glass

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接