程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> 關於C# >> Effective C#原則25 讓你的類型支持序列化

Effective C#原則25 讓你的類型支持序列化

編輯:關於C#

對象的持久是類型的一個核心功能。這是一個在你忽略對它的支持以前,沒 有人會注意到的基本元素之一。 如果你的類型不能恰當的支持序列化,那麼對 於把你類的做為基類或者成員的開發人員來說,你會給他們增加很多的工作量。 當你的類型不支持序列化時,他們不得不圍繞這工作,自己添加實現這個標准的 功能。而對於不能訪問類的私有成員的開發人來說,恰當的實現你的類型的序列 化是不太可能的。如果你的類型不支持序列化,那麼對於你的用戶來說,想再要 實現實它是很困難或者根本就不可能的事。

取而代之的是,為你的實際 類型添加序列化。對於那些不用承載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 _object;
}

這裡的Serializable特性只有在OtherClass也支持序列化時才有 效。如果OtherClass不支持序列化,那麼你在序列化MyType時,因為OtherClass 對象也在裡面,你會得到一個運行時錯誤。這只是因為對OtherClass的內部結構 不清楚,而使序列化成為不可能。

.Net的序列化是把類中所有成員變量 保存到輸出流中。另外,.Net的序列化還支持任意的對象圖(object graph):即 使你的對象上有一個循環引用,serialize 和deserialize 方法都只會為你的實 際對象讀取和儲存一次。當一些web對象反序列化了以後,.Net序列化框架也可 以創建這些web對象的引用。你創建的任何與web相關的對象,在對象圖序列化以 後,你都可以正確的保存它們。最後一個要注意的地方是Serializable 特性同 時支持二進制和SOAP序列化。這一原則裡的所有技術都支持這兩種序列化格式。 但是要記住:只有當所有類型的對象圖都支持序列化時才能成功。這就是為什麼 要讓所有的類型都支持序列化顯得很重要了。一但你放過了一個類,你就輕意的 給對象圖開了個後門,以至於所有使用這個類的人,想要序列化對象圖時變得更 加困難。不久以後,他們就會發現不得不自己寫序列化代碼了。

添加 Serializable特性是一個最簡單的技術來支持對象的序列化。但最簡單的方案並 不是總是正確的方案。有時候,你並不想序列化對象的所有成員:有些成員可能 只存在於長期操作的緩存中,還有一些對象可能占用著一些運行時資源,而這些 資源只能存在於內存中。你同樣可以很好的使用特性來控制這些問題。添加 NonSerialized特性到任何你不想讓它序列化的數據成員上。這給它們標上了不 用序列化的標記:

Serializable]
public class MyType
{
 private string _label;
 [NonSerialized]
  private int _cachedValue;
 private OtherClass _object;
}

你,做為類的設計者,非序列化成員給你多添加了一點點工作。 在序列化過程中,序列化API不會為你初始化非序列化成員。因為類型的構造函 數沒有被調用,所以成員的初始化也不會被執行。當你使用序列化特性時,非序 列成員就保存著系統默認值:0或者null。當默認的0對初始化來說是不正確的, 那麼你須要實現IDeserializationCallback 接口,來初始化這些非序列化成員 。框架會在整個對象圖反序列化以後,調用這個方法。這時,你就可以用它為所 有的非序列化成員進行初始化了。因為整個對象圖已經載入,所以你的類型上的 所有方法的調用及成員的使用都是安全的。不幸的是,這不是傻瓜式的。在整個 對象圖載入後,框架會在對象圖中每個實現了IDeserializationCallback接口的 對象上調用OnDeserialization方法。對象圖中的其它任何對象可以在 OnDeserialization正在進行時調用對象的公共成員。如果它們搶在了前面,那 麼你的非序列化成員就是null或者0。順序是無法保證的,所以你必須確保你的 所有公共成員,都能處理非序列化成員還沒有初始化的這種情況。

到目 前為止,你已經知道為什麼要為所有類型添加序列化了:非序列化類型會在要序 列化的對象中使用時帶來更多的麻煩事。你也學會了用特性來實現最簡單的序列 化方法,還包括如何初始化非序列化成員。

序列化了對象有方法在程序 的不同版本間生存。(譯注:這是一個很重要的問題,因為.Net裡的序列化不像 C++那樣,你可以輕松的自己控制每一個字節的數據,因此版本問題成了序列化 中經常遇到的一個問題。) 添加序列化到一個類型上,就意味著有一天你要讀取 這個對象的早期版本。Serializable特性生成的代碼,在對象圖的成員被添加或 者移除時會拋出異常。當你發現你自己已經要面對多版本問題時,你就需要在序 列化過程中負出更多的操作:使用ISerializable接口。這個接口定義了一些 hook用於自定義序列化你的類型。ISerializable接口裡使用的方法和存儲與默 認的序列化方法和儲存是一致的,這就是說,你可以使用序列化特性。如果什麼 時候有必要提供你自己的擴展序列化時,你可以再添加對ISerializable接口的 支持。

