5.嵌套复合类型的序列化
上一章讨论了简单的复合类型的序列化,大家会发现对于简单的数组和对象其实也很容易。但是如果遇到自己包含自己或者 A 包含 B,B 又包含 A 这类的对象或数组时,PHP 又该如何序列化这种对象和数组呢?本章我们就来讨论这种情况下的序列化形式。
5.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>
"
;
结果是:
大家会发现,将原数组序列化再反序列化后,数组结构变了。原本 $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)。
5.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
)
;
这个例子的结果是:
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 标示后面的数字,就是这个位置。
5.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 同样可以正确的反序列化。这样可以更节省序列化后的内容所占用的空间。
6.自定义对象序列化
6.1.PHP 4 中自定义对象序列化
PHP 4 中提供了 __sleep 和 __wakeup 这两个方法来自定义对象的序列化。不过这两个函数并不改变对象序列化的格式,影响的仅仅是被序列化字段的个数。关于它们的介绍,在 PHP 手册中写的还算比较详细。这里就不再多做介绍了。
6.2.PHP 5 中自定义对象序列化
PHP 5 中增加了接口(interface)功能。PHP 5 本身提供了一个 Serializable 接口,如果用户在自己定义的类中实现了这个接口,那么在该类的对象序列化时,就会被按照用户实现的方式去进行序列化,并且序列化后的标示不再是 O,而改为 C。C 标示的格式如下:
C
:<
name
length
>:
"
<class name>
"
:<
data
length
>:
{
<
data
>
}
其中 <name length> 表示类名 <class name> 的长度,<data length> 表示自定义序列化数据 <data> 的长度,而自定义的序列化数据 <data> 是完全的用户自己定义的格式,与 PHP 序列化格式可以完全无关,这部分数据由用户自己实现的序列化和反序列化接口方法来管理。
Serializable 接口中定义了 2 个方法,serialize() 和 unserialize($data),这两个方法不会被直接调用,而是在调用 PHP 序列化函数时,被自动调用。其中 serialize 函数没有参数,它的返回值就是 <data> 的内容。而 unserialize($data) 有一个参数 $data,这个参数的值就是 <data> 的内容。这样大家应该就明白了,实际上接口中 serialize 方法就是让用户来自己序列化对象中的内容,序列化后的内容格式,PHP 并不关心,PHP 只负责把它充填到 <data> 中,等到反序列化时,PHP 只负责取出这部分内容,然后传给用户实现的 unserialize($data) 接口方法,让用户自己去反序列化这部分内容。
下面举个简单的例子,来说明 Serializable 接口的使用:
class
MyClass
implements
Serializable
{
public
$member
;
function
MyClass
()
{
$this
->
member
=
'
member value
'
;
}
public
function
serialize
()
{
return
wddx_serialize_value
(
$this
->
member
)
;
}
public
function
unserialize
(
$data
)
{
$this
->
member
=
wddx_deserialize
(
$data
)
;
}
}
$a
=
new
MyClass
()
;
echo
serialize
(
$a
)
;
echo
"
\n
"
;
print_r
(
unserialize
(
serialize
(
$a
)))
;
输出结果为(浏览器中的源代码):
C
:
7
:
"
MyClass
"
:
90
:
{
<
wddxPacket
version
=
'
1.0
'
><
header
/><
data
><
string
>
member
value
</
string
></
data
></
wddxPacket
>
}
MyClass
Object
(
[
member
]
=>
member
value
)
因此如果想用其它语言来实现 PHP 序列化中的 C 标示的话,也需要提供一种这样的机制,让用户自定义类时,能够自己在反序列化时处理 <data> 内容,否则,这些内容就无法被反序列化了。
7.Unicode 字符串的序列化
好了,最后再谈谈 PHP 6 中关于 Unicode 字符串序列化的问题吧。
说实话,我不怎么喜欢把字符串搞成双字节 Unicode 这种编码的东西。JavaScript 中也是用这样的字符串,因此在处理字节流的东西时,反而非常的不方便。C# 虽然也是用这种方式来编码字符串,不过还好的是,它提供了全面的编码转换机制,而且提供这种字符串到字节流(实际上是到字节数组)的转换,所以处理起来还算是可以。但是对于不熟悉这个的人来说,转来转去就是个麻烦。
PHP 6 之前一直是按字节来编码字符串的,到了 PHP 6 突然冒出个 Unicode 编码的字符串来,虽然是可选的,但仍然让人觉得非常不舒服,如果配置不当,老的程序兼容性都成问题。
当然加了这个东西以后,许多老的与字符串有关的函数都进行了修改。序列化函数也不例外。因此,PHP 6 中增加了专门的 Unicode 字符串序列化标示 U。PHP 6 中对 Unicode 字符串的序列化格式如下:
U
:<
length
>:
"
<unicode string>
"
;
这里 <length> 是指原 Unicode String 的长度,而不是 <unicode string> 的长度,因为 <unicode string> 是经过编码以后的字节流了。
但是还有一点要注意,<length> 尽管是原 Unicode String 的长度,但是也不是只它的字节数,当然也不完全是指它的字符数,确切的说是之它的字符单位数。因为 Unicode String 中采用的是 UTF16 编码,这种编码方式使用 16 位来表示一个字符的,但是并不是所有的都是可以用 16 位表示的,因此有些字符需要两个 16 位来表示一个字符。因此,在 UTF16 编码中,16 位字符算作一个字符单位,一个实际的字符可能就是一个字符单位,也有可能由两个字符单位组成。因此, Unicode String 中字符数并不总是等于字符单位数,而这里的 <length> 指的就是字符单位数,而不是字符数。
那 <unicode string> 又是怎样被编码的呢?实际上,它的编码也很简单,对于编码小于 128 的字符(但不包括 \),按照单个字节写入,对于大于 128 的字符和 \ 字符,则转化为 16 进制编码的字符串,以 \ 作为开头,后面四个字节分别是这个字符单位的 16 进制编码,顺序按照由高位到低位排列,也就是第 16-13 位所对应的16进制数字字符(abcdef 这几个字母是小写)作为第一个字节,第 12-9 位作为第二个字节,第 8-5 位作为第三个字节,最后的第 4-1 位作为第四个字节。依次编码下来,得到的就是 <uncode string> 的内容了。
我认为对于其他语言来说,没有必要实现这种序列化方式,因为用这种方式序列化的内容,对于目前的主流 PHP 服务器来说都是不支持的,不过倒是可以实现它的反序列化,这样将来即使跟 PHP 6 进行数据交换,也可以互相读懂了。