Serialize Your Deck with Positron [XML Serialization, XSD, C#]

leintor發表於2013-03-29

0. Table of Content

  • 1. Positron S
  • 2. xsd.exe
  • 3. From .xml to .xsd
  • 4. From .xsd to .cs
  • 5. Serialize Your Deck
  • 6. What's More...
  • R. References

 

1. Positron S

《Yu-Gi-Oh! Power of XLinq [C#, XLinq, XML]》中,我們體驗了 XLinq 是如何簡化我們的 XML 處理工作,但現階段就把使用 XLinq 的程式部署到使用者的電腦未免有點為時過早。這次,我們來看看採用業已成熟的 XML Serialization 技術的 Positron,為了標識使用不同技術的 Positron,我在其後加上一個標識字母,目前 Positron 有兩個版本:

  • 1) Positron Q:Q 版 Positron 使用了 XLinq 技術,“Q”代表 Query
  • 2) Positron S:S 版 Positron 使用了 XML Serialization 技術,“S”代表 Serialization

注意:本文將沿用《Yu-Gi-Oh! Power of XLinq [C#, XLinq, XML]》中的 sample.xml 作為原始資料,而某些設計決策也將基於該文的部分分析,如果你沒有讀過該文,我強烈建議你先瀏覽一遍。

 

2. xsd.exe

xsd.exe 是一個神奇的轉換工具,它提供了

  • 1) XDR to XSD
  • 2) XML to XSD
  • 3) XSD to DataSet
  • 4) XSD to Classes
  • 5) Classes to XSD

等一系列的轉換功能。當你用它來生成程式碼檔案時,如果你沒有明確指示使用何種語言,它將預設生成 .cs 檔案。你可以使用 /l 引數來顯示指定使用何種語言,xsd.exe 支援 CS、VB、JS 和 VJS 等語言。在這篇文章,我將會介紹 XML to XSD 和 XSD to Classes 這兩種轉換。

 

3. From .xml to .xsd

3.1 Generate sample.xsd with xsd.exe

開啟 SDK Command Prompt,去到 sample.xml 所在的目錄並輸入

xsd sample.xml

然後按下 [Enter],xsd.exe 將在當前目錄生成一個 sample.xsd 檔案。但這個自動生成版的佈局不便於我們對其展開討論,於是我對其進行等效重排。方法是將原來的匿名型別變為命名型別並從其所屬元素中分離出來,然後使用 的 type 屬性將這兩者重新關聯起來。重排後的版本如下:

Serialize Your Deck with Positron [XML Serialization, XSD, C#] sample.xsd

3.2 vs.

在 Cards 的型別定義中,xsd.exe 將其子元素的排布方式設定為 ,這個意味著其子元素 monstercards、spellcards 和 trapcards 只有一種出現,而該種子元素的出現次數可以任意。這明顯不符合原設計理念。

all 指示所有的子元素可以以任意順序出現,且每種子元素最多隻能出現一次。我假設 Positron S 的使用者懂得均衡卡組,即卡組中既有怪獸卡又有魔法卡和陷阱卡,並使不同種類的卡片數目達到一個恰當的比例。於是,我將三種子元素的排布方式改為:

...

注意:如果我們沒有顯式為 指明 minOccurs 和 maxOccurs 的值,它們都將會使用預設值1,即每種子元素都必須出現一次也只能出現一次。

3.3 xs:string vs. xs:int

在 MonsterCard 的型別定義中,xsd.exe 把 level、atk 和 def 三個屬性的型別指定為 xs:string,但我們很清楚這些屬性的值是整數,所以我把它們都改為 xs:int。這樣做的好處不僅僅在於讓人能從 sample.xsd 中瞭解到這三個屬性的值是整數型別,更重要的是將來使用 xsd.exe 根據 sample.xsd 生成 sample.cs 時,MonsterCard 類中的 m_Level 欄位以及 Level 屬效能被自動對映為 Int32 型別。並且在反序列化時,讓 XmlSerializer 為你進行數值的解析而不必親自動手。

3.4 xs:string vs. xs:enumeration

我們知道 MonsterCard 的型別定義中的 category、attribute 和 type 其實是列舉型別,我希望將來使用 xsd.exe 生成程式碼檔案時,它懂得把這些屬性對映為 .NET 的列舉型別。為了達到這個目的,我們需要獨立定製這些屬性的型別,並使用 的 type 屬性進行型別關聯,即我先前所說“等效重排”。下面我將以 MonsterCard 的 category 作為例子:

首先,我定義一個命名列舉型別:

<xs:simpleType name="MonsterCardCategory">
    
<xs:restriction base="xs:string">
        
<xs:enumeration value="Normal" />
        
<xs:enumeration value="Effect" />
        
<xs:enumeration value="Fusion" />
        
<xs:enumeration value="Ritual" />
    
xs:restriction>
xs:simpleType>

這裡需要注意的有兩點:

  • 1) 列舉型別必須為命名型別,否則 xsd.exe 會忽略之並把 category 對映為 String 型別
  • 2) 列舉型別的基型別必須為 xs:string 或相容型別,否則 xsd.exe 不會將之當作一回事

