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; }           // 一個巢狀物件(可為 nullable)
}
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; }
}

注意 — 一個人可能有多個 Contacts,但地址只有一個(而且它可能是 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"

一切都會運作——Contacts 會從 JSON 陣列變回 List<Contact>HomeAddress 會變回 Address 類別的物件。

3. 集合如何被序列化: List, 陣列, Dictionary

在你的模型裡常見的集合不只 List(List<T>),也可能有 Dictionary(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 中,Dictionary 會變成一個具有動態鍵的物件。

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 的行為一樣,但有些優勢:可以序列化私有欄位(如果你明確設定)、支援帶複雜鍵的字典、支援動態型別,甚至可以透過設定處理循環參考。

範例

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。例如,如果 HomeAddressPersonnull,在 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:循環參考。
如果物件 A 透過參考包含物件 B,而 B 又回頭參考 A(像父母/子女的雙向參考),序列化會「繞圈」,可能導致 StackOverflowException(堆疊溢位)或拋出循環參考的例外。某些 JSON 序列化器可以透過設定來處理,但更好的方式通常是重新檢視物件設計。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION