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

Posted

技术标签:

【中文标题】重复的数组键(注意:从 __sleep() 多次返回的成员变量“a”)【英文标题】:Duplicate array keys (Notice: member variable "a" returned from __sleep() multiple times) 【发布时间】:2020-03-02 05:08:13 【问题描述】:

标题可能看起来有点傻,但我对此非常认真。今天在工作中,我遇到了一个我无法解释的奇怪的 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 属性有两种不同的可见性(公共、私有)。到目前为止没有什么花哨的。魔法发生在__sleep 方法中,当我们serialize 我们的实例时,它会被神奇地调用。我们希望使用get_object_vars 获得的所有对象变量将其减少为仅使用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 中我无法生成具有两个完全相同键的数组?还是我在这里遗漏了一些明显的东西?

我的同事一开始并不想相信我,但在他们了解这里发生的事情后,他们都没有一个很好的解释。

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

【问题讨论】:

把行改成var_dump(array_keys((array)$this));很有意思 我给出了一个答案,但后来删除了它,因为我现在认为鉴于 PHP 手册的这段摘录“根据范围获取给定对象的可访问非静态属性。”这很简单漏洞。我这样说是因为私有祖先属性 $a 对 B 来说是不可“访问的”。我认为这个结果可能是因为你在 A::__sleep 中引用了 $this ,因此它显示了所有的全部范围,但是有将其移至 B::__sleep,行为保持不变。 【参考方案1】:

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

如果我们处于阴影私有属性可见的范围内,那么阴影公共属性不应该是可见的。

测试代码写得很好,我们可以在这里做一个简单的改动:

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"

回到你的问题:

PHP 怎么会提供一个包含两个完全相同的键的数组?谁能解释这里发生了什么,因为在普通 PHP 中我无法生成具有两个完全相同键的数组?

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

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

结果:

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

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

【讨论】:

【参考方案2】:

稍微搞砸后,看起来这不依赖于__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 与子级中的公共$a 是不同的属性。当您更改B 中的可见性时,您并没有更改A$a 的可见性,您实际上是在创建一个具有相同名称的新属性。如果您 var_dump 对象本身,您可以看到这两个属性。

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

【讨论】:

关联数组(哈希表)不可能处于这种状态。 Accessible 只是其中之一,但大小为 2。 @Weltschmerz 我同意。看起来真的很奇怪。 另外,访问索引a 会返回第二个This is $a from A @AbraCadaver 我也注意到了这一点。我想这部分是有道理的,因为当你用重复键编写数组文字时,你最终会得到最后一个值。【参考方案3】:

我的几美分。

我不了解同事,但我不相信并认为这是一个笑话。

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

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

    我的环境是 php 7.2.12 x86(32 位) - 嗯...是的,真丢人

    我摆脱了“魔法”和序列化,只留下了带来问题的东西。

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

    A类下的$key必须是私有的,否则没有奇迹。

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

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

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

    测试替换 - 在第二个密钥上完成替换,重复停留。

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

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

    转换为 stdClass 对象 - 没门!只接受最后一个密钥!

    测试未设置 - 干得好!最后一把钥匙被移除,但第一把钥匙在掌管,只剩下一把钥匙,没有重复。

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

结果输出在代码下方。

<?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)

【讨论】:

以上是关于重复的数组键(注意:从 __sleep() 多次返回的成员变量“a”)的主要内容,如果未能解决你的问题,请参考以下文章

使用 min 从字典中获取最低键和值 [重复]

js的数组、对象、map、set存储数据

mongoDB中的索引数组/子对象导致重复键错误

从匹配的数组键创建多维数组

具有多次重复的正则表达式

[问答题]__sleep()