MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

C#序列化与反序列化(JSON、XML、二进制)方案

2022-03-124.5k 阅读

C#序列化与反序列化基础概念

在C#编程领域,序列化是将对象的状态转换为可以存储或传输的形式的过程,而反序列化则是相反的操作,即将存储或传输的数据重新转换回对象。这一机制在很多场景中都至关重要,例如将对象保存到文件中,在网络上传输对象数据,或者在不同的应用程序域之间共享对象状态。

在C#中,常见的序列化格式有JSON、XML和二进制。每种格式都有其特点,适用于不同的场景。JSON以其简洁、轻量的特点,在Web应用和移动应用开发中广泛应用,特别是在前后端数据交互场景。XML则以其良好的结构化和可读性,常用于配置文件以及与传统企业系统交互。二进制格式虽然可读性差,但在性能和空间占用上表现优异,适用于对性能要求极高的内部系统数据传输和存储。

JSON序列化与反序列化

使用System.Text.Json命名空间(.NET 5及更高版本)

从.NET 5开始,微软引入了System.Text.Json命名空间,提供了高性能的JSON序列化和反序列化功能。

首先,确保项目引用了System.Text.Json。在一个简单的控制台应用中,我们定义一个类来进行演示:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

接下来进行JSON序列化:

using System.Text.Json;

class Program
{
    static void Main()
    {
        var person = new Person { Name = "Alice", Age = 30 };
        string jsonString = JsonSerializer.Serialize(person);
        Console.WriteLine(jsonString);
    }
}

上述代码通过JsonSerializer.Serialize方法将Person对象转换为JSON字符串。输出结果类似:{"Name":"Alice","Age":30}

反序列化操作如下:

using System.Text.Json;

class Program
{
    static void Main()
    {
        string jsonString = "{\"Name\":\"Alice\",\"Age\":30}";
        Person person = JsonSerializer.Deserialize<Person>(jsonString);
        if (person != null)
        {
            Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
        }
    }
}

这里通过JsonSerializer.Deserialize方法将JSON字符串转换回Person对象。

使用Newtonsoft.Json(跨版本兼容性好)

在.NET 5之前,或者在需要更好的跨版本兼容性时,Newtonsoft.Json是一个非常流行的JSON处理库。

首先通过NuGet安装Newtonsoft.Json包。同样以Person类为例,序列化代码如下:

using Newtonsoft.Json;

class Program
{
    static void Main()
    {
        var person = new Person { Name = "Bob", Age = 25 };
        string jsonString = JsonConvert.SerializeObject(person);
        Console.WriteLine(jsonString);
    }
}

反序列化代码如下:

using Newtonsoft.Json;

class Program
{
    static void Main()
    {
        string jsonString = "{\"Name\":\"Bob\",\"Age\":25}";
        Person person = JsonConvert.DeserializeObject<Person>(jsonString);
        if (person != null)
        {
            Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
        }
    }
}

Newtonsoft.Json提供了非常丰富的配置选项,例如可以通过JsonSerializerSettings来控制序列化和反序列化的行为,比如日期格式、忽略某些属性等。

XML序列化与反序列化

使用System.Xml.Serialization命名空间

在C#中,System.Xml.Serialization命名空间提供了对XML序列化和反序列化的支持。

对于前面定义的Person类,要使其可被XML序列化,需要确保类具有公共的无参构造函数(如果自定义了构造函数,必须同时提供无参构造函数):

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Person() { }
}

序列化代码如下:

using System.Xml.Serialization;
using System.IO;

class Program
{
    static void Main()
    {
        var person = new Person { Name = "Charlie", Age = 35 };
        XmlSerializer serializer = new XmlSerializer(typeof(Person));
        using (StreamWriter writer = new StreamWriter("person.xml"))
        {
            serializer.Serialize(writer, person);
        }
    }
}

上述代码将Person对象序列化为XML并保存到person.xml文件中。生成的XML文件内容类似:

<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Charlie</Name>
  <Age>35</Age>
</Person>

反序列化代码如下:

using System.Xml.Serialization;
using System.IO;

class Program
{
    static void Main()
    {
        XmlSerializer serializer = new XmlSerializer(typeof(Person));
        using (StreamReader reader = new StreamReader("person.xml"))
        {
            Person person = (Person)serializer.Deserialize(reader);
            if (person != null)
            {
                Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
            }
        }
    }
}

控制XML结构

可以使用属性来控制XML的生成结构。例如,XmlElement属性可以用于指定元素名称,XmlAttribute属性用于指定属性。

public class Person
{
    [XmlElement("FullName")]
    public string Name { get; set; }
    [XmlAttribute("PersonAge")]
    public int Age { get; set; }
    public Person() { }
}

