.NET 中的序列化和反序列化(DataContract 序列化)

.NET 中的序列化和反序列化(DataContract 序列化)

.NET Framework 中的序列号和反序列化方式多样,支持 4 种原生的序列化方式:

特性 数据契约序列化器 二进制序列化器 XmlSerializer IXmlSerializable
自动化级别 ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐
类型耦合 可选 紧密 松散 松散
版本容错性 ⭐⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
是否保留对象引用 可选 可选
是否可以序列化非公有字段
是否适用于互操作消息 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐⭐
读取XML文件的灵活性 ⭐⭐ - ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐
输出精简程度 ⭐⭐ ⭐⭐⭐⭐ ⭐⭐ ⭐⭐
性能 ⭐⭐ ⭐⭐⭐⭐ ⭐到⭐⭐⭐ ⭐⭐⭐

本次我们讲解数据契约序列化(DataContract)。

数据契约序列化

在 .NET Framework 中,数据契约序列化是微软推荐的序列化方式(见8.10 序列化(第二版) - hihaojie - 博客园)。它的使用非常方便,我们先从一个简单用例开始。

简单用例:序列化与反序列化

假设我们有如下 Person​ 类:

public class Person
{
    public string Name { get; set; }
    public int Age => (DateTime.Now - Birthday).Days / 365;
    public DateTime Birthday { get; set; }
}

我们对它的一个实例进行序列化,有:

Person person = new Person
{
    Name = "John",
    Birthday = new DateTime(2000, 1, 1)
};

var sb = new StringBuilder();
var settings = new XmlWriterSettings
{
    Indent = true
};
using (var xmlWriter = XmlWriter.Create(sb, settings))
{
    var serializer = new DataContractSerializer(typeof(Person));
    serializer.WriteObject(xmlWriter, person);
}
Console.WriteLine(sb.ToString());
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Birthday>2000-01-01T00:00:00</Birthday>
  <Name>John</Name>
</Person

可以看到,数据契约序列化还智能得将只读属性排除在外。

我们将上述字符串进行反序列化,则有:

string xmlString = sb.ToString();
using (var xmlReader = XmlReader.Create(new StringReader(xmlString)))
{
    var serializer = new DataContractSerializer(typeof(Person));
    Person p = (Person)serializer.ReadObject(xmlReader);
    Console.WriteLine($"名字:{p.Name},年龄:{p.Age}");
}

忽略不想序列化的成员

有时我们想忽略部分成员不进行序列化。此时可以通过 IgnoreDataMember​ 特性实现。

以如下代码为例,我们希望 Age​ 属性不进行序列化,标记 IgnoreDataMember​ 特性即可忽略:

public class Person
{
    public string Name { get; set; }
    [IgnoreDataMember]
    public int Age { get; set; }
}
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Name>John</Name>
</Person>

自定义构造器后的报错

现在,我们对 Person​ 类适当改造,简化成员并添加一个有参构造器:

public class Person
{
    public string Name { get; set; }
    
    public Person(string name)
    {
        Name = name;
    }
}

此时再进行序列化却会发生报错,抛出异常 InvalidDataContractException​!

这是因为有参构造器的出现让序列化器不知道如何进行序列化。此时需要 DataContract​ 标记类可以序列化,需要 DataMember​ 标记那些成员进行序列化。

修复后的代码如下:

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name)
    {
        Name = name;
    }
}
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Name>John</Name>
</Person>

在此情况下,未标记 DataMember​ 特性的成员将不进行序列化。

序列化只读成员

前面我们有提到:“数据契约序列化还智能得将只读属性排除在外”。以如下代码为例,序列化时会抛出 InvalidDataContractException​ 异常:

Person p = new Person(1){
    Name = "John"
};

var sb = new StringBuilder();
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(Person));
    serializer.WriteObject(xmlWriter, p);
}
Console.WriteLine(sb.ToString());

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public int Id { get; }

    public Person(int id)
    {
        Id = id;
    }
}

