Skip to content

.NET Core 实战 [No.267~283] 序列化

🏷️ 《.NET Core 实战》

序列化Serialization 也叫“串行化”),就是将某个对象实例的状态信息存储到可传输介质中,例如内存中、文件中以及通过网络发送的数据中。实例的状态信息包括 对象的属性字段成员的值不包括方法和事件)。

从可传输介质中读取数据,重新为对象的属性或字段成员赋值的过程称为反序列化Deserialization)。

现在用的最多的是 JSON 格式。毕竟这几年前端框架比较流行,而且 JavaScript 原生就支持 JSON 格式,很容易的就可以转换为对象处理。

第二种用的较多的是 XML 格式。功能上来说 XML 格式更强一些,相对的结构也就比较复杂,占用的空间也更多一些。

.NETWebServiceWebApi 这两种格式都是支持的。

最后一种不大常用的是 二进制序列化,如字面意思,就是将实例的状态信息以二进制的方式保存。优点是数据体积小,缺点是不便于在不同的网络平台之间传输。这个缺点比较明显,所以用的地方比较少。

二进制序列化

要让自定义类型支持二进制序列化,需要在类型上应用 SerializableAttribute

csharp
[Serializable]
class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

二进制序列化用到的是 BinaryFormatter 类,在 System.Runtime.Serialization.Formatters.Binary 命名空间下。

csharp
string fileName = "demo.data";
// 序列化到文件
using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate))
{
    BinaryFormatter ft = new BinaryFormatter();
    Person ps = new Person() {
        Name = "JiaJia",
        Age = 18,
    };
    ft.Serialize(fs, ps);
}
// 反序列化
using (FileStream fs = new FileStream(fileName, FileMode.Open))
{
    BinaryFormatter ft = new BinaryFormatter();
    Person ps = ft.Deserialize(fs) as Person;
    Console.WriteLine($"Name:{ps.Name}\nAge:{ps.Age}");
}

执行结果:

csharp
Name:JiaJia
Age:18

XML 序列化(XMLSerializer

XML 序列化(XMLSerializer)在 System.Xml.Serialization 命名空间下,用法同 BinaryFormatter 类似。但是 XMLSerializer 只能对 public 类型进行序列化,而且只序列化公共类型中的公共字段。另外,使用 XMLSerializer 时不需要在类型上应用 SerializableAttribute

csharp
public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}
csharp
using (MemoryStream ms = new MemoryStream())
{
    Person ps = new Person()
    {
        Name = "JiaJia",
        Age = 18,
    };
    // 序列化
    XmlSerializer sz = new XmlSerializer(ps.GetType());
    sz.Serialize(ms, ps);
    // 读取 XML 文档
    ms.Position = 0L;
    using (StreamReader reader = new StreamReader(ms, Encoding.UTF8, false, (int)ms.Length, true))
    {
        Console.WriteLine(reader.ReadToEnd());
    }
    // 反序列化
    ms.Position = 0L;
    XmlSerializer dsz = new XmlSerializer(typeof(Person));
    Person dps = dsz.Deserialize(ms) as Person;
    Console.WriteLine($"Name:{dps.Name}\nAge:{dps.Age}");
}

执行结果:

xml
<?xml version="1.0"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>JiaJia</Name>
  <Age>18</Age>
</Person>
Name:JiaJia
Age:18

使用 XMLSerializer 若需要对元素名等进行自定义,需要使用如下几个特性:

  • XmlRootAttribute:应用于类型,自定义根元素的名称(默认为类名);
  • XmlElementAttribute:应用于类型成员,自定义生成的元素名称(默认为类型成员名);
  • XmlArrayAttribute:应用于集合类型成员,自定义集合项的元素名称(默认为类型成员名);
  • XmlArrayItemAttribute:应用于集合类型成员,自定义集合项的子元素的名称(默认为集合成员的类型名);
  • XmlAttributeAttribute:应用于类型成员,将类型成员序列化为元素的属性;

上述特性都在 System.Xml.Serialization 命名空间下,而且都支持使用 Namespace 参数指定元素的命名空间。

下面示例展示了使用上述几种特性后的序列化结果。

