1. 程式人生 > >C# 在.net中序列化讀寫xml方法的總結

C# 在.net中序列化讀寫xml方法的總結

XML是一種很常見的資料儲存方式,我經常用它來儲存一些資料,或者是一些配置引數。 使用C#,我們可以藉助.net framework提供的很多API來讀取或者建立修改這些XML, 然而,不同人使用XML的方法很有可能並不相同。 今天我打算談談我使用XML的一些方法,供大家參考。

回到頂部

最簡單的使用XML的方法

由於.net framework針對XML提供了很多API,這些API根據不同的使用場景實現了不同層次的封裝, 比如,我們可以直接使用XmlTextReader、XmlDocument、XPath來取數XML中的資料, 也可以使用LINQ TO XML或者反序列化的方法從XML中讀取資料。 那麼,使用哪種方法最簡單呢?

我個人傾向於使用序列化,反序列化的方法來使用XML。 採用這種方法,我只要考慮如何定義資料型別就可以了,讀寫XML各只需要一行呼叫即可完成。 例如:

// 1. 首先要建立或者得到一個數據物件
Order order = GetOrderById(123);


// 2. 用序列化的方法生成XML
string xml = XmlHelper.XmlSerialize(order, Encoding.UTF8);


// 3. 從XML讀取資料並生成物件
Order order2 = XmlHelper.XmlDeserialize<Order>(xml, Encoding.UTF8);

就是這麼簡單的事情,XML結構是什麼樣的,我根本不用關心, 我只關心資料是否能儲存以及下次是否能將它們讀取出來。

說明:XmlHelper是一個工具類,全部原始碼如下: 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Serialization;
using System.IO;
using System.Xml;

