对象引用和指针引用
在 PHP 中,标量类型数据是值传递的,而复合类型数据(对象和数组)是引用传递的。但是复合类型数据的引用传递和用 & 符号明确指定的引用传递是有区别的,前者的引用传递是对象引用,而后者是指针引用。
在解释对象引用和指针引用之前,几个例子。
";
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
$b = new SampleClass();
$b->value = &$b;
echo serialize($a);
echo "\n";
echo serialize($b);
echo "\n";
echo "";
?>
这个例子的输出结果是这样:
O:11:"SampleClass":1:{s:5:"value";r:1;}
O:11:"SampleClass":1:{s:5:"value";R:1;}
这里变量 $a 的 value 字段的值被序列化成了 r:1,而 $b 的 value 字段的值被序列化成了 R:1。
但是对象引用和指针引用到底有什么区别?
下面:
echo "
";";
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a;
$b = new SampleClass();
$b->value = &$b;
$a->value = 1;
$b->value = 1;
var_dump($a);
var_dump($b);
echo "
运行结果出乎预料:
object(SampleClass)#1 (1) {
["value"]=>
int(1)
}
int(1)
改变 $a->value 的值仅仅是改变了 $a->value 的值,而改变 $b->value 的值却改变了 $b 本身,这就是对象引用和指针引用的区别。
不过很不幸的是,PHP 对数组的序列化犯了一个错误,虽然数组本身在传递时也是对象引用传递,但是在序列化时,PHP 似乎忘记了这一点,看下:
echo "
";";
$a = array();
$a[1] = 1;
$a["value"] = $a;
echo $a["value"]["value"][1];
echo "\n";
$a = unserialize(serialize($a));
echo $a["value"]["value"][1];
echo "
结果是:
1
将原数组序列化再反序列化后,数组结构变了。原本 $a["value"]["value"][1] 中的值 1,在反序列化之后丢失了。
原因是什么呢?输出序列化之后的结果看一看:
$a = array();
$a[1] = 1;
$a["value"] = $a;
echo serialize($a);
结果是:
a:2:{i:1;i:1;s:5:"value";a:2:{i:1;i:1;s:5:"value";N;}}
原来,序列化之后,$a["value"]["value"] 变成了 NULL,而不是一个对象引用。
也就是说,PHP 只对对象在序列化时才会生成对象引用标示(r)。对所有的标量类型和数组(也包括 NULL)序列化时都不会生成对象引用。但是如果明确使用了 & 符号作的引用,在序列化时,会被序列化为指针引用标示(R)。
引用标示后的数字在上面的例子中大家可能已经看到了,对象引用(r)和指针引用(R)的格式为:
r:;
R:;
这个 是什么
这个 简单的说,就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类型)的位置。
举例说明一下:
class ClassA {
var $int;
var $str;
var $bool;
var $obj;
var $pr;
}
$a = new ClassA();
$a->int = 1;
$a->str = "Hello";
$a->bool = false;
$a->obj = $a;
$a->pr = &$a->str;
echo serialize($a);
这个例子的结果是:
O:6:"ClassA":5:{s:3:"int";i:1;s:3:"str";s:5:"Hello";s:4:"bool";b:0;s:3:"obj";r:1;s:2:"pr";R:3;}
在这个例子中,首先序列化的对象是 ClassA 的一个对象,那么给它编号为 1,接下来要序列化的是这个对象的几个成员,第一个被序列化的成员是 int 字段,那它的编号就为 2,接下来被序列化的成员是 str,那它的编号就是 3,依此类推,到了 obj 成员时,它发现该成员已经被序列化了,并且编号为 1,因此它被序列化时,就被序列化成了 r:1; ,在接下来被序列化的是 pr 成员,它发现该成员实际上是指向 str 成员的一个引用,而 str 成员的编号为 3,因此,pr 就被序列化为 R:3; 了。
PHP 是如何来编号被序列化的对象的呢?实际上,PHP 在序列化时,首先建立一个空表,然后每个被序列化的对象在被序列化之前,都需要先计算该对象的 Hash 值,然后判断该 Hash 值是否已经出现在该表中了,如果没有出现,就把该 Hash 值添加到这个表的最后,返回添加成功。如果出现了,则返回添加失败,但是在返回失败前先判断该对象是否是一个引用(用 & 符号定义的引用),如果不是则也把 Hash 值添加到表后(尽管返回的是添加失败)。如果返回失败,则同时返回上一次出现的位置。
在添加 Hash 值到表中之后,如果添加失败,则判断添加的是一个引用还是一个对象,如果是引用,则返回 R 标示,如果是对象,则返回 r 标示。因为失败时,会同时返回上一次出现的位置,因此,R 和 r 标示后面的数字,就是这个位置。
对象引用的反序列化PHP 在反序列化处理对象引用时很有意思,如果反序列化的字符串不是 PHP 的 serialize() 本身生成的,而是人为构造或者用其它语言生成的,即使对象引用指向的不是一个对象,它也能正确地按照对象引用所指向的数据进行反序列化。例如:
echo "
";";
class StrClass {
var $a;
var $b;
}
$a = unserialize('O:8:"StrClass":2:{s:1:"a";s:5:"Hello";s:1:"b";r:2;}');
var_dump($a);
echo "
运行结果:
object(StrClass)#1 (2) {
["a"]=>
string(5) "Hello"
["b"]=>
string(5) "Hello"
}
上面例子反序列化后,$a->b 的值与 $a->a 的值是一样的,尽管 $a->a 不是一个对象,而是一个字符串。因此如果用其它语言来实现序列化的话,不一定非要把 string 作为标量类型来处理,即使按照对象引用来序列化拥有相同字符串内容的复合类型,用 PHP 同样可以正确的反序列化。这样可以更节省序列化后的内容所占用的空间。