csharp
[XmlRoot("Psn", Namespace = "liujiajia.me")]
public class Person
{
    [XmlElement("Nm", Namespace = "liujiajia.me/Name")]
    public string Name { get; set; }
    [XmlAttribute("Ag", Namespace = "liujiajia.me/Age")]
    public int Age { get; set; }
    [XmlArray("Hsts", Namespace = "liujiajia.me/Histories")]
    [XmlArrayItem("Hst", Namespace = "liujiajia.me/History")]
    public string[] Histories { get; set; }
}
xml
<?xml version="1.0"?>
<Psn xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" d1p1:Ag="18" xmlns:d1p1="liujiajia.me/Age" xmlns="liujiajia.me">
  <Nm xmlns="liujiajia.me/Name">JiaJia</Nm>
  <Hsts xmlns="liujiajia.me/Histories">
    <Hst xmlns="liujiajia.me/History">A</Hst>
    <Hst xmlns="liujiajia.me/History">B</Hst>
  </Hsts>
</Psn>

数据协定(DataContract

数据协定一种约定,它要求 参与约定的类型 以及 其成员结构 必须匹配,但 类型 以及 类型的成员名称 不一定相同

简单点说就是 序列化后的结构要相同

数据协定最大的作用是在网络传输中保证数据模型的统一

使用数据协定序列化为 XML 类型时使用 DataContractSerializer 类,序列化为 JSON 格式时使用 DataContractJsonSerializer 类。

两者的使用方法时相同的,调用 WriteObject 执行序列化,调用 ReadObject 执行反序列化。

使用数据协定时,序列化的类型需要满足下面要求中的一种:

  • 在类型上应用 DataContractAttribute,在需要序列化的成员上应用 DataMemberAttribute
  • 类型是 public 的,此时所有 public 的成员都将被序列化;

上述两个要求都不满足时,会报如下错误:

csharp
System.Runtime.Serialization.InvalidDataContractException
  HResult=0x80131500
  Message=Type 'DataContractDemo.Person' cannot be serialized. Consider marking it with the DataContractAttribute attribute, and marking all of its members you want serialized with the DataMemberAttribute attribute. Alternatively, you can ensure that the type is public and has a parameterless constructor - all public members of the type will then be serialized, and no attributes will be required.
  Source=System.Private.DataContractSerialization
  StackTrace:
   at System.Runtime.Serialization.DataContract.DataContractCriticalHelper.ThrowInvalidDataContractException(String message, Type type)
   at System.Runtime.Serialization.DataContract.DataContractCriticalHelper.CreateDataContract(Type type)
   at System.Runtime.Serialization.DataContract.DataContractCriticalHelper.CreateDataContract(Int32 id, RuntimeTypeHandle typeHandle, Type type)
   at System.Runtime.Serialization.DataContract.DataContractCriticalHelper.GetDataContractSkipValidation(Int32 id, RuntimeTypeHandle typeHandle, Type type)
   at System.Runtime.Serialization.DataContract.GetDataContract(RuntimeTypeHandle typeHandle, Type type, SerializationMode mode)
   at System.Runtime.Serialization.DataContractSerializer.get_RootContract()
   at System.Runtime.Serialization.DataContractSerializer.InternalWriteObject(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.XmlObjectSerializer.WriteObjectHandleExceptions(XmlWriterDelegator writer, Object graph, DataContractResolver dataContractResolver)
   at System.Runtime.Serialization.XmlObjectSerializer.WriteObject(XmlDictionaryWriter writer, Object graph)
   at System.Runtime.Serialization.XmlObjectSerializer.WriteObject(Stream stream, Object graph)
   at DataContractDemo.Program.Main(String[] args) in C:\Users\Administrator\source\repos\SerializationDemo\DataContractDemo\Program.cs:line 22

当使用 DataContractAttributeDataMemberAttribute 时,可以通过设置其 Name 属性的值来自定义序列化后的元素名。DataContractAttribute 还以设置 Namespace 参数来指定根元素的命名空间(由于 JSON 没有根元素,此时 NameNamespace 参数无效)。DataMemberAttribute 可以通过 Order 参数改变序列化时成员的顺序,默认是根据成员名称升序排列的。

另外还可以在成员上应用 IgnoreDataMemberAttribute 阻止成员被序列化。

csharp
[DataContract(Namespace = "liujiajia.me")]
public class Person
{
    [DataMember(Order = 1)]
    public string Name { get; set; }
    [DataMember(Order = 2)]
    public int Age { get; set; }
}

序列化为 XML 格式

csharp
using (MemoryStream ms = new MemoryStream())
{
    Person ps = new Person()
    {
        Name = "JiaJia",
        Age = 18,
    };
    // 序列化到内存流
    DataContractSerializer szr = new DataContractSerializer(ps.GetType());
    szr.WriteObject(ms, ps);
    // 读取序列化结果
    ms.Position = 0L;
    using (StreamReader reader = new StreamReader(ms, Encoding.UTF8, false, (int)ms.Length, true))
    {
        Console.WriteLine(reader.ReadToEnd());
    }
    // 反序列化
    ms.Position = 0L;
    DataContractSerializer dsz = new DataContractSerializer(typeof(Person));
    Person dps = dsz.ReadObject(ms) as Person;
    Console.WriteLine($"Name:{dps.Name}\nAge:{dps.Age}");
}

和使用 XMLSerializer 相比 DataContractSerializer 会删除 XML 文档中的空白字符,以压缩文档体积,便于网络传输。

xml
<Person xmlns="liujiajia.me" xmlns:i="http://www.w3.org/2001/XMLSchema-instance"><Name>JiaJia</Name><Age>18</Age></Person>
Name:JiaJia
Age:18

DataContractSerializer 支持保留实例引用,开启后,序列化时会为每个实例分配一个 id,以保证每个实例在序列化时只生成一次。如果某个实例被多个成员重复引用,那么之后实例第一次出现时才会填充数据,以缩减文档的长度。

csharp
using (MemoryStream ms = new MemoryStream())
{
    Person ps = new Person()
    {
        Name = "JiaJia",
        Age = 18,
    };
    List<Person> pss = new List<Person>()
    {
        ps,
        ps,
        ps,
    };
    // 序列化到内存流
    DataContractSerializerSettings settings = new DataContractSerializerSettings();
    // 开启保留实例引用
    settings.PreserveObjectReferences = true;
    DataContractSerializer szr = new DataContractSerializer(pss.GetType(), settings);
    szr.WriteObject(ms, pss);
    // 读取序列化结果
    ms.Position = 0L;
    using (StreamReader reader = new StreamReader(ms))
    {
        Console.WriteLine(reader.ReadToEnd());
    }
}

序列化结果如下:

xml
<ArrayOfPerson z:Id="1" z:Size="3" xmlns="liujiajia.me" xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns:z="http://schemas.microsoft.com/2003/10/Serialization/"><Person z:Id="2"><Name z:Id="3">JiaJia</Name><Age>18</Age></Person><Person z:Ref="2" i:nil="true"/><Person z:Ref="2" i:nil="true"/></ArrayOfPerson>

序列化为 JSON 格式

序列化为 JSON 格式使用 DataContractJsonSerializer 类,用法同 DataContractSerializer

csharp
using (MemoryStream ms = new MemoryStream())
{
    Person ps = new Person()
    {
        Name = "JiaJia",
        Age = 18,
    };
    // 序列化到内存流
    DataContractJsonSerializer szr = new DataContractJsonSerializer(ps.GetType());
    szr.WriteObject(ms, ps);
    // 读取序列化结果
    ms.Position = 0L;
    using (StreamReader reader = new StreamReader(ms, Encoding.UTF8, false, (int)ms.Length, true))
    {
        Console.WriteLine(reader.ReadToEnd());
    }
    // 反序列化
    ms.Position = 0L;
    DataContractJsonSerializer dsz = new DataContractJsonSerializer(typeof(Person));
    Person dps = dsz.ReadObject(ms) as Person;
    Console.WriteLine($"Name:{dps.Name}\nAge:{dps.Age}");
}

执行结果如下:

json
{"Name":"JiaJia","Age":18}
Name:JiaJia
Age:18

参考:《.NET Core 实战:手把手教你掌握 380 个精彩案例》 -- 周家安 著