然後,把 category 屬性的 type 設定為 MonsterCardCategory:

<xs:attribute name="category" type="MonsterCardCategory" />

接著,我們可以用同樣的方法處理其它列舉型別的屬性。

3.5 vs.

重讀 sample.xsd,你會發現,無論是 MonsterCard、SpellCard 或者 TrapCard,都有著三個功能相同的成員:img、name 和 body text。為了減少重複,我決定對它們進行泛化,提取公共部分。

首先,我定義一個 Card 型別:

<xs:complexType name="Card" abstract="true">
    
<xs:simpleContent msdata:ColumnName="description" msdata:Ordinal="2">
        
<xs:extension base="xs:string">
            
<xs:attribute name="img" type="xs:string" use="required" />
            
<xs:attribute name="name" type="xs:string" use="required" />
        
xs:extension>
    
xs:simpleContent>
xs:complexType>

注意:我將 Card 的 abstract 屬性設為 true,這點很重要,它保證了在將來的 XML 文件中出現的是 Card 的繼承型別而不是 Card 這個型別。這一點和程式語言的抽象類在設計理念上是一致的。

然後讓 MonsterCard、SpellCard 和 TrapCard 繼承 Card,要做到這點,我們可以修改 的 base 屬性,使其指向 Card。

然而, 用在 或者 上會對 xsd.exe 所生成的程式碼產生不同的影響。對於前者,xsd.exe 會把 base 屬性所指定的型別對映為類的一個欄位,即我們通常說的 Composition;對於後者,情形就是我們所熟悉的 Inheritance。很明顯,這裡我們應該選用 Inheritance,因為 Card 的 abstract 屬性被設為 true,如果使用 Composition 的話,抽象類 Card 作為類的一個欄位而存在,必須有(非抽象)派生類才能產生例項變數,這樣我們就重新回到 Inheritance 了。

現在,我用 SpellCard 來示範如何實現繼承:

<xs:complexType name="SpellCard">
    
<xs:complexContent>
        
<xs:extension base="Card">
            
<xs:attribute name="category" type="SpellCardCategory" />
        
xs:extension>
    
xs:complexContent>
xs:complexType>

雖然這三種卡都有 category 屬性,但因為該屬性實際上具有不同的含義,並且型別也不同,所以不被納入它們的共性。

3.6 cards.xsd

至此,我們已經完成了整個 XML Schema 的製作了:

Serialize Your Deck with Positron [XML Serialization, XSD, C#] cards.xsd

值得注意的是,所有型別的屬性的 use 屬性值都被設為 required,這是因為實際的卡片中必定包含這些資訊,這樣做保證了資訊的完整性。另外,我去掉了那些以 msdata 為字首的屬性,因為這些東西僅在你進行 XSD to DataSet 轉換時才有用,而在這裡明顯是垃圾資料,當然,如果你認為將來有可能來一個 XSD to DataSet 的話,你也可以保留它們。

 

4. From .xsd to .cs

4.1 Generate cards.cs with xsd.exe

再次啟動你的 SDK Command Prompt,去到 cards.xsd 所在的目錄並輸入

xsd cards.xsd /c

然後按下 [Enter],xsd.exe 將在當前目錄生成一個 cards.cs 檔案。為了便於討論,我對程式碼進行等效整理。整理的內容包括:

  • 1) 去掉所有的註釋。這些註釋是 xsd.exe 自動生成的,它們不但對於我們的討論不起作用,還妨礙了我們的視線。
  • 2) 使用 Attribute 的簡寫形式。例如 [System.Xml.Serialization.XmlAttributeAttribute()] 將被改為 [XmlAttribute()]。
  • 3) 重構類的私有欄位的名字。例如 monstercardsField 將被改為 m_MonsterCards。
  • 4) 把類內透過 this 使用自身私有欄位改為直接使用私有欄位。例如 this.m_MonsterCards 將被改為 m_MonsterCards。
  • 5) 修改一下程式碼的縮排。

整理後的版本如下:

Serialize Your Deck with Positron [XML Serialization, XSD, C#]cards.cs

從程式碼中,我們可以看到,xsd.exe 的確很能幹,生成的程式碼基本上都符合預期的設想了。

4.2 attribute & body text

預設情況下,XmlSerializer 會將類的公有欄位和公有屬性序列化為 XML 元素,但我們很清楚,三大卡類的資料要麼以 的形式出現,要麼以 body text 的形式出現。為了使得 XmlSerializer 能產生正確的輸出,我們為那些將以 形式輸出的公有屬性應用 XmlAttribute,為那個將以 body text 形式輸出的公有屬性應用 XmlText。

目前三大卡類的公有屬性的名字都是全小寫的,接下來我準備使用 Pascal 命名方式為它們重新命名。要順利完成重構,除了常規的重構手續之外,我們還需要叫XmlAttribute 通知 XmlSerializer 將來使用重構之前的名字來進行序列化。下面用 Card.name 這個公有屬性來舉例:

// Code #01

[XmlAttribute(
"name")]
public string Name
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
get return m_Name; }
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
set { m_Name = value; }
}

由於每個元素有且僅有一個 body text,於是重新命名卡片描述這個公有屬性就僅需辦理常規手續了:

// Code #02

[XmlText()]
public string Description
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
get return m_Description; }
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
set { m_Description = value; }
}

接著,我們可以用同樣的方法處理其他公有屬性。

4.3 array & array item

Cards 類中的三個公有屬性都是陣列型別,為了使得 XmlSerializer 將來能夠產生正確的輸出,你應該為這些屬性應用 XmlArray 和 XmlArrayItem。現在,我又希望使用 Pascal 命名方式對這三個公有屬性進行重新命名,那麼除了常規的重構手續,我還需要向 XmlArray 說明情況。下面以 Cards.trapcards 為例:

// Code #03

[XmlArray(
"trapcards", Form = XmlSchemaForm .Unqualified)]
[XmlArrayItem(
"trapcard", Form = XmlSchemaForm.Unqualified)]
public TrapCard[] TrapCards
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
get return m_TrapCards; }
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
set { m_TrapCards = value; }
}

4.4 include

三大卡類繼承了抽象類 Card,為了使得將來 XmlSerializer 在進行序列化式能夠正確把 Card 的成員包含到三大卡類裡,我們需要明確指出三大卡類和 Card 的繼承關係,這可以通過 XmlInclude 來做到。熟悉 C++ 的朋友會發現這個做法很象 C++ 的 #include,不同的是,#include 是在 client 端使用,而 XmlInclude 則在 server 端使用,並且有多少個派生類就使用多少次 XmlInclude。假如我們並不知道有多少個可能的派生類,那麼我們將陷入泥潭。一個可行的解決方法就是使用 Composition 來實現複用,即上一部分(§ 3.5)所說的在 上使用 。此時,Card 就不能被宣告為 abstract 了,而將來 XmlSerializer 所生成的 XML 文件也將會和預期的有出入,因為 Card 將分別作為三大卡類的子元素被序列化(它是一個 ,因此不能被序列化到 上)。另一個可行的解決方法就是放棄複用。究竟選用哪種做法就要具體問題具體分析了。

4.5 Replace Array with Collection

當我們把 XML 檔案反序列化後,通常我們並不僅僅為了檢視資料,更多時候我們會修改資料,例如新增新的資料或者刪除現有的資料。Cards 內部使用陣列來儲存反序列化後的資料明顯有著侷限性,現在我打算改用集合類來儲存資料。以 m_MonsterCards 為例:

// Code #04

private List<MonsterCard> m_MonsterCards;

接著,公有屬性 MonsterCards 也需要作相應的調整:

// Code #05

[XmlArray(
"monstercards", Form = XmlSchemaForm .Unqualified)]
[XmlArrayItem(
"monstercard", Form = XmlSchemaForm.Unqualified)]
public MonsterCard[] MonsterCards
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
get return m_MonsterCards.ToArray(); }
    
set
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
{
        m_MonsterCards.Clear();
        m_MonsterCards.AddRange(value);
    }

}

然後,我們應該為 m_MonsterCards 提供常用的操作介面:

// Code #06

public void Add(MonsterCard monsterCard)
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
    m_MonsterCards.Add(item);
}


public void Remove(MonsterCard item)
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
    m_MonsterCards.Remove(item);
}

為了使得 Remove 執行的更有效,你應該為 MonsterCard 類實現 System.IComparable 介面。

現在萬事俱備,只欠建構函式了,我們應該在建構函式中例項化 m_MonsterCards:

// Code #07

public Cards()
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
    m_MonsterCards 
= new List<MonsterCard>();

    
// 
}

或者有人會問:為什麼不直接讓客戶端操作集合類而要這樣大費周折呢?沒錯,公開集合類能夠簡化類的程式碼,但這樣一來,客戶端和 Cards 的耦合度就提高了,不便於我們將來修改 Cards 的實現。

 

5. Serialize Your Deck

5.1 Cards.Load