序列化后生成的XML如下:

<?xml version="1.0" encoding="utf-8"?>
<Person PersonAge="40" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <FullName>David</FullName>
</Person>

二进制序列化与反序列化

使用System.Runtime.Serialization.Formatters.Binary命名空间

二进制序列化在C#中通过System.Runtime.Serialization.Formatters.Binary命名空间实现。要进行二进制序列化,类必须标记为[Serializable]

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

序列化代码如下:

using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

class Program
{
    static void Main()
    {
        var person = new Person { Name = "Eve", Age = 28 };
        BinaryFormatter formatter = new BinaryFormatter();
        using (FileStream stream = new FileStream("person.bin", FileMode.Create))
        {
            formatter.Serialize(stream, person);
        }
    }
}

上述代码将Person对象序列化为二进制文件person.bin

反序列化代码如下:

using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

class Program
{
    static void Main()
    {
        BinaryFormatter formatter = new BinaryFormatter();
        using (FileStream stream = new FileStream("person.bin", FileMode.Open))
        {
            Person person = (Person)formatter.Deserialize(stream);
            if (person != null)
            {
                Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
            }
        }
    }
}

注意事项

二进制序列化依赖于类型的完全匹配,包括程序集版本等信息。如果类型定义发生了不兼容的变化,反序列化可能会失败。例如,如果在序列化后给类添加了一个新的非可序列化字段,反序列化可能会出现问题。为了应对这种情况,可以使用[NonSerialized]属性标记那些不应该被序列化的字段,并且在类型发生变化时,合理处理版本兼容性问题,比如使用序列化代理。

复杂对象的序列化与反序列化

嵌套对象

在实际应用中,对象往往是复杂的,包含嵌套对象。例如,我们定义一个Address类,并在Person类中包含Address对象:

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Address HomeAddress { get; set; }
    public Person() { }
}

对于JSON序列化,无论是System.Text.Json还是Newtonsoft.Json,都能自动处理嵌套对象。例如,使用System.Text.Json

using System.Text.Json;

class Program
{
    static void Main()
    {
        var address = new Address { Street = "123 Main St", City = "Anytown" };
        var person = new Person { Name = "Frank", Age = 32, HomeAddress = address };
        string jsonString = JsonSerializer.Serialize(person);
        Console.WriteLine(jsonString);
    }
}

输出结果类似:{"Name":"Frank","Age":32,"HomeAddress":{"Street":"123 Main St","City":"Anytown"}}

对于XML序列化,同样能处理嵌套对象:

using System.Xml.Serialization;
using System.IO;

class Program
{
    static void Main()
    {
        var address = new Address { Street = "456 Elm St", City = "Othercity" };
        var person = new Person { Name = "Grace", Age = 29, HomeAddress = address };
        XmlSerializer serializer = new XmlSerializer(typeof(Person));
        using (StreamWriter writer = new StreamWriter("person.xml"))
        {
            serializer.Serialize(writer, person);
        }
    }
}

生成的XML如下:

<?xml version="1.0" encoding="utf-8"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>Grace</Name>
  <Age>29</Age>
  <HomeAddress>
    <Street>456 Elm St</Street>
    <City>Othercity</City>
  </HomeAddress>
</Person>

二进制序列化也能很好地处理嵌套对象,只要嵌套的对象也标记为[Serializable]

[Serializable]
public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
}

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

然后进行二进制序列化和反序列化操作与之前简单对象的操作类似。

集合对象

处理集合对象也是常见的需求。例如,我们有一个包含多个Person对象的列表:

using System.Collections.Generic;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public Person() { }
}

class Program
{
    static void Main()
    {
        var people = new List<Person>
        {
            new Person { Name = "Hank", Age = 35 },
            new Person { Name = "Ivy", Age = 27 }
        };
        // JSON序列化
        string jsonString = JsonSerializer.Serialize(people);
        Console.WriteLine(jsonString);
        // XML序列化
        XmlSerializer serializer = new XmlSerializer(typeof(List<Person>));
        using (StreamWriter writer = new StreamWriter("people.xml"))
        {
            serializer.Serialize(writer, people);
        }
        // 二进制序列化
        BinaryFormatter formatter = new BinaryFormatter();
        using (FileStream stream = new FileStream("people.bin", FileMode.Create))
        {
            formatter.Serialize(stream, people);
        }
    }
}

JSON序列化后的结果是一个包含多个Person对象的JSON数组。XML序列化会生成一个根元素,包含多个Person子元素。二进制序列化则将整个列表序列化为二进制数据。

