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)的格式爲:
大家一定很奇怪後面這個 <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 進行數據交換,也可以互相讀懂了。