CodeGym /课程 /C# SELF /处理嵌套对象和集合

处理嵌套对象和集合

C# SELF
第 45 级 , 课程 1
可用

1. 介绍

简单对象的序列化就像寄明信片:很简单,不会丢东西。但很多时候我们的对象变成了“家庭相册”:里面有嵌套对象、集合,甚至集合里又套集合。

想象一下,用户不仅仅是一个有名字和年龄的 Person,还可能有一组联系人 (Contact)、多个家庭/工作地址,或者一个 List<Pet> 字段(如果他/她喜欢宠物)。这些都是嵌套对象和集合。

.NET 的序列化器如何处理这些?当我们序列化复杂对象树时需要注意什么?如果集合里又有集合,会不会全乱掉?今天我们来弄清楚应该期待什么——以及在复杂场景下该怎么做。

什么是 .NET 里的“嵌套”

嵌套对象就是作为你的主对象的一个属性或字段的另一个对象。比如,下面是扩展后的用户模型示例:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public List<Contact> Contacts { get; set; }         // 嵌套对象的集合
    public Address? HomeAddress { get; set; }           // 一个嵌套对象(可为空)
}
public class Contact
{
    public string Type { get; set; }                    // 比如 Email 或 Phone
    public string Value { get; set; }
}
public class Address
{
    public string City { get; set; }
    public string Street { get; set; }
}

注意 —— 人可能有多个联系人,而地址只有一个(并且可能是 null)。这是很多模型的常见模式。

2. 使用 System.Text.Json 序列化对象和集合

简单示例:序列化并反序列化一个对象

来看看实际是怎么工作的:

using System.Text.Json;

var person = new Person
{
    Name = "安娜",
    Age = 28,
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "anna@example.com" },
        new Contact { Type = "Phone", Value = "+1234567890" }
    },
    HomeAddress = new Address { City = "柏林", Street = "亚历山大广场, 1" }
};