如果想对只读属性也进行序列化,可以实例化 DataContractSerializerSettings​ 类型成员,并令其 SerializeReadOnlyTypes​ 属性为 true,将其传入 DataContractSerializer​ 构造器,标记序列化时也对只读属性进行序列化。

修改后打代码如下,此时再进行序列化,xml 内容会包含 Id 信息:

Person p = new Person(1){
    Name = "John"
};

var sb = new StringBuilder();
var serialiazerSettings = new DataContractSerializerSettings
{
    SerializeReadOnlyTypes = true
};
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(Person), serialiazerSettings);
    serializer.WriteObject(xmlWriter, p);
}
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Id>1</Id>
  <Name>John</Name>
</Person>

自定义命名空间和元素名称

在前面的例子我们可以看到,数据契约序列化会默认使用命名空间“xmlns="http://schemas.datacontract.org/2004/07/"​”,元素名称和 C# 代码一致。我们可以通过标记 DataContract​ 和 DataMember​ 特性,将其自定义成自己想要的内容。

以如下代码为例,序列化后的 XML 命名空间是“xmlns="HiHaojie.DataContractTest"​”,Person​ 类对应的元素名是 MyPerson​,Name 对应的元素名是 MyName​:

[DataContract(Namespace = "HiHaojie.DataContractTest", Name = "MyPerson")]
public class Person
{
    [DataMember(Name = "MyName")]
    public string Name { get; set; }
}
<?xml version="1.0" encoding="utf-16"?>
<MyPerson xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="HiHaojie.DataContractTest">
  <MyName>John</MyName>
</MyPerson>

序列化时的成员顺序

数据契约序列化的成员顺序遵循如下准则:

  1. 先序列化基类成员,再序列化子类成员
  2. 字母表顺序,使用字符串的序数(ordinal)进行排序
  3. 从低 Order 值到高 Order 值(仅限被 Order 修饰的数据成员)

以如下代码为例,先序列化了基类成员 Age​、Name​,再序列化子类成员,而子类成员按照字母表顺序,先序列化没有标注 Order​ 的 Father​、Mother​,再按 Order​ 标注顺序序列化 Grade​、Id​,

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public int Age { get; set; }
}

[DataContract]
public class Student : Person
{
    [DataMember(Order = 2)]
    public int Grade { get; set; }

    [DataMember(Order = 1)]
    public int Id { get; set; }

    [DataMember]
    public string Father { get; set; }

    [DataMember]
    public string Mother { get; set; }
}
<?xml version="1.0" encoding="utf-16"?>
<Student xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Age>20</Age>
  <Name>John Smith</Name>
  <Address>221 Baker Street</Address>
  <Father>Henry Smith</Father>
  <Mother>Marry Smith</Mother>
  <Id>1</Id>
  <Grade>3</Grade>
</Student>

需要注意的是,反序列化也会按照上述规则执行,对于未匹配到的成员会保持默认值。

反序列化时的成员检查

假设有这样一个场景:要进行反序列化的 xml 字符串必须包含 Person​ 实例的 Id​,否则应当抛出异常。

在序列化之后再对成员进行检查显然很繁琐,为此 DataMember​ 支持通过 IsRequired​ 设置某个成员反序列化必须存在。

以如下代码为例,因为 xml 不存在 Id​ 内容,因此抛出异常 SerializationException​:

string xmlString =
"""
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Name>John Smith</Name>
</Person>
""";
using (var xmlReader = XmlReader.Create(new StringReader(xmlString)))
{
    var serializer = new DataContractSerializer(typeof(Person));
    Person p = (Person)serializer.ReadObject(xmlReader);
}

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; }

    [DataMember(IsRequired = true)]
    public int Id { get; set; }
}

处理循环依赖

以如下代码为例,我们为 Person​ 类添加了一个 Partner​ 属性,为 Person​ 类型。我们还定义了 person1​ 和 person2​ 两个实例,二者相互引用:

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public Person Partner { get; set; }
}
Person person1 = new Person
{
    Name = "John",
};
Person person2 = new Person
{
    Name = "Marry",
    Partner = person1
};
person1.Partner = person2;
List<Person> persons = [person1, person2];

var sb = new StringBuilder();
var settings = new XmlWriterSettings
{
    Indent = true
};
using (var xmlWriter = XmlWriter.Create(sb, settings))
{
    var serializer = new DataContractSerializer(typeof(List<Person>));
    serializer.WriteObject(xmlWriter, persons);
}
Console.WriteLine(sb.ToString());

实际执行时,因出现循环依赖,该代码会发生报错,抛出异常 SerializationException​。

有两种方式可以解决此问题。

方式一:通过 DataContract(IsReference = true)​ 标记循环引用

我们可以通过 DataContract(IsReference = true)​ 告知序列化器追踪引用信息,此时序列化不会报错:

[DataContract(IsReference = true)]
public class Person
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public Person Partner { get; set; }
}
<?xml version="1.0" encoding="utf-16"?>
<ArrayOfPerson xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Person z:Id="i1" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/">
    <Name>John</Name>
    <Partner z:Id="i2">
      <Name>Marry</Name>
      <Partner z:Ref="i1" />
    </Partner>
  </Person>
  <Person z:Ref="i2" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" />
</ArrayOfPerson>

反序列化时循环依赖的状态也能正常恢复:

string content = sb.ToString();
using (var xmlReader = XmlReader.Create(new StringReader(content)))
{
    var serializer = new DataContractSerializer(typeof(List<Person>));
    List<Person> value = (List<Person>)serializer.ReadObject(xmlReader);
    foreach (var person in value)
    {
        // 依次输出 John、Marry、Marry、John
        Console.WriteLine(person.Name);
        Console.WriteLine(person.Partner.Name);
    }
}

方式二:通过 DataContractSerializerSettings.PreserveObjectReferences​ 标记循环引用

对于第三方的,无法标记特性的类,上一节通过DataContract(IsReference = true)​ 进行标记的方案便无法实施。此时我们可以通过 DataContractSerializerSettings​ 的 PreserveObjectReferences​ 属性为 true,令 DataContract 追踪引用信息。

以如下代码为例,它与方式一的序列化效果一致:

Person person1 = new Person
{
    Name = "John",
};
Person person2 = new Person
{
    Name = "Marry",
    Partner = person1
};
person1.Partner = person2;
List<Person> persons = [person1, person2];

var sb = new StringBuilder();
var serializerSetting = new DataContractSerializerSettings
{
    PreserveObjectReferences = true
};
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(List<Person>), serializerSetting);
    serializer.WriteObject(xmlWriter, persons);
}
Console.WriteLine(sb.ToString());

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public Person Partner { get; set; }
}

多态成员的序列化与反序列化

假设我们有三个类:Person​、Student​、Teacher​,其中 Person​ 是另外两个的基类。我们定义一个 List<Person>​ 变量 persons​,并为它添加 Student​、Teacher​ 实例。因为不知道参与序列化的类型有哪些,此时序列化 persons​ 变量会发生报错,抛出异常 SerializationException​:

[DataContract]
public abstract class Person
{
    [DataMember]
    public string Name { get; set; }
}

[DataContract]
public class Teacher : Person
{
    [DataMember]
    public string Subject { get; set; }
}

[DataContract]
public class Student : Person
{
    [DataMember]
    public int Grade { get; set; }
}
Person student = new Student
{
    Name = "John",
    Grade = 1
};
Person teacher = new Teacher
{
    Name = "Marry",
    Subject = "Math"
};
List<Person> persons = [student, teacher];

var sb = new StringBuilder();
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(List<Person>));
    serializer.WriteObject(xmlWriter, persons);
}
Console.WriteLine(sb.ToString());

为此有两种解决方案:

方案一:通过 KnownTypeAttribute​ 在基类标注子类类型

我们可以通过 KnownTypeAttribute​ 为基类标注子类类型,告知序列化器可能存在的子类。Person​ 类改为如下即可:

[DataContract]
[KnownType(typeof(Student))]
[KnownType(typeof(Teacher))]
public abstract class Person
{
    [DataMember]
    public string Name { get; set; }
}

