解決編程中序列化問題
一、問題重現
為了重現我實際遇到的問題,我特意將問題簡化,為此我寫了一個簡單的例子(你可以從這里下載)。在下面的代碼片斷中,我創建了一個名稱為ContextItem的類型,代表一個需要維護的上下文項。由于需要在WCF服務調用實現自動傳遞,我將起定義成DataContract。ContextItem包含Key,Value和ReadOnly三個屬性,不用說ReadOnly表示該ContextItem可以被修改。注意Value屬性Set方法的定義——如果ReadOnly則拋出異常。
1: [DataContract(Namespace = "http://www.artech.com")]
2: public class ContextItem
3: {
4: private object value = null;
5: [DataMember]
6: public string Key { get; private set; }
7: [DataMember]
8: public object Value
9: {
10: get
11: {
12: return this.value;
13: }
14: set
15: {
16: if (this.ReadOnly)
17: {
18: throw new InvalidOperationException("Cannot change the value of readonly context item.");
19: }
20: this.value = value;
21: }
22: }
23: [DataMember]
24: public bool ReadOnly { get; set; }
25: public ContextItem(string key, object value)
26: {
27: if (string.IsNullOrEmpty(key))
28: {
29: throw new ArgumentNullException("key");
30: }
31: this.Key = key;
32: this.Value = value;
33: }
34: }
為了演示序列化和反序列化,我寫了如下兩個靜態的幫助方法。Serialize和Deserialize分別用于序列化和反序列化,前者將對象序列成成XML并保存到指定的文件中,后者則從文件讀取XML并反序列化成相應的對象。
1: public static T Deserialize<T>(string fileName)
2: {
3: DataContractSerializer serializer = new DataContractSerializer(typeof(T));
4: using (XmlReader reader = new XmlTextReader(fileName))
5: {
6: return (T)serializer.ReadObject(reader);
7: }
8: }
9:
10: public static void Serialize<T>(T instance, string fileName)
11: {
12: DataContractSerializer serializer = new DataContractSerializer(typeof(T));
13: using (XmlWriter writer = new XmlTextWriter(fileName, Encoding.UTF8))
14: {
15: serializer.WriteObject(writer, instance);
16: }
17: Process.Start(fileName);
18: }
我們的程序很簡單。從如下的代碼片斷中,我們先創建一個ContextItem對象,然后將ReadOnly屬性設置成true。然后調用Serialize方法將對象序列化成XML并保存在一個名稱為context.xml的文件中。然后調用Deserialize方法,讀取該文件進行反序列化。
1: static void Main(string[] args)
2: {
3: var contextItem1 = new ContextItem("__userId", "Foo");
4: contextItem1.ReadOnly = true;
5: Serialize<ContextItem>(contextItem1, "context.xml");
6: var contextItem2 = Deserialize<ContextItem>("context.xml");
7: }
序列化操作能夠正常執行,但當程序執行到Deserialize的時候拋出如下一個InvalidOperationException異常。
從上面給出的截圖,我們不難看出,異常是在給ContextItem對象的Value屬性賦值的時候拋出的。如果對DataContractSerializer序列化器的序列化/反序列化規則的有所了解的話,應該知道:對于數據契約(DataContract)基于屬性(Property)的數據成員(DataMember),序列器在反序列化的時候是通過調用Set方法對其進行初始化的。在本例中,由于ReadOnly是True,在對Value進行反序列化的時候必然會調用Set方法。但是,只讀的ContextItem卻不能對其賦值,所以異常拋出。
那么,如何來解決這個問題呢?我最初的想法是這樣:在序列化的時候將ReadOnly屬性設置成False,然后添加另一個屬性專門用于保存真實的值。在進行反序列的時候,由于ReadOnly為false,所以不會出現異常。當反序列化完成之后,在將ReadOnly的初始值賦上。雖然上述的方案能夠解決問題,但是為此對ContextItem添加一個只在序列化和反序列化的過程中在有用的屬性,總覺得很丑陋。
我們不妨換一種思路:異常產生于對Value屬性凡序列化時發現ReadOnly非True的情況。那么怎樣采用避免這種情況的發生呢?如果Value屬性先于ReadOnly屬性被序列化,那么ReadOnly的初始值就是False,這個問題不就解決了嗎?這就是我們的第一個解決方案。
三、解決方案一:通過控制屬性反序列化順序
那么,如果控制那么屬性先被反序列化,那么后被序列化呢?這就是要了解DataContractSerializer序列化器的序列化和發序列化規則了。在默認的情況下,DataContractSerializer是按照數據成員的名稱的順序進行序列化的。這可以從生成出來的XML的結構看出來。而XML元素的先后順序決定了反序列化的順序。
1: <ContextItem xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.artech.com">
2: <Key>__userId</Key>
3: <ReadOnly>true</ReadOnly>
4: <Value xmlns:d2p1="http://www.w3.org/2001/XMLSchema" i:type="d2p1:string">Foo</Value>
5: </ContextItem>
在上面的例子中,ContextItem的ReadOnly排在Value的前面,會先被序列化。那么,是不是我們要更新Value或者ReadOnly的數據成員(DataMember,不是屬性名稱)呢?這肯定不是我們想要的解決方案。在SOA的世界中,DataMember是契約的一部分,往往是不容許更改的。
如果在不更改數據成員名稱的前提下讓屬性Value先于ReadOnly被序列化,需要用到DataContractSerializer另一條反序列化規則:我們可以通過DataMemberAttribute特性的Order屬性控制序列化后的屬性在XML元素列表中的位置。
為此,我們有了答案,我們只需要將ContextItem稍加改動就可以了。在如下的代碼中,在為Value和ReadOnly兩個屬性應用DataMemberAttribute的時候,將Order屬性分別設置成1和2,這樣就能使ContextItem對象在被序列化的時候,Value和ReadOnly屬性對應的XML元素將永遠會有前后之分。這里還需要注意的是,在Value屬性的Set方法中,判斷是否只讀,采用的不是ReadOnly屬性,而是對應的readonly字段。這一點非常重要,如果調用ReadOnly屬性將會迫使該屬性被反序列化。
1: [DataContract(Namespace = "http://www.artech.com")]
2: public class ContextItem
3: {
4: private object value = null;
5: private bool readOnly;
6: [DataMember]
7: public string Key { get; private set; }
8:
9: [DataMember(Order = 1)]
10: public object Value
11: {
12: get
13: {
14: return this.value;
15: }
16: set
17: {
18: if (this.readOnly)
19: {
20: throw new InvalidOperationException("Cannot change the value of readonly context item.");
21: }
22: this.value = value;
23: }
24: }
25: [DataMember(Order =2)]
26: public bool ReadOnly
27: {
28: get
29: {
30: return readOnly;
31: }
32: set
33: {
34: readOnly = value;
35: }
36: }
37: //Others
38: }
有興趣的讀者可以親自試試看,如果我們進行了如上的更改,前面的程序就能正常運行了。到這里,有的讀者可以要問了,你不是說僅僅有一行代碼的變化嗎,我看上面改動的不止一行嘛。沒有錯,我們完全可以作更少的更改來解決問題。
四、解決方案二:將數據成員定義在字段上而不是屬性上
我們再換一種思維,之所以出現異常是在反序列化的時候調用Value屬性的Set方法所致。如果在反序列化的時候不調用這個方法不就得了嗎?那么,如何才能避免對Value屬性的Set方法的調用呢?方法很簡單,那就是將數據成員定義在字段上,而不是屬性上。基于屬性的數據成員在反序列化的時候不得不通過調用Set方法對數據項進行初始化,而基于字段的數據成員在反序列化的時候只需要直接對其復制就可以了。
基于這樣的思路,我們對原來的ContextItem進行簡單的改動——將DataMemberAttribute特性從Value屬性移到value字段上。需要注意的,為了符合于原來的Schema,需要將DataMemberAttribute特性的Name屬性設置成“Value”。
1: [DataContract(Namespace = "http://www.artech.com")]
2: public class ContextItem
3: {
4: [DataMember]
5: public string Key { get; private set; }
6:
7: [DataMember(Name = "Value")]
8: private object value = null;
9: public object Value
10: {
11: get
12: {
13: return this.value;
14: }
15: set
16: {
17: if (this.ReadOnly)
18: {
19: throw new InvalidOperationException("Cannot change the value of readonly context item.");
20: }
21: this.value = value;
22: }
23: }
24: [DataMember]
25: public bool ReadOnly { get; set; }
26: //Others
27: }
28: }
總結
雖然這僅僅是一個很小的問題,解決的方案看起來也是如此的簡單。但是,這并不意味著這是一個可以被忽視的問題,背后隱藏對DataMemberAttribute序列化的序列化規則的理解。