namespace MyMVC
{
    public static class XmlHelper
    {
        private static void 
XmlSerializeInternal(Stream stream, object o, Encoding encoding) { if( o == null ) throw new ArgumentNullException("o"); if( encoding == null ) throw new ArgumentNullException("encoding"); XmlSerializer serializer = new XmlSerializer(o.GetType()); XmlWriterSettings settings = new XmlWriterSettings(); settings.Indent = true; settings.NewLineChars = "\r\n"; settings.Encoding = encoding; settings.IndentChars = " "; using( XmlWriter writer = XmlWriter.Create(stream, settings) ) { serializer.Serialize(writer, o); writer.Close(); } } /// <summary> /// 將一個物件序列化為XML字串 /// </summary> /// <param name="o">要序列化的物件</param> /// <param name="encoding">編碼方式</param> /// <returns>序列化產生的XML字串</returns> public static string XmlSerialize(object o, Encoding encoding) { using( MemoryStream stream = new MemoryStream() ) { XmlSerializeInternal(stream, o, encoding); stream.Position = 0; using( StreamReader reader = new StreamReader(stream, encoding) ) { return reader.ReadToEnd(); } } } /// <summary> /// 將一個物件按XML序列化的方式寫入到一個檔案 /// </summary> /// <param name="o">要序列化的物件</param> /// <param name="path">儲存檔案路徑</param> /// <param name="encoding">編碼方式</param> public static void XmlSerializeToFile(object o, string path, Encoding encoding) { if( string.IsNullOrEmpty(path) ) throw new ArgumentNullException("path"); using( FileStream file = new FileStream(path, FileMode.Create, FileAccess.Write) ) { XmlSerializeInternal(file, o, encoding); } } /// <summary> /// 從XML字串中反序列化物件 /// </summary> /// <typeparam name="T">結果物件型別</typeparam> /// <param name="s">包含物件的XML字串</param> /// <param name="encoding">編碼方式</param> /// <returns>反序列化得到的物件</returns> public static T XmlDeserialize<T>(string s, Encoding encoding) { if( string.IsNullOrEmpty(s) ) throw new ArgumentNullException("s"); if( encoding == null ) throw new ArgumentNullException("encoding"); XmlSerializer mySerializer = new XmlSerializer(typeof(T)); using( MemoryStream ms = new MemoryStream(encoding.GetBytes(s)) ) { using( StreamReader sr = new StreamReader(ms, encoding) ) { return (T)mySerializer.Deserialize(sr); } } } /// <summary> /// 讀入一個檔案,並按XML的方式反序列化物件。 /// </summary> /// <typeparam name="T">結果物件型別</typeparam> /// <param name="path">檔案路徑</param> /// <param name="encoding">編碼方式</param> /// <returns>反序列化得到的物件</returns> public static T XmlDeserializeFromFile<T>(string path, Encoding encoding) { if( string.IsNullOrEmpty(path) ) throw new ArgumentNullException("path"); if( encoding == null ) throw new ArgumentNullException("encoding"); string xml = File.ReadAllText(path, encoding); return XmlDeserialize<T>(xml, encoding); } } }

或許有人會說:我使用XPath從XML讀取資料也很簡單啊。
我認為這種說法有一個限制條件:只需要從XML中讀取少量的資料。
如果要全部讀取,用這種方法會寫出一大堆的機械程式碼出來! 所以,我非常反感用這種方法從XML中讀取全部資料。

回到頂部

型別定義與XML結構的對映

如果是一個新專案,我肯定會毫不猶豫的使用序列化和反序列化的方法來使用XML, 然而,有時在維護一個老專案時,面對一堆只有XML卻沒有與之對應的C#型別時, 我們就需要根據XML結構來逆向推導C#型別,然後才能使用序列化和反序列化的方法。 逆向推導的過程是麻煩的,不過,型別推匯出來之後,後面的事情就簡單多了。

為了學會根據XML結構逆向推導型別,我們需要關注一下型別定義與XML結構的對映關係。
注意:有時候我們也會考慮XML結構對於傳輸量及可閱讀性的影響,所以關注一下XML也是有必要的。

這裡有一個XML檔案,是我從Visual Sutdio的安裝目錄中找到的: 

怎樣用反序列化的方式來讀取它的資料呢,我在部落格的最後將給出完整的實現程式碼。
現在,我們還是看一下這個XML有哪些特點吧。

<LinkGroup ID="sites" Title="Venus Sites" Priority="1500">

對於這個節點來說,它包含了三個資料項(屬性):ID,Title,Priority。 這樣的LinkGroup節點有三個。
類似的還有Glyph節點。

<LItem URL="http://www.asp.net" LinkGroup="sites">ASP.NET Home Page</LItem>

LItem節點除了與LinkGroup有著類似的資料(屬性)之外,還包含著一個字串:ASP.NET Home Page , 這是另外一種資料的存放方式。

另外,LinkGroup和LItem都允許重複出現,我們可以用陣列或者列表(Array,List)來理解它們。

我還發現一些巢狀關係:LinkGroup可以包含Glyph,Context包含著Links,Links又包含了多個LItem。
不管如何巢狀,我發現數據都是包含在一個一個的XML節點中。

如果用專業的單詞來描述它們,我們可以將ID,Title,Priority這三個資料項稱為 XmlAttribute, LItem,LinkGroup節點稱為 XmlElement,”ASP.NET Home Page“出現的位置可以稱為 InnerText。 基本上,XML就是由這三類資料組成。

下面我來演示如何使用這三種資料項。

回到頂部

使用 XmlElement

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

public class Class1
{
    public int IntValue { get; set; }

    public string StrValue { get; set; }
}

下面是序列化與反序列的呼叫程式碼:

Class1 c1 = new Class1 { IntValue = 3, StrValue = "Fish Li" };
string xml = XmlHelper.XmlSerialize(c1, Encoding.UTF8);
Console.WriteLine(xml);

Console.WriteLine("---------------------------------------");

Class1 c2 = XmlHelper.XmlDeserialize<Class1>(xml, Encoding.UTF8);
Console.WriteLine("IntValue: " + c2.IntValue.ToString());
Console.WriteLine("StrValue: " + c2.StrValue);

執行結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Class1 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <IntValue>3</IntValue>
    <StrValue>Fish Li</StrValue>
</Class1>
---------------------------------------
IntValue: 3
StrValue: Fish Li

結果顯示,IntValue和StrValue這二個屬性生成了XmlElement。

小結:預設情況下(不加任何Attribute),型別中的屬性或者欄位,都會生成XmlElement。

回到頂部

使用 XmlAttribute

再來定義一個型別:

public class Class2
{
    [XmlAttribute]
    public int IntValue { get; set; }