string json = JsonSerializer.Serialize(person, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

结果大概如下:

{
  "Name": "安娜",
  "Age": 28,
  "Contacts": [
    {
      "Type": "Email",
      "Value": "anna@example.com"
    },
    {
      "Type": "Phone",
      "Value": "+1234567890"
    }
  ],
  "HomeAddress": {
    "City": "柏林",
    "Street": "亚历山大广场, 1"
  }
}

序列化器能很好地理解嵌套。如果属性是另一个对象,它会把它作为嵌套对象序列化;如果是列表,就序列化为数组。

事实:只要这些类型是 public 并且有 public 的 getter/setter(get/set),这种序列化就是自动工作的。

反序列化:开箱即用

string jsonInput = /* JSON,我们刚刚得到了 */;
Person deserializedPerson = JsonSerializer.Deserialize<Person>(jsonInput);
Console.WriteLine(deserializedPerson.Name); // "安娜"
Console.WriteLine(deserializedPerson.Contacts[0].Type); // "Email"

一切正常 —— 联系人会从 JSON 数组变回 List<Contact>,HomeAddress 会变回 Address 类型的对象。

3. 集合如何被序列化:List、数组、Dictionary

你的模型里常常不只有列表(List<T>),还有字典(Dictionary<TKey, TValue>)或多维数组。

数组示例

public class Team
{
    public string Name { get; set; }
    public Person[] Members { get; set; }
}

var team = new Team
{
    Name = "开发者",
    Members = new[]
    {
        new Person { Name = "阿列克谢", Age = 31 },
        new Person { Name = "叶卡捷琳娜", Age = 27 }
    }
};
string json = JsonSerializer.Serialize(team, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Name": "开发者",
  "Members": [
    {
      "Name": "阿列克谢",
      "Age": 31,
      "Contacts": null,
      "HomeAddress": null
    },
    {
      "Name": "叶卡捷琳娜",
      "Age": 27,
      "Contacts": null,
      "HomeAddress": null
    }
  ]
}

集合的序列化就像把一堆相同类型的明信片打包:每个元素是一张“明信片”。

Dictionary 示例

public class Phonebook
{
    public Dictionary<string, string> Phones { get; set; }
}

var phonebook = new Phonebook
{
    Phones = new Dictionary<string, string>
    {
        { "安德烈", "+12998887766" },
        { "玛丽亚", "+12882223344" }
    }
};

string json = JsonSerializer.Serialize(phonebook, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "Phones": {
    "安德烈": "+12998887766",
    "玛丽亚": "+12882223344"
  }
}

在 JSON 中,字典变成了一个具有动态键的对象。

4. 嵌套集合(在 List 里再套 List,数组的数组)

.NET 能序列化这种“套娃”结构:

public class Zoo
{
    public List<List<Animal>> AnimalGroups { get; set; }
}
public class Animal { public string Name { get; set; } }

var zoo = new Zoo
{
    AnimalGroups = new List<List<Animal>>
    {
        new List<Animal> { new Animal { Name = "狮子" }, new Animal { Name = "老虎" } },
        new List<Animal> { new Animal { Name = "熊" } }
    }
};

string json = JsonSerializer.Serialize(zoo, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine(json);

JSON:

{
  "AnimalGroups": [
    [ { "Name": "狮子" }, { "Name": "老虎" } ],
    [ { "Name": "熊" } ]
  ]
}

反序列化时同样可以原样还原。

5. 在 XmlSerializer 中序列化嵌套对象的特性

序列化嵌套对象示例

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

var person = new Person
{
    Name = "伊万",
    Age = 35,
    HomeAddress = new Address { City = "波恩", Street = "贝多芬街, 100" },
    Contacts = new List<Contact>
    {
        new Contact { Type = "Email", Value = "ivan@domain.de" }
    }
};

var serializer = new XmlSerializer(typeof(Person));
using var writer = new StringWriter();
serializer.Serialize(writer, person);
Console.WriteLine(writer.ToString());

结果:

<?xml version="1.0" encoding="utf-16"?>
<Person xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Name>伊万</Name>
  <Age>35</Age>
  <Contacts>
    <Contact>
      <Type>Email</Type>
      <Value>ivan@domain.de</Value>
    </Contact>
  </Contacts>
  <HomeAddress>
    <City>波恩</City>
    <Street>贝多芬街, 100</Street>
  </HomeAddress>
</Person>

XmlSerializer 对集合的处理

默认情况下,XmlSerializer 会把集合序列化为一个名为 "Contacts" 的标签,里面每个元素都会生成一个单独的 "Contact" 标签。为此集合类型必须是 public,且元素类型必须可序列化。

重要点。 如果集合为空,XML 仍然会包含一个空标签:

<Contacts />

支持的集合。 XmlSerializer 支持 List<T>、数组 T[]、实现了 ICollection<T> 的集合。但通常不支持 Dictionary<TKey, TValue>!要序列化字典,得用把字典包成键值对列表或使用其它序列化库等技巧。

6. Newtonsoft.Json 对对象和集合的支持

Newtonsoft.Json 在处理嵌套时更灵活一些。99% 的情况它和 System.Text.Json 的行为类似,但有优势:可以序列化私有字段(如果显式设置)、支持键更复杂的字典、dynamic 类型,甚至可以处理循环引用(通过一些配置)。

示例

using Newtonsoft.Json;

var person = new Person
{
    Name = "帕维尔",
    Age = 40,
    Contacts = new List<Contact>
    {
        new Contact { Type = "SMS", Value = "+10000000013" }
    }
};
string json = JsonConvert.SerializeObject(person, Formatting.Indented);
Console.WriteLine(json);

不同点。 如果对象里有私有字段,可以通过设置 ContractResolver 来序列化它们。但对于基本的嵌套集合和对象,一切同样是开箱即用的。

7. 常见错误、陷阱和注意点

错误 1:嵌套对象为 null
如果你序列化的对象某些属性没填(null),在 JSON 或 XML 中这些属性可能不会出现,或者会被写成 null。例如,如果 Person 的 HomeAddress 为 null,在 JSON 中会是:

"HomeAddress": null

在 XML 中默认不会出现该标签 —— 可以通过序列化器设置来包含它(比如 [XmlElement(IsNullable=true)])。

错误 2:私有属性/字段的序列化。
默认情况下大多数序列化器(JSON 和 XML)只处理 public 属性。如果你想序列化私有或受保护的字段,需要把它们设为 public 或者通过序列化器配置来启用(例如在 Newtonsoft.Json 里用 ContractResolver)。

错误 3:没有无参构造函数的类。
在 XML 序列化(以及很多 JSON 场景)中,类通常需要一个 public 的无参构造函数。如果没有,会在反序列化时抛异常。

错误 4:在 XML 中序列化字典。
XmlSerializer 不能直接序列化 Dictionary<TKey, TValue>。这经常让人意外。解决办法是把字典包成键值对列表,或者用其它序列化工具。

错误 5:循环引用。
如果一个对象通过引用包含另一个对象,而另一个对象又引用回第一个(比如父和子之间),循环序列化会导致栈溢出(StackOverflowException)或关于循环引用的异常。JSON 序列化器可以通过配置绕开,但通常更好的做法是重新考虑对象设计。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION