CodeGym /課程 /C# SELF /字典的序列化

字典的序列化

C# SELF
等級 46 , 課堂 1
開放

1. 引言

字典(或在 C# 裡的 Dictionary<TKey, TValue>)是「鍵-值」配對的集合。當你需要通過唯一標識快速查找值時(例如在電話簿裡按名字找電話),這種類型非常好用。

跟列表(List<T>)不同,列表元素順序有意義,而字典專注於通過鍵快速訪問資料。但序列化列表比較直觀(就是 JSON 陣列),序列化字典會有一些細節需要注意:

  • 鍵必須是可序列化的類型(通常是字串,但有時也可能是數字或其他簡單類型)。
  • JSON 裡沒有「字典」這個單獨類型 — 只有物件或陣列。

我們接下來詳細看看 .NET 怎麼序列化字典、可能遇到的問題,以及如何讓程式正確處理這類結構。

2. 對字串鍵的字典進行序列化

先從最常見的情況開始 — 鍵和值都是字串的字典。


// 範例字典:書名與其作者
var books = new Dictionary<string, string>
{
    ["馬斯特與瑪格麗塔"] = "米哈伊爾·布爾加科夫",
    ["哈利·波特"] = "喬安·羅琳",
    ["蒼蠅之王"] = "威廉·戈爾丁"
};

// 序列化為 JSON 字串
string json = JsonSerializer.Serialize(books, new JsonSerializerOptions { WriteIndented = true });
Console.WriteLine("字典已序列化為 JSON:\n" + json);

// 儲存到檔案(同步)
File.WriteAllText("books.json", json);
Console.WriteLine("JSON 已寫入檔案 books.json。");

// 從檔案讀回(同步)
string jsonFromFile = File.ReadAllText("books.json");

// 反序列化回字典
var restoredBooks = JsonSerializer.Deserialize<Dictionary<string, string>>(jsonFromFile);
Console.WriteLine("反序列化結果:");
foreach (var pair in restoredBooks)
    Console.WriteLine($"{pair.Key} -> {pair.Value}");

books.json 裡會是什麼?

{
  "馬斯特與瑪格麗塔": "米哈伊爾·布爾加科夫",
  "哈利·波特": "喬安·羅琳",
  "戰爭與和平": "威廉·戈爾丁"
}

這是怎麼運作的?

序列化器會把 Dictionary<string, string> 變成一個 JSON 物件,每個鍵成為屬性名,對應的值成為屬性值。當鍵是字串且唯一時,這種方式非常方便。

3. 鍵不是字串的字典

只要鍵是字串就很簡單。那如果鍵是數字怎麼辦?


var bookIds = new Dictionary<int, string>
{
    [1001] = "馬斯特與瑪格麗塔",
    [1002] = "哈利·波特",
    [1003] = "蒼蠅之王"
};

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

結果:

{
  "1001": "馬斯特與瑪格麗塔",
  "1002": "哈利·波特",
  "1003": "蒼蠅之王"
}

發生了什麼?

  • C# 把數字鍵轉成字串,因為 JSON 的屬性名只能是字串。
  • 在反序列化回 Dictionary<int, string> 時,序列化器會嘗試把字串再轉回數字。

反序列化範例:


var restoredBookIds = JsonSerializer.Deserialize<Dictionary<int, string>>(jsonIntKeys);
// 一切正常!鍵又變回數字了。
那如果鍵是複雜類型、例如物件呢?

var dict = new Dictionary<Author, string>
{
    [new Author { Name = "戈爾丁", BirthYear = 1911 }] = "蒼蠅之王"
};

試圖序列化這種字典會拋出一個 例外

System.NotSupportedException: Serialization and deserialization of 'Dictionary<Author, string>' instances are not supported.

為什麼會這樣?

JSON 物件的屬性名只能是字串。所以字典在序列化時,鍵必須是能夠被唯一且明確地轉成字串的簡單類型(通常是字串或數字)。複雜物件不能直接當作鍵來用標準方式序列化為 JSON。

4. 值是複雜物件的字典

鍵的問題說完了 — 接下來看看值是複雜物件(例如 BookAuthor)時會怎樣。

範例


public class Author
{
    public string Name { get; set; }
    public int BirthYear { get; set; }
}

var authorDirectory = new Dictionary<string, Author>
{
    ["bulgakov"] = new Author { Name = "米哈伊爾·布爾加科夫", BirthYear = 1891 },
    ["golding"] = new Author { Name = "威廉·戈爾丁", BirthYear = 1911 }
};

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

結果:

{
  "bulgakov": {
    "Name": "米哈伊爾·布爾加科夫",
    "BirthYear": 1891
  },
  "golding": {
    "Name": "威廉·戈爾丁",
    "BirthYear": 1911
  }
}
  • 所有嵌套的物件會按照物件結構被序列化。
  • 反序列化回 Dictionary<string, Author> 也會正常工作。

5. 作為其他物件一部分的字典

字典常常作為更複雜物件的欄位存在。比如某圖書館有一本目錄,鍵是類型名稱,值是該類型的書單。


public class Book
{
    public string Title { get; set; }
    public string Author { get; set; }
}

public class Library
{
    public Dictionary<string, List<Book>> CatalogByGenre { get; set; }
}

var library = new Library
{
    CatalogByGenre = new Dictionary<string, List<Book>>
    {
        ["科幻"] = new List<Book>
        {
            new Book { Title = "索拉里斯", Author = "斯坦尼斯瓦夫·萊姆" }
        },
        ["經典"] = new List<Book>
        {
            new Book { Title = "蒼蠅之王", Author = "威廉·戈爾丁" },
            new Book { Title = "可見的黑暗", Author = "威廉·戈爾丁" }
        }
    }
};

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

輸出 JSON 的片段:

{
  "CatalogByGenre": {
    "科幻": [
      {
        "Title": "索拉里斯",
        "Author": "斯坦尼斯瓦夫·萊姆"
      }
    ],
    "經典": [
      {
        "Title": "蒼蠅之王",
        "Author": "威廉·戈爾丁"
      },
      {
        "Title": "可見的黑暗",
        "Author": "威廉·戈爾丁"
      }
    ]
  }
}

一切都正常 — 嵌套的字典和集合會被遞迴地序列化與反序列化。

6. 字典序列化的特點與坑

重複的鍵

字典裡鍵是唯一的。但如果你手動提供一段含有重複鍵的 JSON:

{
  "foo": "first",
  "foo": "second"
}

結果:最後一個值("second")會覆蓋前面的,不會報錯。這是大多數 JSON 解析器的行為。

元素順序

字典是無序集合。序列化出來的鍵在 JSON 裡的順序可能和原始不同。如果順序很重要 — 用鍵值對列表(List<KeyValuePair<string, T>>),但一般字典不該依賴順序。

JSON 與嵌套字典

嵌套層級沒有硬性限制,但每個層級都必須滿足 JSON 的規則(鍵為字串,值為合法的 JSON 物件或陣列)。

使用 JsonSerializerOptions

有時你希望屬性名不是 PascalCase,而是 camelCase。這在與前端(JavaScript)整合時很常見,因為前端通常習慣用 camelCase。


var options = new JsonSerializerOptions
{
    PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
    WriteIndented = true
};

string camelJson = JsonSerializer.Serialize(authorDirectory, options);

注意:對字典來說,這個設定只會影響被序列化的嵌套物件(它們的屬性),而不會改變字典的鍵。字典的鍵總是以你在 C# 裡指定的字串來序列化。

7. 關於複雜與非字串鍵的問題

對於字串鍵和數值鍵(例如 int, long, Guid)的字典,通常「開箱即用」就能工作。但如果你嘗試用自定義類或結構做鍵 — 會得到 NotSupportedException

對這種情況有幾種常見的解法:

  • 改用別的儲存格式,例如把字典序列化為一個物件陣列,每個物件有 "Key""Value" 欄位。
  • 寫一個自訂的轉換器(JsonConverter),把複雜鍵轉成字串並在反序列化時還原。
  • 如果結構實在太複雜,考慮重新設計,不要用複雜物件當字典鍵。

把字典序列化為鍵值對列表的替代範例


public class AuthorInfo
{
    public Author Author { get; set; }
    public string Book { get; set; }
}
// 用法:替代 Dictionary<Author, string>
var list = new List<AuthorInfo>
{
    new AuthorInfo { Author = new Author { Name = "威廉·戈爾丁", BirthYear = 1911 }, Book = "蒼蠅之王" }
};
// 這樣的列表可以正常序列化

比較:字典 vs 鍵值對列表(序列化時)

集合類型 JSON 結構 何時使用
Dictionary<string, Book>
{ "key1": {...}, "key2": {...} }
鍵是簡單字串,需要快速查找並保證唯一性
List<KeyValuePair<string, Book>>
[{"Key": "key1", "Value": {...}}, ... ]
鍵是複雜類型、需要保留順序、允許重複鍵

8. 面試常見問題

1. 能否序列化 Dictionary<DateTime, string>
可以,但鍵會被轉成字串形式(通常是 ISO 格式類似 "yyyy-MM-ddTHH:mm:ss")。反序列化時可能會遇到地區設定或日期格式的問題。

2. 如果序列化 Dictionary<int, string> 會怎樣?
鍵會被序列化成字串,即使原始字典裡是數字。反序列化一般能正常把它們還原成數字鍵。

3. 為什麼不能把物件當字典鍵來序列化?
因為只有字串才能當 JSON 物件的屬性名,物件不能。

4. 想要序列化擁有複雜鍵的字典該怎麼做?
最好重新考慮資料結構,或把字典序列化為「鍵-值」列表,讓鍵本身作為完整物件被序列化,而不是作為屬性名。

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