重复的数组键(注意:成员变量 "a" 多次从 __sleep() 返回)

Duplicate array keys (Notice: member variable "a" returned from __sleep() multiple times)

标题可能看起来有点傻,但我是认真的。今天在工作中我遇到了一个奇怪的 PHP 行为,我无法解释。幸运的是,此行为已在 PHP 7.4 中得到修复,因此似乎也有人偶然发现了这一点。

我做了一个小例子来说明哪里出了问题:

<?php

class A {
    private $a = 'This is $a from A';

    public $b = 'This is $b from A';

    public function __sleep(): array
    {
        var_dump(array_keys(get_object_vars($this)));

        return [];
    }
}

class B extends A
{
    public $a = 'This is $a from B';
}

$b = new B;

serialize($b);

运行 此处代码:https://3v4l.org/DBt3o

下面是对这里发生的事情的一些解释。我们必须 类 A 和 B 共享一个 属性 $a。细心的读者注意到,属性 $a 有两种不同的可见性(public,private)。到目前为止没有什么特别的。魔法发生在 __sleep 方法中,当我们 serialize 我们的实例时,它被神奇地调用。我们希望使用 get_object_vars 获得的所有 object 变量将其减少为仅使用 array_keys 的键并使用 var_dump.

输出所有内容

我会期待这样的事情(这发生在 PHP 7.4 并且是我的预期输出):

array(2) {
  [0]=>
  string(1) "b"
  [1]=>
  string(1) "a"
}

但我得到的是:

array(3) {
  [0]=>
  string(1) "a"
  [1]=>
  string(1) "b"
  [2]=>
  string(1) "a"
}

PHP 怎么会交付一个包含两个完全相同键的数组?谁能解释一下内部发生了什么,因为在 PHP 中我无法生成具有两个完全相同键的数组?还是我错过了一些明显的东西?

我的同事起初不想相信我,但他们中的 none 在了解这里发生的事情后对此有很好的解释。

我真的很想看到一个好的解释。

稍加修改后,看起来这并不依赖于 __sleep()

显然,在 PHP 7 的早期版本中总是如此(但在 PHP 5 中显然不是)。这个较小的示例显示了相同的行为。

class A {
    private $a = 'This is $a from A';

    public function showProperties() { return get_object_vars($this); }
}

class B extends A
{
    public $a = 'This is $a from B';
}

$b = new B;
var_dump($b->showProperties());

来自 PHP 7.0 - 7.3

的输出
array(2) {
  ["a"]=>
  string(17) "This is $a from B"
  ["a"]=>
  string(17) "This is $a from A"
}

我认为父项中的私有 $a 与子项中的 public $a 不同 属性。当您更改 B 中的可见性时,您并没有更改 A$a 的可见性,您实际上是在创建一个具有相同名称的新 属性。如果您 var_dump 对象本身,您可以看到这两个属性。

虽然它应该不会有太大影响,因为您将无法从子 class 中的父 class 访问私有 属性,即使您可以看到它存在于那些更早的 PHP 7 版本中。

我找不到问题中错误的报告,但有趣的是 this commit 似乎解决了同样的问题:

If we are in a scope where the shadowed private property is visible, the shadowing public property should not be visible.

测试代码写的很好,稍微改一下就可以了:

class Test
{
    private $prop = "Test";

    function run()
    {
        return get_object_vars($this);
    }
}

class Test2 extends Test
{
    public $prop = "Test2";
}

$props = (new Test2)->run();

$props 上调用 var_dump() 显示:

array(2) {
  ["prop"]=>
  string(5) "Test2"
  ["prop"]=>
  string(4) "Test"
}

回到你的问题:

How could it be, that PHP delivers an array with two completely identical keys? Who is able to explain what happens here internally because in plain PHP I'm not able to generate an array with two completely identical keys?

是的,您不能拥有包含两个相同键的数组:

var_dump(array_flip(array_flip($props)));

结果:

array(1) {
  ["prop"]=>
  string(4) "Test"
}

但我不同意你 two completely identical keys 的观点,因为这两个具有相同键名的元素在哈希表内部并未存储相同的键。也就是说,除了潜在的冲突之外,它们被存储为唯一的整数,并且由于这种情况在内部发生,因此忽略了对用户输入的限制。

我的情侣美分。

我不知道同事的情况,但我不相信,认为这是一个笑话。

为了解释 - 问题肯定在 "get_object_vars" 变量下,因为它返回重复的关联数组。应该是同一个键的两个不同的散列 table 值(这是不可能的,但唯一的解释来了)。我找不到任何指向内部 get_object_vars() 实现的链接(尽管 PHP 基于开源,因此可以通过某种方式获取代码和调试)。此外,我正在考虑(到目前为止未成功)查看内存中的数组表示形式,包括哈希 table。另一方面,我能够使用 PHP "legal" 函数并对数组做一些技巧。

