PHP serialize 序列化之嵌套复合类型的序列化,包括对象引用和指针引用、对象引用和指针引用的区别、引用标示后的数字、对象引用的反序列化。
嵌套复合类型的序列化
前面讨论了简单的复合类型的序列化,但是如果遇到自己包含自己或者 A 包含 B,B 又包含 A 这类的对象或数组时,PHP 又该如何序列化这种对象和数组呢?
1、对象引用和指针引用
在 PHP 中,标量类型数据是值传递的,而复合类型数据(对象和数组)是引用传递的。但是复合类型数据的引用传递和用 & 符号明确指定的引用传递是有区别的,前者的引用传递是对象引用,而后者是指针引用。
在解释对象引用和指针引用之前,先让我们看几个例子。
以下为引用内容:
<?php
echo "<pre>";
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 "</pre>";
?>
这个例子的输出结果是这样的:
以下为引用内容:
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 "<pre>";
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 "</pre>";
大家会发现,运行结果也许出乎你的预料:
以下为引用内容:
object(SampleClass)#1 (1) {
["value"]=>
int(1)
}
int(1)
改变 $a->value 的值仅仅是改变了 $a->value 的值,而改变 $b->value 的值却改变了 $b 本身,这就是对象引用和指针引用的区别。
不过很不幸的是,PHP 对数组的序列化犯了一个错误,虽然数组本身在传递时也是对象引用传递,但是在序列化时,PHP 似乎忘记了这一点,看下面的例子:
以下为引用内容:
echo "<pre>";
$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 "</pre>";
结果是:
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)。
[page]
2、引用标示后的数字
在上面的例子中大家可能已经看到了,对象引用(r)和指针引用(R)的格式为:
以下为引用内容:
r:<number>;
R:<number>;
大家一定很奇怪后面这个 <number> 是什么吧?
这个 <number> 简单的说,就是所引用的对象在序列化串中第一次出现的位置,但是这个位置不是指字符的位置,而是指对象(这里的对象是泛指所有类型的量,而不仅限于对象类型)的位置。
我想大家可能还不是很明白,那么我来举例说明一下:
以下为引用内容:
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);
这个例子的结果是:站长教学网 eduyo.com
以下为引用内容:
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 标示后面的数字,就是这个位置。
[page]
3、对象引用的反序列化
PHP 在反序列化处理对象引用时很有意思,如果反序列化的字符串不是 PHP 的 serialize() 本身生成的,而是人为构造或者用其它语言生成的,即使对象引用指向的不是一个对象,它也能正确地按照对象引用所指向的数据进行反序列化。例如:
以下为引用内容:
echo "<pre>";
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 "</pre>";
运行结果:
以下为引用内容:
object(StrClass)#1 (2) {
["a"]=>
string(5) "Hello"
["b"]=>
string(5) "Hello"
}
大家会发现,上面的例子反序列化后,$a->b 的值与 $a->a 的值是一样的,尽管 $a->a 不是一个对象,而是一个字符串。因此如果大家用其它语言来实现序列化的话,不一定非要把 string 作为标量类型来处理,即使按照对象引用来序列化拥有相同字符串内容的复合类型,用 PHP 同样可以正确的反序列化。这样可以更节省序列化后的内容所占用的空间。
(责任编辑:ken)