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

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

 

Written by Allen Lee

 

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 文件。但这个自动生成版的布局不便于我们对其展开讨论,于是我对其进行等效重排。方法是将原来的匿名类型变为命名类型并从其所属元素中分离出来,然后使用 <xs:element> 的 type 属性将这两者重新关联起来。重排后的版本如下:

sample.xsd

3.2 <xs:choice> vs. <xs:all>

在 Cards 的类型定义中,xsd.exe 将其子元素的排布方式设置为 <xs:choice>,这个意味着其子元素 monstercards、spellcards 和 trapcards 只有一种出现,而该种子元素的出现次数可以任意。这明显不符合原设计理念。

all 指示所有的子元素可以以任意顺序出现,且每种子元素最多只能出现一次。我假设 Positron S 的用户懂得均衡卡组,即卡组中既有怪兽卡又有魔法卡和陷阱卡,并使不同种类的卡片数目达到一个恰当的比例。于是,我将三种子元素的排布方式改为:

<xs:all>...</xs:all>

注意:如果我们没有显式为 <xs:all> 指明 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 的枚举类型。为了达到这个目的,我们需要独立定制这些属性的类型,并使用 <xs:attribute> 的 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 <xs:simpleContent> vs. <xs:complexContent>

重读 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,要做到这点,我们可以修改 <xs:extension> 的 base 属性,使其指向 Card。

然而,<xs:extension> 用在 <xs:simpleContent> 或者 <xs:complexContent> 上会对 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 的制作了:

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) 修改一下代码的缩进。

整理后的版本如下:

cards.cs

从代码中,我们可以看到,xsd.exe 的确很能干,生成的代码基本上都符合预期的设想了。

4.2 attribute & body text

默认情况下,XmlSerializer 会将类的公有字段和公有属性序列化为 XML 元素,但我们很清楚,三大卡类的数据要么以 <xs:attribute> 的形式出现,要么以 body text 的形式出现。为了使得 XmlSerializer 能产生正确的输出,我们为那些将以 <xs:attribute> 形式输出的公有属性应用 XmlAttribute,为那个将以 body text 形式输出的公有属性应用 XmlText。

目前三大卡类的公有属性的名字都是全小写的,接下来我准备使用 Pascal 命名方式为它们重命名。要顺利完成重构,除了常规的重构手续之外,我们还需要叫XmlAttribute 通知 XmlSerializer 将来使用重构之前的名字来进行序列化。下面用 Card.name 这个公有属性来举例:

// Code #01

[XmlAttribute(
"name")]
public string Name
{
    
get return m_Name; }
    
set { m_Name = value; }
}

由于每个元素有且仅有一个 body text,于是重命名卡片描述这个公有属性就仅需办理常规手续了:

// Code #02

[XmlText()]
public string Description
{
    
get return m_Description; }
    
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
{
    
get return m_TrapCards; }
    
set { m_TrapCards = value; }
}

4.4 include

三大卡类继承了抽象类 Card,为了使得将来 XmlSerializer 在进行序列化式能够正确把 Card 的成员包含到三大卡类里,我们需要明确指出三大卡类和 Card 的继承关系,这可以通过 XmlInclude 来做到。熟悉 C++ 的朋友会发现这个做法很象 C++ 的 #include,不同的是,#include 是在 client 端使用,而 XmlInclude 则在 server 端使用,并且有多少个派生类就使用多少次 XmlInclude。假如我们并不知道有多少个可能的派生类,那么我们将陷入泥潭。一个可行的解决方法就是使用 Composition 来实现复用,即上一部分(§ 3.5)所说的在 <xs:simpleContent> 上使用 <xs:extension>。此时,Card 就不能被声明为 abstract 了,而将来 XmlSerializer 所生成的 XML 文档也将会和预期的有出入,因为 Card 将分别作为三大卡类的子元素被序列化(它是一个 <xs:complexType>,因此不能被序列化到 <xs:attribute> 上)。另一个可行的解决方法就是放弃复用。究竟选用哪种做法就要具体问题具体分析了。

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
{
    
get return m_MonsterCards.ToArray(); }
    
set
    
{
        m_MonsterCards.Clear();
        m_MonsterCards.AddRange(value);
    }

}

然后,我们应该为 m_MonsterCards 提供常用的操作接口:

// Code #06

public void Add(MonsterCard monsterCard)
{
    m_MonsterCards.Add(item);
}


public void Remove(MonsterCard item)
{
    m_MonsterCards.Remove(item);
}

为了使得 Remove 执行的更有效,你应该为 MonsterCard 类实现 System.IComparable<T> 接口。

现在万事俱备,只欠构造函数了,我们应该在构造函数中实例化 m_MonsterCards:

// Code #07

public Cards()
{
    m_MonsterCards 
= new List<MonsterCard>();

    
// 
}

或者有人会问:为什么不直接让客户端操作集合类而要这样大费周折呢?没错,公开集合类能够简化类的代码,但这样一来,客户端和 Cards 的耦合度就提高了,不便于我们将来修改 Cards 的实现。

 

5. Serialize Your Deck

5.1 Cards.Load

该方法的代码很简单:

// Code #08

public static Cards Load(string filename)
{
    
using (XmlReader reader = XmlReader.Create(filename))
    
{
        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)
{
    XmlWriterSettings settings 
= new XmlWriterSettings();
    settings.Indent 
= true;
    settings.IndentChars 
= "    ";

    
using (XmlWriter writer = XmlWriter.Create(filename, settings))
    
{
        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 的核心代码已经制作完毕:

cards.cs

对比之前那个版本,除了上面的一系列改动之外,我还去掉了所有类型的 SerializableAttribute,因为它与 XML Serialization 无关。当然,如果你将来希望把这些类型用于 Binary Serialization,你仍然可以保留它们。

另外,抽象类实现了 System.IComparable<Card> 接口,以便 List<T>.Remove 方法内部使用。那么,如何界定两张卡片的异同呢?在我印象中,卡片的名字是唯一的(我从没看过两张不同的卡片有着相同的名字),但为了安全起见,我特意咨询了 Upper Deck 官方,果然,卡片的名字是唯一的!这样 IComparable<Card>.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

 

posted @ 2005-10-24 20:13 Allen Lee 阅读(...) 评论(...) 编辑 收藏