重复的数组键(注意:从 __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”)的主要内容,如果未能解决你的问题,请参考以下文章