.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>
序列化时的成员顺序
数据契约序列化的成员顺序遵循如下准则:
- 先序列化基类成员,再序列化子类成员
- 字母表顺序,使用字符串的序数(ordinal)进行排序
- 从低 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 实现:
-
OnSerializing:在序列化前,将二位数组转为交错数组 -
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; }
}
假设有如下场景:
- 我们有服务器 A,中继站 B,客户端 C。A、C 使用新版
Person 类,B 使用因未及时更新,使用旧版Person 类。 - 数据要从 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
忽略版本容错
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]:在反序列化之后调用被该特性修饰的方法。
参考文献
- 《框架设计指南:构建可复用.NET库的约定、惯例与模式》第二版
- 《C#7.0 核心技术指南》
Info
上述两本书的部分内容,可参阅我的阅读笔记阅读笔记目录汇总 - hihaojie - 博客园

浙公网安备 33010602011771号