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 对象的属性名只能是字符串。因此在序列化时,字典的键必须是能够唯一映射为字符串的简单类型(通常是 string 或 number)。复杂对象不能用作键(至少用默认的序列化方式不能)。
4. Словарь с вложенными объектами в качестве значения
键已经讲清楚——现在看如果值是复杂对象(比如 Book 或 Author)会怎样。
Пример
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);
重要: 对字典来说,这个选项只影响嵌套对象的属性名(它们会被转成 camelCase),但不会影响字典的键。字典的键会按 C# 中指定的字符串原样序列化。
7. Проблемы со сложными и нестроковыми ключами
对于带字符串键和数值键(比如 int, long, Guid)的字典,序列化通常是“开箱即用”的。但如果你尝试用自定义类或结构体作为键——会遇到 NotSupportedException。
针对这种情况,有几个解决办法:
- 使用不同的存储格式,比如把字典序列化成一组包含 "Key" 和 "Value" 字段的对象数组。
- 写一个自定义转换器(JsonConverter),把复杂键转换成字符串并在反序列化时还原。
- 如果键的结构真的很复杂,可能需要重新考虑架构,避免用复杂对象作为字典键。
Пример обхода через сериализацию как списка пар
public class AuthorInfo
{
public Author Author { get; set; }
public string Book { get; set; }
}
// 用 List<AuthorInfo> 代替 Dictionary<Author, string>
var list = new List<AuthorInfo>
{
new AuthorInfo { Author = new Author { Name = "威廉·戈尔丁", BirthYear = 1911 }, Book = "蝇王" }
};
// 这样的列表可以直接序列化
Сравнение: словарь vs. список пар при сериализации
| Тип коллекции | JSON-структура | Когда использовать |
|---|---|---|
|
|
键是简单字符串,需要快速查找和唯一性 |
|
|
键是复杂类型、需要控制顺序、允许重复 |
8. Типовые вопросы на собеседованиях
1. Можно ли сериализовать Dictionary<DateTime, string>?
可以,但键会被转换为字符串表示(通常是 ISO 格式,类似 "yyyy-MM-ddTHH:mm:ss")。反序列化时可能会遇到地区设置和日期格式的问题。
2. Что произойдет, если сериализовать Dictionary<int, string>?
键会被序列化为字符串,即使原来是数字。反序列化回去通常没有问题。
3. Почему нельзя сериализовать словарь с объектами в качестве ключа?
因为只有字符串才能作为 JSON 对象的属性名,而对象不能。
4. А если хочется сериализовать словарь с комплексным ключом?
最好重构数据结构,或者把字典序列化为键值对列表,让键作为对象字段来序列化,而不是作为属性名。
GO TO FULL VERSION