    [XmlElement]
    public string StrValue { get; set; }
}

注意,我在二個屬性上增加的不同的Attribute.

下面是序列化與反序列的呼叫程式碼: 

Class2 c1 = new Class2 { IntValue = 3, StrValue = "Fish Li" };
string xml = XmlHelper.XmlSerialize(c1, Encoding.UTF8);
Console.WriteLine(xml);

Console.WriteLine("---------------------------------------");

Class2 c2 = XmlHelper.XmlDeserialize<Class2>(xml, Encoding.UTF8);
Console.WriteLine("IntValue: " + c2.IntValue.ToString());
Console.WriteLine("StrValue: " + c2.StrValue);

執行結果如下(我將結果做了換行處理):

<?xml version="1.0" encoding="utf-8"?>
<Class2 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
        IntValue="3">
    <StrValue>Fish Li</StrValue>
</Class2>
---------------------------------------
IntValue: 3
StrValue: Fish Li

結果顯示:
1. IntValue 生成了XmlAttribute
2. StrValue 生成了XmlElement(和不加[XmlElement]的效果一樣,表示就是預設行為)。

小結:如果希望型別中的屬性或者欄位生成XmlAttribute,需要在型別的成員上用[XmlAttribute]來指出。

回到頂部

使用 InnerText

還是來定義一個型別:

public class Class3
{
    [XmlAttribute]
    public int IntValue { get; set; }

    [XmlText]
    public string StrValue { get; set; }
}

注意,我在StrValue上增加的不同的Attribute.

下面是序列化與反序列的呼叫程式碼: 

Class3 c1 = new Class3 { IntValue = 3, StrValue = "Fish Li" };
string xml = XmlHelper.XmlSerialize(c1, Encoding.UTF8);
Console.WriteLine(xml);

Console.WriteLine("---------------------------------------");

Class3 c2 = XmlHelper.XmlDeserialize<Class3>(xml, Encoding.UTF8);
Console.WriteLine("IntValue: " + c2.IntValue.ToString());
Console.WriteLine("StrValue: " + c2.StrValue);

執行結果如下(我將結果做了換行處理):

<?xml version="1.0" encoding="utf-8"?>
<Class3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    IntValue="3">Fish Li</Class3>
---------------------------------------
IntValue: 3
StrValue: Fish Li

結果符合預期:StrValue屬性在增加了[XmlText]之後,生成了一個文字節點(InnerText)

小結:如果希望型別中的屬性或者欄位生成InnerText,需要在型別的成員上用[XmlText]來指出。

回到頂部

重新命名節點名稱

看過前面幾個示例,大家應該能發現:通過序列化得到的XmlElement和XmlAttribute都與型別的資料成員或者型別同名。 然而有時候我們可以希望讓屬性名與XML的節點名稱不一樣,那麼就要使用【重新命名】的功能了,請看以下示例:

[XmlType("c4")]
public class Class4
{
    [XmlAttribute("id")]
    public int IntValue { get; set; }

    [XmlElement("name")]
    public string StrValue { get; set; }
}

序列化與反序列的呼叫程式碼前面已經多次看到,這裡就省略它們了。
執行結果如下(我將結果做了換行處理):

<?xml version="1.0" encoding="utf-8"?>
<c4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" 
    id="3">
    <name>Fish Li</name>
</c4>
---------------------------------------
IntValue: 3
StrValue: Fish Li

看看輸出結果中的紅字粗體字,再看看型別定義中的三個Attribute的三個字串引數,我想你能發現規律的。

小結:XmlAttribute,XmlElement允許接受一個別名用來控制生成節點的名稱,型別的重新命名用XmlType來實現。

回到頂部

列表和陣列的序列化

繼續看示例程式碼:

Class4 c1 = new Class4 { IntValue = 3, StrValue = "Fish Li" };
Class4 c2 = new Class4 { IntValue = 4, StrValue = "http://www.cnblogs.com/fish-li/" };

// 說明:下面二行程式碼的輸出結果是一樣的。
List<Class4> list = new List<Class4> { c1, c2 };
//Class4[] list = new Class4[] { c1, c2 };

string xml = XmlHelper.XmlSerialize(list, Encoding.UTF8);
Console.WriteLine(xml);

// 序列化的結果,反序列化一定能讀取,所以就不再測試反序列化了。

執行結果如下:

<?xml version="1.0" encoding="utf-8"?>
<ArrayOfC4 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <c4 id="3">
        <name>Fish Li</name>
    </c4>
    <c4 id="4">
        <name>http://www.cnblogs.com/fish-li/</name>
    </c4>
</ArrayOfC4>

現在c4節點已經重複出現了,顯然,是我們期待的結果。

不過,ArrayOfC4,這個節點名看起來太奇怪了,能不能給它也重新命名呢?
繼續看程式碼,我可以定義一個新的型別:

// 二種Attribute都可以完成同樣的功能。
//[XmlType("c4List")]
[XmlRoot("c4List")]
public class Class4List : List<Class4> { }

然後,改一下呼叫程式碼:

Class4List list = new Class4List { c1, c2 };

執行結果如下:

<?xml version="1.0" encoding="utf-8"?>
<c4List xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <c4 id="3">
        <name>Fish Li</name>
    </c4>
    <c4 id="4">
        <name>http://www.cnblogs.com/fish-li/</name>
    </c4>
</c4List>

小結:陣列和列表都能直接序列化,如果要重新命名根節點名稱,需要建立一個新型別來實現。

回到頂部

列表和陣列的做為資料成員的序列化

首先,還是定義一個型別:

public class Root
{
    public Class3 Class3 { get; set; }