做一個為例子:考慮你如何來支持MyType的第2個版本,也就是添 加了另一個字段到類中時。簡單的添加一個字段都會產生一個新的類型,而這與 先前已經存在磁盤上的版本是不兼容的:

[Serializable]
public class MyType
{
 private string _label;
  [NonSerialized]
 private int _value;
 private OtherClass  _object;
 // 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 System.Runtime.Serialization;
using System.Security.Permissions;
[Serializable]
public sealed class MyType : ISerializable
{
 private string _label;
 [NonSerialized]
 private int _value;
 private OtherClass _object;
 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" );
  _object = ( OtherClass )info.GetValue( "_object", 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( "_object", _object );
  inf.AddValue( "_value2", _value2 );
 }
}

序列化流 是以鍵/值對應的方法來保存每一個元素的。默認的特性生成的代碼是以變量名 做為鍵來存儲值。當你添加了ISerializable接口後,你必須匹配鍵名以及變量 順序。這個順序就是在類中定義時的順序。(順便說一句,這實際上就是說重新 排列類中的變量名或者重新給變量命名,都會破壞對已經創建了的文件的兼容性 。)

同樣,我已經要求過SerializationFormatter的安全許可。如果不實 行恰當的保護,對於你的類來說,GetObjectData()可能存在安全漏洞。惡意代 碼可能會產生一個StreamingContext,從而可以用GetObjectData()方法從對象 中取得值,或者不斷修改版本而取得另一個SerializationInfo,或者重新組織 修改的對象。這就許可了惡意的開發者來訪問對象的內部狀態,在流中修改它們 ,然而發送一個修改後的版本給你。對SerializationFormatter進行許可要求可 以封閉這個安全漏洞。這樣可以確保只有受信任的代碼才能恰當的訪問類的內部 狀態(參見原則47)。

但在使用ISerializable接口時有一個弊端,你可以 看到,我很早就讓MyType成為密封(sealed)的,這就強制讓它只能成為葉子類 (leaf class)。在基類實現ISerializable接口就隱式的讓所有派生類也序列化 。實現ISerializable就意味關所有派生類必須創建受保護構造函數以及反序列 化。另外,為了支持非密封類,你必須在GetObjectData()方法創建hook,從而 讓派生類可以添加它們自己的數據到流中。編譯器不會捕獲任何這樣的錯誤,當 從流中讀取派生類時,因缺少恰當的構造構造函數會在運行時拋出異常。缺少 hook的GetObjectData()方法也意味著從派生類來的數據不會保存到文件中。當 然也不會有錯誤拋出。所以我要推薦:在葉類中實現Serializable。

我 沒有說這,因為它不工作:為了派生類的序列化,你的基類必須支持序列化。修 改MyType ,讓它成為了一個可序列化的基類,你要把序列化構造函數修改為 protected,然後創建一個虛方法,這樣派生類就可以重載它並存儲它們的數據 。

using System.Runtime.Serialization;
using System.Security.Permissions;
[Serializable]
public class MyType : ISerializable
{
 private string _label;
  [NonSerialized]
 private int _value;
 private OtherClass  _object;
 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" );
  _object = ( OtherClass )info.GetValue( "_object", 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( "_object", _object );
  inf.AddValue( "_value2", _value2 );
  WriteObjectData( inf, cxt );
 }
 // Overridden in derived classes to write
 // derived class data:
 protected virtual void
   WriteObjectData(
  SerializationInfo inf,
   StreamingContext cxt )
 {
 }
}

一個派生 類應該提供它自己的序列化構造函數,並且重載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 );
 }
}

從流中寫入和讀取值的順序必須保持一致。我相信先讀寫基類的數據應該簡單一 些,所以我就這樣做了。如果你寫的代碼不對整個繼承關系進行精確的順序序列 化,那麼你的序列化代碼是無效的。

.Net框架提供了一個簡單的方法, 也是標准的算法來支持對象的序列化。如果你的類型須要持久,你應該遵守這個 標准的實現。如果你的類型不支持序列化,那化其它使用這個類的類也不能序列 。為了讓使用類的客戶更加方便,盡可能的使用默認序列化特性,並且在默認的 特性不滿足時要實現ISerializable 接口。

返回教程目錄

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved