C++基本功之 物件序列化
稍微正式一點的應用都會用到物件的序列化/反序列化操作,常見的需求包括:
物件的持久儲存。比如把一個物件儲存到檔案;在需要的時候,再把物件從檔案中讀出來;
物件的傳遞。比如要把一個物件通過管道,socket等任何手段傳送到對端;
從資料構建物件。物件型別未知,但是我們可以從資料中構建一個物件出來。
我們現在來分析這些需求。看看C++如何有效地序列化/反序列化物件。下文中,凡提到序列化,都包含相應的反序列化。
一個物件的狀態由其所有的內部成員狀態決定。所以序列化物件其實可以等價於序列化其內部物件,遞迴地,其內部物件的序列化又可以歸於構成這些內部物件的序列化。我們可以發現,到了最後,所有的物件構成都歸於基本型別的序列化。
注意,這裡的討論並不適用於任何的資源管理類物件。
首先,我們需要的是最基本的支援,亦即對C++基本型別的序列化和反序列化;同時,由於標準庫中的字串類使用極為廣泛,我們同樣加入支援;最後,我們也加入對非格式的POD資料的支援,這樣,據大多數的序列化需求應該可以得到滿足。以下直接給出實現,其後是簡單的說明,請仔細揣摩。
說明:
1. 使用獨立的名字空間,避免名字衝突;
2. 對於bool型,理論上使用一個bit就可以表達其所有的資訊。但是我們實際使用還是一個位元組,因為這是記憶體操作的最小單位。為了有效地利用額外的資訊,我們這裡加入了對bool的校驗,使用兩個特殊的值;這兩個值可以是任意值,只要不同即可。
3. 實現兩個類,分別完成基本物件,字串及其寬字元版本以及POD資料的序列化和反序列化;
4. 使用成員模板來消除基本物件序列化/反序列話過程中的程式碼重複;
5. 對於I/O操作失敗,我們直接丟擲異常。注意,這不是臨時方案。而是經過仔細設計的序列化/反序列化過程的錯誤處理機制;
6. 構造這些物件需要輸出或輸入流物件,而不僅僅限於檔案;
有了這個基礎,我們就可以設計通用的序列化/反序列化介面了,非常簡單:
那麼,這個介面怎麼使用呢?假設你有如下類,如果實現對其序列化/反序列化的支援呢?
下面就是實現程式碼以及用來測試的程式碼:
注意到了嗎?實現一個物件的序列化/反序列化操作需要:
1. 從介面serializable中繼承
2. 實現相應的介面。對於序列化,按照順序依次把成員寫入流;對於反序列化,則要嚴格按照相反的順序依次讀入
3. 如果某個成員不是基本資料型別,有兩種選擇:a. 實現該成員所屬型別的序列化/反序列化,然後直接呼叫之;b. 直接序列化該成員的子成員。這樣做會嚴重破壞物件的完整性,所以非常不推薦使用。
實現了物件的序列話之後,使用起來就特別容易了。在我們的測試程式碼中,我們把物件序列化為一個字串,然後反序列化之到另一個物件。我們最後得到兩個狀態完全一致的物件。如果我們把該字串通過socket傳到遠方,實際上就是物件的遠端傳遞了。所有流行的遠端物件呼叫都是用類似的基礎機制達成其目的。