KnownTypeAttribute​ 还支持传入返回类型是 Type[]​ 的方法名称,通过该方法获取子类类型。需要注意的是:该方法需要是静态的、:

[DataContract]
[KnownType(nameof(GetSubclassType))]
public abstract class Person
{
    [DataMember]
    public string Name { get; set; }

    public static Type[] GetSubclassType()
    {
        return new Type[] { typeof(Teacher), typeof(Student) };
    }
}

方案二:通过 DataContractSerializer​ 构造器传入参与序列化的类型

DataContractSerializer​ 的若干构造器中,部分构造器支持传入 IEnumerable<Type>​ 类型实例,借此告知序列化器存在哪些子类:

Type[] types = [typeof(Student), typeof(Teacher)];
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(List<Person>), types);
    serializer.WriteObject(xmlWriter, persons);
}
Console.WriteLine(sb.ToString());

省心的数据契约序列化:NetDataContractSerializer

上面两节中,对于循环依赖、多态类型都需要我们手动去标注。有没有什么方法省去手动标记呢?答案是使用 NetDataContractSerializer​ 进行序列化。

以如下 Person​、Student​、Teacher​ 类型为例,我们没有为其标记 DataContractAttribute​、KnownType​,并且实例之间相互引用,使用 NetDataContractSerializer​ 仍然可以正常进行序列化、反序列化:

public abstract class Person
{
    public string Name { get; set; }

    public Person Partner { get; set; }
}

public class Teacher : Person
{
    public string Subject { get; set; }
}

public class Student : Person
{
    public int Grade { get; set; }
}
Person student = new Student
{
    Name = "John",
};
Person teacher = new Teacher
{
    Name = "Marry",
    Partner = student
};
student.Partner = teacher;
List<Person> persons = new List<Person> { student, teacher };

var sb = new StringBuilder();
var settings = new XmlWriterSettings
{
    Indent = true
};
using (var xmlWriter = XmlWriter.Create(sb, settings))
{
    var serializer = new NetDataContractSerializer();
    serializer.WriteObject(xmlWriter, persons);
}
Console.WriteLine(sb.ToString());

using (var xmlWriter = XmlReader.Create(new StringReader(sb.ToString())))
{
    var serializer = new NetDataContractSerializer();
    var list = (List<Person>)serializer.ReadObject(xmlWriter);
    foreach (var person in list)
    {
        Console.WriteLine(person.Name);
        Console.WriteLine(person.Partner.Name);
    }
}
<?xml version="1.0" encoding="utf-16"?>
<ArrayOfUserQuery.Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" z:Id="1" z:Type="System.Collections.Generic.List`1[[UserQuery+Person, query_gpasuf, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]" z:Assembly="0" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/" xmlns="http://schemas.datacontract.org/2004/07/">
  <_items z:Id="2" z:Size="4">
    <UserQuery.Person z:Id="3" z:Type="UserQuery+Student" z:Assembly="query_gpasuf, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
      <Name z:Id="4">John</Name>
      <Partner z:Id="5" z:Type="UserQuery+Teacher" z:Assembly="query_gpasuf, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
        <Name z:Id="6">Marry</Name>
        <Partner z:Ref="3" i:nil="true" />
        <Subject i:nil="true" />
      </Partner>
      <Grade>0</Grade>
    </UserQuery.Person>
    <UserQuery.Person z:Ref="5" i:nil="true" />
    <UserQuery.Person i:nil="true" />
    <UserQuery.Person i:nil="true" />
  </_items>
  <_size>2</_size>
  <_version>2</_version>
</ArrayOfUserQuery.Person>

Notice

NetDataContractSerializer​ 仅在 .NET Framework 支持,新版 .NET 出于安全角度不再支持。

序列化/反序列化的钩子方法

如果要在序列化之前或者序列化之后执行一个自定义方法,则可以在该方法上标记以下特性:

  • 序列化

    • [OnSerializing]​:在序列化之前调用这个方法。
    • [OnSerialized]​:在序列化之后调用这个方法。
  • 反序列化

    • [OnDeserializing]​:在反序列化之前调用这个方法。
    • [OnDeserialized]​:在反序列化之后调用这个方法。

这些方法只能包含一个 StreamingContext​ 类型的参数,该参数数据契约序列化器并不使用,仅用于保证和二进制引擎的兼容性。

用这四个特性修饰的方法可以是私有的。如果子类型需要参与其中,那么它们可以使用相同的特性来标记自己的方法,这些方法同样会得到执行

Notice

上述方法并不需要 SerializationInfo​ 参数,仅当类型实现 ISerializable​ 接口时,其构造器、GetObjectData​ 方法需要该参数。

Info

更多内容见第17章 序列化 - hihaojie - 博客园的17.4.1 序列化与反序列化钩子、8.10 序列化(第二版) - hihaojie - 博客园的2 对数据协定序列化的支持

OnDeserializedAttribute​ 的使用

DataContract 反序列化很特别的一点是:它不会使用目标类型的构造器创建相应实例。

假设我们有如下 Person​ 类和对应的 xml 内容,反序列化时我们可以注意到“调用构造器”这行内容并未输出,p.Name​ 属性值为 null,p.Age​ 属性值为 0:

string xmlContent =
"""
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
</Person>
""";
using (var xmlReader = XmlReader.Create(new StringReader(xmlContent)))
{
    var serializer = new DataContractSerializer(typeof(Person));
    Person p = (Person)serializer.ReadObject(xmlReader);
    Console.WriteLine(p.Age);
    Console.WriteLine(p.Name is null);
}

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; } = string.Empty;

    [DataMember]
    public int Age { get; set; }
    
    public Person()
    {
        Age = 1;
        Console.WriteLine("调用构造器。");
    }
}

有时我们想让属性成员具有初始值,上述行为显然不是我们想要的。此时我们可以定义一个方法,在该方法中进行初始化操作。该方法需要具有 StreamingContext​ 参数,并用 OnDeserialized​ 特性标注。将代码修改为如下形式后,“OnDeserialized方法被调用。”这句内容会输出,p.Name​ 不再为 null,p.Age​ 值为 1:

[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; } = string.Empty;

    [DataMember]
    public int Age { get; set; }
    
    public Person()
    {
        Age = 1;
        Console.WriteLine("调用构造器。");
    }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        Console.WriteLine($"{nameof(OnDeserialized)}方法被调用。");
        if (Name is null)
        {
            Name = string.Empty;
        }
        if (Age <= 0)
        {
            Age = 1;
        }
    }
}

Info

DataContract​ 序列化引擎无需无参构造器便能实例化,是借助 FormatterServices.GetUninitializedObject()​ 方法实现的。在新版 .NET 中,该方法已被标记为弃用,推荐使用 RuntimeHelpers.GetUninitializedObject(Type)​ 方法。

另见:

OnSerializing​ 的使用

在本节提到的 4 个钩子方法中,最常用的便是上一节的 OnDeserialized​。OnSerializing​ 方法我仅在一种场景下用过:序列化时兼容矩阵数组。

假设我们有如下 Command​ 类型,内含一个 Params​ 属性成员,为 object[]​ 类型。我通过该类型实例的序列化字符串在两个程序间传递数据:

[DataContract]
[KnownType(typeof(int[,]))]
public class Command
{
    [DataMember]
    public object[] Params { get; set; }
}

DataContract 不支持多维数组,当我想序列化多维数组时会发生报错。以如下代码为例,运行时会抛出 NotSupportException​:

string methodName = "AddPoints";
int[,] matrix =
    {
        { 0, 0 }, { 1, 0 },
        { 0, 1 }, { 1, 1 }
    };

Command command = new Command{
    Params = [methodName, matrix]
};

var sb = new StringBuilder();
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(Command));
    serializer.WriteObject(xmlWriter, command);
}
Console.WriteLine(sb.ToString());

string xmlContent = sb.ToString();
using (var xmlReader = XmlReader.Create(new StringReader(xmlContent)))
{
    var serializer = new DataContractSerializer(typeof(Command));
    Command value = (Command)serializer.ReadObject(xmlReader);
    Console.WriteLine(value.Params[0]);
    Console.WriteLine(value.Params[1].GetType());
}

已知我们仅会用到二维数组,不会用到交错数组(Jagged Array),有什么办法可以做到传输二维数组呢?是的,我们可以借助 OnSerializing​ 和 OnDeserialized​ 实现:

  1. OnSerializing​:在序列化前,将二位数组转为交错数组
  2. OnDeserialized​:在反序列化后,将交错数组转为二维数组

为此 Command​ 类需要改成这样:

[DataContract]
[KnownType(typeof(int[][]))]
public class Command
{
    [DataMember]
    public object[] Params { get; set; }

    [OnDeserialized]
    private void OnDeserialized(StreamingContext context)
    {
        for (int i = 0; i < Params.Length; i++)
        {
            if (Params[i] is not int[][] jaggedArray){
                continue;
            }
            Params[i] = ToMatrix(jaggedArray);
        }
    }

    [OnSerializing]
    private void OnSerializing(StreamingContext context)
    {
        for (int i = 0; i < Params.Length; i++)
        {
            if (Params[i] is not int[,] matrix){
                continue;
            }
            Params[i] = ToJaggedArray(matrix);
        }
    }

    private int[,] ToMatrix(int[][] array)
    {
        int[,] matrix = new int[array.Length, array.First().Length];
        for (int i = 0; i < matrix.GetLength(0); i++)
        {
            for (int j = 0; j < matrix.GetLength(1); j++)
            {
                matrix[i, j] = array[i][j];
            }
        }
        return matrix;
    }

    private int[][] ToJaggedArray(int[,] matrix)
    {
        int[][] array = new int[matrix.GetLength(0)][];
        int dimension = matrix.GetLength(1);
        for (int i = 0; i < array.Length; i++)
        {
            array[i] = new int[dimension];
            for (int j = 0; j < dimension; j++)
            {
                array[i][j] = matrix[i, j];
            }
        }
        return array;
    }
}

此时序列化将得到如下 xml 内容,反序列化后也能正常得到二维数组:

<?xml version="1.0" encoding="utf-16"?>
<Command xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Params xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
    <d2p1:anyType xmlns:d3p1="http://www.w3.org/2001/XMLSchema" i:type="d3p1:string">AddPoints</d2p1:anyType>
    <d2p1:anyType i:type="d2p1:ArrayOfArrayOfint">
      <d2p1:ArrayOfint>
        <d2p1:int>0</d2p1:int>
        <d2p1:int>0</d2p1:int>
      </d2p1:ArrayOfint>
      <d2p1:ArrayOfint>
        <d2p1:int>1</d2p1:int>
        <d2p1:int>0</d2p1:int>
      </d2p1:ArrayOfint>
      <d2p1:ArrayOfint>
        <d2p1:int>0</d2p1:int>
        <d2p1:int>1</d2p1:int>
      </d2p1:ArrayOfint>
      <d2p1:ArrayOfint>
        <d2p1:int>1</d2p1:int>
        <d2p1:int>1</d2p1:int>
      </d2p1:ArrayOfint>
    </d2p1:anyType>
  </Params>
</Command>

自定义集合类型的序列化

DataContract 是否支持自定义集合的序列化呢?答案是肯定的。以如下代码为例 MyCollection​ 为自定义集合类型,派生自 Collection<int>​,其序列化/反序列化对应的结果如下:

public class MyCollection : Collection<int>
{
    protected override void InsertItem(int index, int item)
    {
        if (Items.Contains(item))
        {
            throw new InvalidOperationException();
        }
        base.InsertItem(index, item);
    }
}
MyCollection collection = [1, 2, 3];

var sb = new StringBuilder();
var setting = new XmlWriterSettings{ Indent = true };
using (var xmlWriter = XmlWriter.Create(sb, setting))
{
    var serializer = new DataContractSerializer(typeof(MyCollection));
    serializer.WriteObject(xmlWriter, collection);
}
Console.WriteLine(sb.ToString());

string xmlContent = sb.ToString();
using (var xmlReader = XmlReader.Create(new StringReader(xmlContent)))
{
    var serializer = new DataContractSerializer(typeof(MyCollection));
    var elements = (MyCollection)serializer.ReadObject(xmlReader);
    foreach (var element in elements)
    {
        // 可以正常输出 1 2 3
        Console.WriteLine(element);
    }
}
<?xml version="1.0" encoding="utf-16"?>
<ArrayOfint xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
  <int>1</int>
  <int>2</int>
  <int>3</int>
</ArrayOfint>

当我们想修改自定义集合的 Name​、Namespace​、IsReference​、KeyName​、ValueName​ 这些关键内容时,就不能通过 DataContract 了,而是要使用 CollectionDataContract​。以如下代码为例,我们对 MyCollection​ 的 Name​、Namespace​ 进行了修改:

[CollectionDataContract(Name = "CustomCollection",  Namespace = "HiHaojie.DataContractTest")]
public class MyCollection : Collection<int>
{
    protected override void InsertItem(int index, int item)
    {
        if (Items.Contains(item))
        {
            throw new InvalidOperationException();
        }
        base.InsertItem(index, item);
    }
}
<?xml version="1.0" encoding="utf-16"?>
<CustomCollection xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="HiHaojie.DataContractTest">
  <int>1</int>
  <int>2</int>
  <int>3</int>
</CustomCollection>

Tips

KeyName​ 和 ValueName​ 用于自定义字典设定 Key、Value 的名称。

版本容错

Person​ 类进行了一次更迭,老版本的 Person​ 类只包含 Name​ 属性。新版本的 Person 添加了 Id​ 属性。对应的代码如下:

// 旧版 Person 类
[DataContract]
public class Person 
{
    [DataMember]
    public string Name { get; set; }
}
// 新版 Person 类
[DataContract]
public class Person
{
    [DataMember]
    public string Name { get; set; }

    [DataMember]
    public int Id { get; set; }
}

假设有如下场景:

  1. 我们有服务器 A,中继站 B,客户端 C。A、C 使用新版 Person​ 类,B 使用因未及时更新,使用旧版 Person​ 类。
  2. 数据要从 A 传递给 B,B 反序列化得到 Person 的实例并进行加工,加工后再序列化为 xml 传递给 C。

在 B 加工数据时,如何保证 Id​ 信息不丢失呢?答案是使用 IExtensibleDataObject​ 接口。该接口支持反序列化时存储不存在的成员。

以如下代码为例,xml 字符串的内容包含了 Id,Person​ 类不包含 Id 属性,反序列化得到的 p 也不包含 Id,但对它进行序列化后,得到的 xml 字符串却仍然包含 Id:

[DataContract]
public class Person : IExtensibleDataObject
{
    [DataMember]
    public string Name { get; set; }

    ExtensionDataObject? IExtensibleDataObject.ExtensionData { get; set; }
}
string xmlString =
"""
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Id>1</Id>
  <Name>John Smith</Name>
</Person>
""";
Person p;
using (var xmlReader = XmlReader.Create(new StringReader(xmlString)))
{
    var serializer = new DataContractSerializer(typeof(Person));
    p = (Person)serializer.ReadObject(xmlReader);
    Console.WriteLine(p.Name);
}
var sb = new StringBuilder();
var settings = new XmlWriterSettings
{
    Indent = true
};
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(Person));
    serializer.WriteObject(xmlWriter, p);
}
Console.WriteLine(sb.ToString());

Info

更多内容见2 对数据协定序列化的支持17.2.5 版本容错性

忽略版本容错

DataContractSerializerSettings​ 拥有属性 IgnoreExtensionDataObject​,为 bool​ 类型。令其为 true 后序列化将忽略 IExtensibleDataObject​ 所存储的成员。

以如下代码为例,再次序列化得到的 xml 将不含 Id 信息:

string xmlString =
"""
<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
  <Id>1</Id>
  <Name>John Smith</Name>
</Person>
""";
Person p;
var serialiazerSettings = new DataContractSerializerSettings
{
    IgnoreExtensionDataObject = true
};

using (var xmlReader = XmlReader.Create(new StringReader(xmlString)))
{
    var serializer = new DataContractSerializer(typeof(Person), serialiazerSettings);
    p = (Person)serializer.ReadObject(xmlReader);
    Console.WriteLine(p.Name);
}
var sb = new StringBuilder();
var settings = new XmlWriterSettings
{
    Indent = true
};
using (var xmlWriter = XmlWriter.Create(sb))
{
    var serializer = new DataContractSerializer(typeof(Person));
    serializer.WriteObject(xmlWriter, p);
}
// 输出:
// <?xml version="1.0" encoding="utf-16"?>
// <Person xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/">
//   <Name>John Smith</Name>
// </Person>
Console.WriteLine(sb.ToString());

[DataContract]
public class Person : IExtensibleDataObject
{
    [DataMember]
    public string Name { get; set; }

    ExtensionDataObject? IExtensibleDataObject.ExtensionData { get; set; }
}

总结

本篇涉及的类型、特性如下:

DataContractSerializer​ 和 NetDataContractSerializer

两种 DataContract 序列化器。

DataContractAttribute

该特性用于标记类型,告知序列化器该类型参与序列化。它的主要属性有:

  • Namespace​:用于设置当前类型序列化后对应的 xmlns;
  • Name​:用于设置当前类型序列化后在 xml 中的元素名称;
  • IsReference​:用于设置当前类型序列化时十分追踪引用信息。

CollectionDataContract

该特性用于标记自定义集合类型,告知序列化器如何序列化内部成员。主要属性有:

  • Name​:用于设置当前类型序列化后在 xml 中的元素名称;
  • Namespace​:用于设置当前类型序列化后对应的 xmlns;
  • IsReference​:用于设置当前类型序列化时十分追踪引用信息;
  • KeyName​:用于自定义字典类型,设定字典中 key 的名称
  • ValueName​:用于自定义字典类型,设定字典中 value 的名称

DataMemberAttribute

该特性用于标记成员是否可以序列化。它的主要属性有:

  • Name​:用于设置当前成员在 xml 字符串中的名称
  • Order​:用于设置序列化时各成员顺序
  • IsRequired​:用于设置该成员是否必须存在。若不存在,反序列化时将抛出异常。

IgnoreDataMember

该特性用于标记属性,用于告知序列化器忽略该成员。

KnownTypeAttribute

该特性用于标记类型,用于告知序列化器它有哪些子类会参与序列化。

除了使用该特性,我们也可以直接向 DataContractSerializer​ 构造器中传入要参数序列化类型的 Type。

DataContractSerializerSettings

该类型用于设置序列化的一些规则,主要属性有:

  • SerializeReadOnlyTypes​ 属性:用于设置是否序列化只读属性;
  • PreserveObjectReferences​ 属性:用于设置十分追踪引用信息;
  • IgnoreExtensionDataObject​ 属性:用于设置是否忽略版本容错;

IExtensibleDataObject​ 接口

该接口用于版本容错。

钩子方法

序列化

  • [OnSerializing]​:在序列化之前调用被该特性修饰的方法。
  • [OnSerialized]​:在序列化之后调用被该特性修饰的方法。

反序列化

  • [OnDeserializing]​:在反序列化之前调用被该特性修饰的方法。
  • [OnDeserialized]​:在反序列化之后调用被该特性修饰的方法。

参考文献

  1. 《框架设计指南:构建可复用.NET库的约定、惯例与模式》第二版
  2. 《C#7.0 核心技术指南》

Info

上述两本书的部分内容,可参阅我的阅读笔记阅读笔记目录汇总 - hihaojie - 博客园

posted @ 2025-06-28 13:10  hihaojie  阅读(68)  评论(0)    收藏  举报