反序列化操作也类似,将相应格式的数据转换回集合对象。例如,JSON反序列化:

string jsonString = "[{\"Name\":\"Hank\",\"Age\":35},{\"Name\":\"Ivy\",\"Age\":27}]";
List<Person> people = JsonSerializer.Deserialize<List<Person>>(jsonString);
if (people != null)
{
    foreach (var person in people)
    {
        Console.WriteLine($"Name: {person.Name}, Age: {person.Age}");
    }
}

性能与优化

JSON序列化性能优化

System.Text.Json中,通过使用Utf8JsonWriterUtf8JsonReader可以进一步提升性能,特别是在处理大量数据时。例如,手动使用Utf8JsonWriter进行序列化:

using System.Text.Json;
using System.IO;

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

class Program
{
    static void Main()
    {
        var person = new Person { Name = "Jack", Age = 30 };
        using (MemoryStream stream = new MemoryStream())
        {
            using (Utf8JsonWriter writer = new Utf8JsonWriter(stream))
            {
                writer.WriteStartObject();
                writer.WriteString("Name", person.Name);
                writer.WriteNumber("Age", person.Age);
                writer.WriteEndObject();
            }
            string jsonString = System.Text.Encoding.UTF8.GetString(stream.ToArray());
            Console.WriteLine(jsonString);
        }
    }
}

这种方式避免了反射带来的性能开销,在性能敏感的场景中表现更好。

XML序列化性能优化

对于XML序列化,可以通过预生成序列化器来提升性能。xsd.exe工具可以根据XML Schema生成C#类和序列化代码。另外,减少不必要的属性和复杂的XML结构定义也能提升序列化和反序列化的速度。

二进制序列化性能优化

二进制序列化本身性能较高,但在处理大型对象图时,可能会遇到性能问题。可以通过优化对象结构,减少不必要的字段,并且合理使用[NonSerialized]属性来避免序列化不必要的数据,从而提升性能。同时,在反序列化时,注意内存管理,避免大量对象同时加载导致内存溢出。

安全性考虑

JSON序列化安全

在使用JSON反序列化时,要注意防止JSON注入攻击。例如,恶意用户可能构造恶意的JSON数据,导致反序列化过程中执行恶意代码。使用System.Text.Json时,默认情况下对反序列化有一定的安全性保护。但在使用Newtonsoft.Json时,需要注意配置JsonSerializerSettings,例如设置ObjectCreationHandling = ObjectCreationHandling.RejectNull来防止创建空对象,避免潜在的安全风险。

XML序列化安全

XML序列化存在XXE(XML外部实体注入)风险。要防范这种风险,在反序列化XML时,禁用外部实体解析。例如,在XmlReaderSettings中设置ProhibitDtd = trueXmlResolver = null

XmlReaderSettings settings = new XmlReaderSettings
{
    ProhibitDtd = true,
    XmlResolver = null
};
using (XmlReader reader = XmlReader.Create("person.xml", settings))
{
    XmlSerializer serializer = new XmlSerializer(typeof(Person));
    Person person = (Person)serializer.Deserialize(reader);
}

二进制序列化安全

二进制序列化可能存在反序列化漏洞,例如利用二进制格式的特性进行恶意代码执行。为了安全,要确保只从可信来源进行二进制数据的反序列化,并且对反序列化的类型进行严格验证,避免反序列化不可信的类型。

跨平台与兼容性

JSON跨平台兼容性

JSON是一种跨平台的格式,几乎所有现代编程语言都有JSON处理库。在C#中生成的JSON数据可以很方便地在其他平台如JavaScript、Java、Python等中进行反序列化。同样,其他平台生成的JSON数据也能在C#中顺利反序列化,只要数据结构匹配。

XML跨平台兼容性

XML也是一种广泛支持的跨平台格式。许多平台都有XML处理库,并且XML Schema提供了一种定义数据结构的标准方式,使得不同平台之间可以基于相同的Schema进行数据交互。不过,由于XML的复杂性,在不同平台之间处理XML时,可能需要注意命名空间、编码等细节问题。

二进制序列化跨平台兼容性

二进制序列化在C#中生成的二进制数据通常只能在.NET环境中进行反序列化,因为它依赖于.NET特定的类型信息和格式。如果需要跨平台传输数据,二进制序列化不是一个好的选择,除非目标平台也有相应的.NET兼容环境或专门的二进制格式解析库。

在实际应用中,根据项目的需求,如是否需要与其他平台交互、性能要求、数据可读性等,合理选择序列化与反序列化方案至关重要。每种方案都有其优势和适用场景,通过深入理解和实践,可以充分发挥它们的作用,构建高效、安全且可互操作的应用程序。