# 原則27:總是使你的類型可序列化
**By D.S.Qiu**
**尊重他人的勞動,支持原創,轉載請注明出處:[http://dsqiu.iteye.com](http://dsqiu.iteye.com)**
持久化是類型的核心特征。沒有人會注意到除非你沒有支持它。如果你的類型沒有支持恰當支持序列化,你就會給想要使用你的類作為成員或基類的開發者增加工作。當你的類沒有支持序列化,它們必須圍著它添加自己對這個標準特征的實現。當你的類有不能訪問的 private 細節時,就不可能正確的實現序列化。因此,如果你不提供序列化機制,使用者是很難甚至是不可能實現這個機制。
相反,實踐中要為你的類添加序列化。對于那些除了不展示 UI 組件,窗口或表單的所有類是很有實踐意義的。感覺到有額外工作量是沒理由的。.NET 序列化支持是那么簡單的讓你沒有任何理由去支持它。大多數情況下,添加 Serializable 特性就足夠了:
```
[Serializable]
public class MyType
{
private string label;
private int value;
}
```
添加 Serializable 特性就可以工作是因為這個類的所有成員都是可序列化的:string 和 int 都支持 .NET 序列化。這就是為什么都要支持序列化的原因,當你添加自定義類的域是就會變得更加明顯:
```
[Serializable]
public class MyType
{
private string label;
private int value;
private OtherClass otherThing;
}
```
這里的 Serializable 特性只有當 OtherClass 類支持 .NET 序列化才有效。如果 OtherClass 不可序列化,會報一個運行時錯誤,所以你就不得不自己寫代碼序列化 MyType 和內部的 OtherClass 對象。如果沒有掌握 OtherClass 內部定義是不可能。
.NET 序列化會將對象的所有成員保存到輸出流中。此外,.NET 序列化支持任意的對象關系圖:即使對象有環引用,序列化和反序列化方法都只會保存和存儲每個實際對象一次。當 web 對象反序列化時, .NET 的序列化框架同時也會重創建 web 對象的引用。當對象關系圖被反序列化,任何 web 相關的對象都會被正確創建。最后重要一點是 Serializable 特性同時支持二進制和 SOAP 序列化。這個原則里介紹所有技術都支持這兩種序列化機制。但是記住只有對象關系圖中所有類型都支持序列化機制才有用。那就是為什么在所有類型支持序列化重要的原因。一旦你遺漏一個,你就在對象關系圖中留了一個孔,就使得使用你的類的其他人很難輕易支持序列化。不久以后,每個人都會發現不得不自己寫實現序列化的代碼。
添加 Serializable 特性是序列化對象的最簡單技術。但是最簡單的解決方法不總是最合適的解決方案。有時,你希望序列化對象的所有成員:有些成員可能只會存在長期操作的緩存中。其他成員可能持有只會在內存操作的運行時資源。你同樣可以使用特性可以管理所有可能。添加 [NonSerialized] 特性給任意數據成員就不會作為對象狀態的一部分保存。這就它們變得不可序列化:
```
[Serializable]
public class MyType
{
private string label;
[NonSerialized]
private int cachedValue;
private OtherClass otherThing;
}
```
不可序列化成員會增加你一點工作量。序列化 API 在反序列化時是不會初始化不可序列化成員。任何構造函數會被調用,成員變量的初始化也不會被執行。當你使用序列化特性,不可序列化成員會獲得系統默認初始化值:0或 null 。如果默認0初始化不正確,你需要實現 IDeserializationCallBack 接口來初始化這些不可序列化成員。 IDeserializationCallBack 包含一個方法: ONDeserialization 。框架當整個對象關系圖都反序列化之后會調用這個方法。 你使用這個方法初始化對象的不可序列化成員。因為整個關系圖已經被讀取,你調用類型上任何函數或序列化成員是安全的。不幸的是,它不是萬無一失的。在整個對象關系圖都讀取之后,框架會調用每個實現了 IDeserializationCallBack 對象的 OnDeserialization 方法。在執行 OnDeserialization 可以調用關系圖中的任何其他對象的 public 成員。如果它們先執行了,你對象的非序列化成員會是 null 或0。調用順序是無法保證的,所以你必須保證所有 public 方法都要處理不可序列化成員沒有被初始化的情況。
到目前為止,你已經知道為什么要對所有類型添加序列化:非不可序列化類型在序列化類型中使用會帶來更多工作。你已經學會使用特性的最簡單序列化方法,包括怎么初始化不可序列化成員。
序列化數據會在成員的不同版本存在。給你的類型加上序列化意味著將來你需要讀取舊的版本。反序列化時如果發現類有域被添加或移除,就會拋出異常。當你需要支持多個版本你就需要更多控制序列化進程,實現 ISerializable 接口。這接口定義一些 hook 用于自己定義類的序列化機制。 ISerializable 接口的方法和存儲和默認序列化的方法和存儲是一致的。這說明當你創建類時可以繼續使用序列化特性。如果你覺得需要提供自己的擴展時,你可以添加 ISerializable 接口的支持。
例如,考慮你如何來支持 MyType 的第2個版本,也就是添加了另一個域到類中時。簡單的添加一個域都會產生一個新的類型,而這與先前已經存在磁盤上的版本是不兼容的:
```
[Serializable]
public class MyType
{
private MyType(SerializationInfo info, StreamingContext cntxt)
{
}
private string label;
[NonSerialized]
private int value;
private OtherClass otherThing;
// Added in version 2
// The runtime throws Exceptions
// with it finds this field missing in version 1.0
// files.
private int value2;
}
```
你可以添加 ISerializable 來解決這個行為。 ISerializable 接口定義一個方法,但是你不得不實現兩個。 ISerializable 定義 GetObjectData() 方法寫數據到流中。此外,你必須提供一個序列化構造器從流中 初始化對象:
```
private MyType(SerializationInfo info, StreamingContext cntxt)
```
下面的序列化構造函數演示了如何從先前的版本中讀取數據,以及讀取當前版本中的數據和默認添加的Serializable特性生成的序列化保持一致:
```
using global::System.Runtime.Serialization;
using global::System.Security.Permissions;
[Serializable]
public sealed class MyType : ISerializable
{
private string label;
[NonSerialized]
private int value;
private OtherClass otherThing;
private const int DEFAULT_VALUE = 5;
private int value2;
// public constructors elided.
// Private constructor used only
// by the Serialization framework.
private MyType(SerializationInfo info, StreamingContext cntxt)
{
label = info.GetString("label");
otherThing = (OtherClass)info.GetValue("otherThing",typeof(OtherClass));
try
{
value2 = info.GetInt32("value2");
}
catch (SerializationException)
{
// Found version 1\.
value2 = DEFAULT_VALUE;
}
}
[SecurityPermissionAttribute(SecurityAction.Demand, SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo inf,StreamingContext cxt)
{
inf.AddValue("label", label);
inf.AddValue("otherThing", otherThing);
inf.AddValue("value2", value2);
}
}
```
序列化流把每項當做 key/value 對存儲。默認特性生成的代碼是使用變量名作為鍵值存儲值。當你添加 ISerializable 接口,你必須匹配鍵名和變量的順序。這個順序就是類中聲明它們的順序。(順便說下,這個實際說明類中變量聲明順序變了或者重命名了就破壞了已經存儲的序列化文件的兼容性。)
同時,我已經要求 SerializationFormatter 的安全許可。如果沒有恰當的保護, GetObjectData 可能是一個安全漏洞。惡意代碼可以創建一個 StreamingContext ,并且使用 GetObjectData 獲得對象中的值,或者續斷修改 SerializationInfo 的版本,或者重寫組織修改的對象。這可能運行惡意開發者訪問你對象的內部狀態,在流中修改,并且返回給你。要求 SerializationFormatter 安全許可進而就封閉了這個潛在的漏洞。這就確保只有被信任的代碼才能訪問對象的內部狀態。
但是實現 ISerializable 接口有一個弊端。你可以看到,我之前使得 MyType 為 sealed 。就強制它是一個葉節點類。在基類實現 ISerializable 接口就要復雜到考慮所有子類。實現 ISerializable 意味著每個子類都必須創建一個 protected 構造方法用于反序列化。另外,為了支持非封閉類,你需要在 GetObjectData 方法中創建 hook ,讓子類可以添加自己的數據到流中。編譯器不會捕獲出現的錯誤。當從流中讀取子類時,如果沒有恰當的構造函數會拋出運行時異常。缺少 GetObjectData() 的鉤子意味著子類派生的數據不會被保存到文件中。沒有錯誤拋出。我很想建議地說“在葉節點實現實現可序列化”。但我沒有那樣說因為那不能正常工作。你的基類必須為子類實現可序列化。修改 MyType 使得它成為一個可序列化基類,莫修改序列化構造函數為 protected 并且創建一個虛方法子類可以重載自己的版本存儲數據:
```
[Serializable]
public class MyType : ISerializable
{
private string label;
[NonSerialized]
private int value;
private OtherClass otherThing;
private const int DEFAULT_VALUE = 5;
private int value2;
// public constructors elided.
// Protected constructor used only by the
// Serialization framework.
protected MyType(SerializationInfo info, StreamingContext cntxt)
{
label = info.GetString("label");
otherThing = (OtherClass)info.GetValue("otherThing",typeof(OtherClass));
try
{
value2 = info.GetInt32("value2");
}
catch (SerializationException e)
{
// Found version 1\.
value2 = DEFAULT_VALUE;
}
}
[SecurityPermissionAttribute(SecurityAction.Demand,SerializationFormatter = true)]
void ISerializable.GetObjectData(SerializationInfo inf, StreamingContext cxt)
{
inf.AddValue("label", label);
inf.AddValue("otherThing", otherThing);
inf.AddValue("value2", value2);
WriteObjectData(inf, cxt);
}
// Overridden in derived classes to write
// derived class data:
protected virtual void
WriteObjectData(SerializationInfo inf,StreamingContext cxt)
{
// Should be an abstract method,
// if MyType should be an abstract class.
}
}
```
子類可以提供自己的序列化構造函數和重載 WriteObjectData 方法:
```
public class DerivedType : MyType
{
private int derivedVal;
private DerivedType(SerializationInfo info,
StreamingContext cntxt) : base(info, cntxt)
{
derivedVal = info.GetInt32("_DerivedVal");
}
protected override void WriteObjectData(SerializationInfo inf, StreamingContext cxt)
{
inf.AddValue("_DerivedVal", derivedVal);
}
}
```
從序列化流中寫入和檢索值的順序必須保持一致。我首先對基類的值進行讀和寫因為我相信它是最簡單的。如果你不按照繼承關系中正確的順序去讀和寫,序列化代碼就失敗。
本原則的例子代碼中都沒有使用自動(隱式)屬性。這就是設計。自動屬性使用編譯器產生的支持域來存儲。你不能訪問支持域,因為域的名字是無效的 C# 記號(它是一個有效的 CLR 符號)。這就使得二進制序列化對使用自動屬性的類而言非常脆弱。你不能寫自己的序列化構造函數,或 GetObjectData 方法訪問那些支持域。這樣只能對簡單類型有效,任何子類和增加域都會失敗。隨著時間推移,你會發現這個問題,并且你不能修復這個問題。任何時候你給你的類型添加 Serializable 特性,你必須使用自己的支持域存儲具體地實現屬性。
.NET 框架提供簡單,標準算法序列化對象。如果你的類型需要持久化,你應該遵從標準的實現。如果你的類型沒有支持序列化,使用這個類的其他類同樣也不支持序列化。盡量為你的客戶端支持這個機制。盡可能的使用默認序列化特性,并且在默認的特性不滿足時要實現 ISerializable 接口。
小結:
雖然序列化的實現機制很簡單,但是細節還是有很多講究的。累死了,打了這么多字,跑步去,加油!
歡迎各種不爽,各種噴,寫這個純屬個人愛好,秉持”分享“之德!
有關本書的其他章節翻譯請[點擊查看](/category/297763),轉載請注明出處,尊重原創!
如果您對D.S.Qiu有任何建議或意見可以在文章后面評論,或者發郵件(gd.s.qiu@gmail.com)交流,您的鼓勵和支持是我前進的動力,希望能有更多更好的分享。
轉載請在**文首**注明出處:[http://dsqiu.iteye.com/blog/2086349](/blog/2086349)
更多精彩請關注D.S.Qiu的博客和微博(ID:靜水逐風)
- 第一章 C# 語言習慣
- 原則1:使用 屬性(Poperty)代替可直接訪問的數據成員(Data Member)
- 原則2:偏愛 readonly 而不是 const
- 原則3:選擇 is 或 as 而不是強制類型轉換
- 原則4:使用條件特性(conditional attribute)代替 #if
- 原則5:總是提供 ToString()
- 原則6:理解幾個不同相等概念的關系
- 原則7:明白 GetHashCode() 的陷阱
- 原則8:優先考慮查詢語法(query syntax)而不是循環結構
- 原則9:在你的 API 中避免轉換操作
- 原則10:使用默認參數減少函數的重載
- 原則11:理解小函數的魅力
- 第二章 .NET 資源管理
- 原則12:選擇變量初始化語法(initializer)而不是賦值語句
- 原則13:使用恰當的方式對靜態成員進行初始化
- 原則14:減少重復的初始化邏輯
- 原則15:使用 using 和 try/finally 清理資源
- 原則16:避免創建不需要的對象
- 原則17:實現標準的 Dispose 模式
- 原則17:實現標準的 Dispose 模式
- 原則18:值類型和引用類型的區別
- 原則19:確保0是值類型的一個有效狀態
- 原則20:更傾向于使用不可變原子值類型
- 第三章 用 C# 表達設計
- 原則21:限制你的類型的可見性
- 原則22:選擇定義并實現接口,而不是基類
- 原則23:理解接口方法和虛函數的區別
- 原則24:使用委托來表達回調
- 原則25:實現通知的事件模式
- 原則26:避免返回類的內部對象的引用
- 原則27:總是使你的類型可序列化
- 原則28:創建大粒度的網絡服務 APIs
- 原則29:讓接口支持協變和逆變
- 第四章 和框架一起工作
- 原則30:選擇重載而不是事件處理器
- 原則31:用 IComparable<T> 和 IComparer<T> 實現排序關系
- 原則32:避免 ICloneable
- 原則33:只有基類更新處理才使用 new 修飾符
- 原則34:避免定義在基類的方法的重寫
- 原則35:理解 PLINQ 并行算法的實現
- 原則36:理解 I/O 受限制(Bound)操作 PLINQ 的使用
- 原則37:構造并行算法的異常考量
- 第五章 雜項討論
- 原則38:理解動態(Dynamic)的利與弊
- 原則39:使用動態對泛型類型參數的運行時類型的利用
- 原則40:使用動態接收匿名類型參數