    public List<Class2> List { get; set; }
}

序列化的呼叫程式碼:

Class2 c1 = new Class2 { IntValue = 3, StrValue = "Fish Li" };
Class2 c2 = new Class2 { IntValue = 4, StrValue = "http://www.cnblogs.com/fish-li/" };

Class3 c3 = new Class3 { IntValue = 5, StrValue = "Test List" };

Root root = new Root { Class3 = c3, List = new List<Class2> { c1, c2 } };

string xml = XmlHelper.XmlSerialize(root, Encoding.UTF8);
Console.WriteLine(xml);

執行結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Class3 IntValue="5">Test List</Class3>
    <List>
        <Class2 IntValue="3">
            <StrValue>Fish Li</StrValue>
        </Class2>
        <Class2 IntValue="4">
            <StrValue>http://www.cnblogs.com/fish-li/</StrValue>
        </Class2>
    </List>
</Root>

假設這裡需要為List和Class2的節點重新命名,該怎麼辦呢?
如果繼續使用前面介紹的方法,是行不通的。

下面的程式碼演示瞭如何重新命名列表節點的名稱:

public class Root
{
    public Class3 Class3 { get; set; }

    [XmlArrayItem("c2")]
    [XmlArray("cccccccccccc")]
    public List<Class2> List { get; set; }
}

序列化的呼叫程式碼與前面完全一樣,得到的輸出結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Class3 IntValue="5">Test List</Class3>
    <cccccccccccc>
        <c2 IntValue="3">
            <StrValue>Fish Li</StrValue>
        </c2>
        <c2 IntValue="4">
            <StrValue>http://www.cnblogs.com/fish-li/</StrValue>
        </c2>
    </cccccccccccc>
</Root>

想不想把cccccccccccc節點去掉呢(直接出現c2節點)?
下面的型別定義方式實現了這個想法:

public class Root
{
    public Class3 Class3 { get; set; }

    [XmlElement("c2")]
    public List<Class2> List { get; set; }
}

輸出結果如下:

<?xml version="1.0" encoding="utf-8"?>
<Root xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <Class3 IntValue="5">Test List</Class3>
    <c2 IntValue="3">
        <StrValue>Fish Li</StrValue>
    </c2>
    <c2 IntValue="4">
        <StrValue>http://www.cnblogs.com/fish-li/</StrValue>
    </c2>
</Root>

小結:陣列和列表都在序列化時,預設情況下會根據型別中的資料成員名稱生成一個節點, 列表項會生成子節點,如果要重新命名,可以使用[XmlArrayItem]和[XmlArray]來實現。 還可以直接用[XmlElement]控制不生成列表的父節點。

回到頂部

型別繼承與反序列化

列表元素可以是同一種類型,也可以不是同一種類型(某個型別的派生類)。
例如下面的XML:

<?xml version="1.0" encoding="utf-8"?>
<XRoot xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <List>
        <x1 aa="1" bb="2" />
        <x1 aa="3" bb="4" />
        <x2>
            <cc>ccccccccccc</cc>
            <dd>dddddddddddd</dd>
        </x2>
    </List>
</XRoot>

想像一下,上面這段XML是通過什麼型別得到的呢?

答案如下(注意紅色粗體部分):

public class XBase { }

[XmlType("x1")]
public class X1 : XBase
{
    [XmlAttribute("aa")]
    public int AA { get; set; }

    [XmlAttribute("bb")]
    public int BB { get; set; }
}

[XmlType("x2")]
public class X2 :