該方法的程式碼很簡單:

// Code #08

public static Cards Load(string filename)
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
    
using (XmlReader reader = XmlReader.Create(filename))
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
{
        XmlSerializer serializer 
= new XmlSerializer(typeof(Cards));
        
return (Cards)serializer.Deserialize(reader);
    }

}

當我們需要進行反序列化時,只需:

// Code #09

Cards cards 
= Cards.Load(filename);

另外,你不需要自行檢查指定檔案是否存在,因為 XmlReader.Create 會在指定檔案不存在是丟擲 FileNotFoundException 的。

5.2 Cards.Save

該方法的程式碼也非常簡單:

// Code #10

public void Save(string filename)
Serialize Your Deck with Positron [XML Serialization, XSD, C#]
{
    XmlWriterSettings settings 
= new XmlWriterSettings();
    settings.Indent 
= true;
    settings.IndentChars 
= "    ";

    
using (XmlWriter writer = XmlWriter.Create(filename, settings))
Serialize Your Deck with Positron [XML Serialization, XSD, C#]    
{
        XmlSerializer serializer 
= new XmlSerializer(typeof(Cards));
        serializer.Serialize(writer, 
this);
    }

}

值得注意的是,你必須傳遞一個 XmlWriterSettings 的例項給 XmlWriter.Create,且該例項的 Indent 屬性被設為 true,否則輸出的 XML 文件將會凌亂不堪,因為沒有經過縮排。另外,如果你希望改變用於縮排的字元,你可以設定 IndentChars 屬性,預設為兩個空格,這裡我將之設為四個空格。

5.3 Pay Attention to the Default Constructor

原本三大卡類都沒有定義建構函式,於是 .NET 為你提供一個以便它們能正常工作,而 Positron 也工作良好。但有一天你心血來潮為三大卡類分別提供一個建構函式用於初始化所有欄位,你覺得這樣方便,然而,Positron 出現異常了,它抱怨你沒有提供預設建構函式。

原來,當我們為類提供建構函式後,.NET 將不再為它提供預設建構函式,而 XmlSerializer 又非要和預設建構函式一起工作不可,如果我們沒有為類顯式提供預設建構函式,XmlSerializer 將會罷工!另外,不管你提供的預設建構函式是 public、protected 還是 private,只要你提供了,XmlSerializer 就會高興了。

5.5 cards.cs

至此,整個 Positron S 的核心程式碼已經制作完畢:

Serialize Your Deck with Positron [XML Serialization, XSD, C#]cards.cs

對比之前那個版本,除了上面的一系列改動之外,我還去掉了所有型別的 SerializableAttribute,因為它與 XML Serialization 無關。當然,如果你將來希望把這些型別用於 Binary Serialization,你仍然可以保留它們。

另外,抽象類實現了 System.IComparable 介面,以便 List.Remove 方法內部使用。那麼,如何界定兩張卡片的異同呢?在我印象中,卡片的名字是唯一的(我從沒看過兩張不同的卡片有著相同的名字),但為了安全起見,我特意諮詢了 Upper Deck 官方,果然,卡片的名字是唯一的!這樣 IComparable.CompareTo 的實現就是僅僅比較兩張卡的名字了。

 

6. What's More...

6.1 Is Your Data Source Valid?

我在這裡提供的 Cards.Load 很簡單,實際上,你可以在處理任何資料來源之前對其進行驗證,例如資料來源的資料的型別是否與預期設想吻合,資料有否越界,即不落在預期的範圍內,這樣可以保證在作進一步處理前資料來源是有效的(valid)。這裡的有效是指待處理的資料來源(即 XML 文件)不但良構,還要符合預先定義的 DTD 或者 schema。

6.2 Enable Linq for Positron S

如果你讀過我的《Yu-Gi-Oh! Power of XLinq [C#, XLinq, XML]》,你應該會記得我曾經在那篇文章示範如何尋找等級3或者以下並且具有效果的怪獸。現在我們來看看如何結合 Linq 和 Positron S 來尋找目標:

// Code #11

Cards cards 
= Cards.Load("sample.xml");
var wanted 
= from c in cards.MonsterCards
             where (c.Level 
< 4)&&(c.Category == MonsterCardCategory.Effect)
             orderby c.Atk
             select c;

6.3 An O/X Mapping Solution?

第一次接觸 XML Serialization 時,我覺得它的做法很像 O/R Mapping,雖然我不知道稱它為 O/X Mapping 是否恰當,但它卻實實在在提供了 Object 和 XML Data Source 之間的相互轉換。與常規的 DOM 處理方式相比,它的確為我帶來了巨大的便利。

 

R. References

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/20200170/viewspace-757418/,如需轉載,請註明出處,否則將追究法律責任。

相關文章