这是我尝试使用该关联数组测试某些功能的尝试。下面是输出。无需解释 - 您可以看到所有内容并自己尝试相同的代码,所以只有一些评论。

  1. 我的环境是 php 7.2.12 x86(32 位)- 好吧...是的,真丢脸

  2. 我摆脱了 "magic" 和序列化,只剩下带来问题的东西。

  3. 完成了对 classes A 和 B 以及函数调用的一些重构。

  4. classA下的$key必须是private的,否则不出奇迹

  5. 部分测试变量 - 除了主要问题外没什么有趣的。

  6. 部分测试copy_vars - 数组被复制了!已成功添加新密钥。

  7. 部分测试迭代和 new_vars - 迭代通过重复没有问题,但新数组不接受重复,接受最后一个键。

  8. 测试替换 - 在第二个键上完成替换,重复保留。

  9. 正在测试 ksort - 数组未更改,无法识别重复项

  10. 测试分类 - 在更改值和 运行 分类后,我能够更改顺序并交换重复键。现在第一个键变成第二个,新键是我们按键调用数组或分配键时的键。结果我能够更改两个键!之前我认为重复键是一种不可见的键,现在很明显当我们引用或分配键时最后一个键起作用。

  11. 转换为 stdClass 对象 - 不可能!只接受了最后一个密钥!

  12. 正在测试未设置 - 干得好!删除了最后一把钥匙,但第一把钥匙负责并且只剩下一把钥匙,没有重复。

  13. 内部表示测试-这是一个添加一些其他功能并查看重复来源的主题。我现在正在考虑。

结果输出在代码下方

<?php

class A {
    private $key = 'This is $a from A';

    protected function funcA() {
        $vars = get_object_vars($this);

        return $vars;
    }
}

class B extends A
{
    public $key = 'This is $a from B';

    public function funcB() {
        return $this->funcA();
    }
}

$b = new B();

$vars = $b->funcB();

echo "testing vars:\n\n\n";

var_dump($vars);
var_dump($vars['key']);
var_dump(array_keys($vars));

echo "\n\n\ntesting copy_vars:\n\n\n";

$copy_vars = $vars;
$copy_vars['new_key'] = 'this is a new key';

var_dump($vars);
var_dump($copy_vars);

echo "\n\n\ntesting iteration and new_vars:\n\n\n";

$new_vars = [];
foreach($vars as $key => $val) {
    echo "adding '$key', '$val'\n";
    $new_vars[$key] = $val;
}

var_dump($new_vars);

echo "\n\n\ntesting replace key (for copy):\n\n\n";

var_dump($copy_vars);
$copy_vars['key'] = 'new key';
var_dump($copy_vars);

echo "\n\n\ntesting key sort (for copy):\n\n\n";

var_dump($copy_vars);
ksort($copy_vars);
var_dump($copy_vars);

echo "\n\n\ntesting asort (for copy):\n\n\n";

$copy_vars['key'] = "A - first";
var_dump($copy_vars);
asort($copy_vars);
var_dump($copy_vars);
$copy_vars['key'] = "Z - last";
var_dump($copy_vars);

echo "\n\n\ntesting object conversion (for copy):\n\n\n";

var_dump($copy_vars);
$object = json_decode(json_encode($copy_vars), FALSE);
var_dump($object);


echo "\n\n\ntesting unset (for copy):\n\n\n";

var_dump($copy_vars);
unset($copy_vars['key']);
var_dump($copy_vars);


echo "\n\n\ntesting inernal representation:\n\n\n";

debug_zval_dump($vars);

现在输出:

testing vars:


array(2) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(17) "This is $a from A"
}
string(17) "This is $a from A"
array(2) {
  [0]=>
  string(3) "key"
  [1]=>
  string(3) "key"
}



testing copy_vars:


array(2) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(17) "This is $a from A"
}
array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(17) "This is $a from A"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing iteration and new_vars:


adding 'key', 'This is $a from B'
adding 'key', 'This is $a from A'
array(1) {
  ["key"]=>
  string(17) "This is $a from A"
}



testing replace key (for copy):


array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(17) "This is $a from A"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(7) "new key"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing key sort (for copy):


array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(7) "new key"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(7) "new key"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing asort (for copy):


array(3) {
  ["key"]=>
  string(17) "This is $a from B"
  ["key"]=>
  string(9) "A - first"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(17) "This is $a from B"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing object conversion (for copy):


array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}
object(stdClass)#2 (2) {
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing unset (for copy):


array(3) {
  ["key"]=>
  string(9) "A - first"
  ["key"]=>
  string(8) "Z - last"
  ["new_key"]=>
  string(17) "this is a new key"
}
array(2) {
  ["key"]=>
  string(9) "A - first"
  ["new_key"]=>
  string(17) "this is a new key"
}



testing inernal representation:


array(2) refcount(2){
  ["key"]=>
  string(17) "This is $a from B" refcount(2)
  ["key"]=>
  string(17) "This is $a from A" refcount(4)
}