1. 介绍
我们已经习惯在程序里创建对象。记得吗,我们很快就要更深入地学习类和对象,但现在我们已经能理解,变量、列表、甚至简单的字符串不是随便的东西,它们在程序里是某种“实体”。比如我们可以创建一个变量 int age = 30; 或者 string name = "瓦西娅";. 但是,如果我们需要把一个完整的用户信息保存到文件,用户有名字、年龄、地址、喜欢的书单等等,该怎么办?
想象一下:你在写游戏。你有一个 Player 对象,里面有很多属性:health、level、inventory(物品列表)、地图坐标等等。玩家玩着玩着,升级、捡到神器。然后他决定退出游戏。会发生什么?保存在内存里的所有数据都会消失!太伤心了!为了不让这事发生,我们需要把 Player 对象的状态保存到文件,当玩家回来时再把它加载回来。
这时候就轮到序列化出场了。它就是在内存里的“活”对象和磁盘上“死但持久”的数据之间搭桥的东西。
2. 序列化过程(从对象到文件)
我们来拆解一下把对象变成可以写入文件的字节流的“魔法”过程。
假设我们有这样一个类 Book(书籍):
// 这是我们创建 Book 对象的“蓝图”或“模板”
public class Book
{
// 书的属性
public string Title { get; set; } // 书名
public string Author { get; set; } // 作者
public int Year { get; set; } // 出版年份
// 构造函数 - 用来创建新的 Book 对象
public Book(string title, string author, int year)
{
Title = title;
Author = author;
Year = year;
}
// 用于方便输出书信息的方法(对序列化不是必须,但很有用)
public void DisplayInfo()
{
Console.WriteLine($"名称: {Title}, 作者: {Author}, 年份: {Year}");
}
}
步骤 1:创建要序列化的对象。
首先,当然要有一个我们想保存的对象。例如,我们创建了一个 Book 的实例:
Book myFavoriteBook = new Book("银河系漫游指南", "道格拉斯·亚当斯", 1979);
这个对象 myFavoriteBook 目前存在于内存中。
步骤 2:选择工具(序列化器)。
我们不能直接把对象“拷贝”到磁盘上。计算机不能直接在文件里理解对象——它需要字节。需要一个专门的工具——序列化器。它的任务是把我们的对象分解成组成部分(它的属性: Title, Author, Year)并把这些部分转成字节序列或文本(比如 JSON 或 XML)。
今天我们不深入具体实现——只把它当成一个“转换盒子”来理解。
步骤 3:把对象转换成数据流。
序列化器拿到我们的对象 myFavoriteBook,查看它的属性(Title, Author, Year)并把每个属性转换成可以写入的格式。所有这些字节(或文本字符)被拼成一条连续的数据流——一条“信息带”。
步骤 4:把数据流写入文件。
现在有了这条“信息带”,我们使用熟悉的 FileStream,如果是文本格式(比如 JSON)可能还会用 StreamWriter,或者直接用 FileStream(用于纯二进制数据),把这条数据写到磁盘上。
3. 反序列化过程(从文件到对象)
步骤 1:从文件读取数据流。
我们再次使用 FileStream,必要时用 StreamReader(如果是文本格式),来读取文件内容。数据以字节流或文本“带”的形式返回。
步骤 2:选择工具(反序列化器)。
需要一个相反的工具——反序列化器。它需要知道如何解释收到的字节/文本并正确重建对象结构。很重要的一点:通常反序列化要用和序列化相同类型的序列化器(通常也是同一个库),否则你的“组合说明”可能无法被正确理解。
步骤 3:把数据流转换回对象。
反序列化器读取数据,知道哪里是 Title,哪里是 Author,哪里是 Year,并据此创建一个新的 Book 对象,填充它的属性。
步骤 4:得到可用的对象。
好了!我们又有了一个完整的 Book 对象在内存里,可以继续使用。
示例:手动保存我们的“超棒书”(为了理解概念)
暂且不使用专门的库:用最简单的“手动”序列化和反序列化,借助 StreamWriter 和 StreamReader。这能帮助理解基本原理。
我们的对象 Book:
public class Book
{
public string Title { get; set; }
public string Author { get; set; }
public int Year { get; set; }
public Book(string title, string author, int year)
{
Title = title;
Author = author;
Year = year;
}
public void DisplayInfo()
{
Console.WriteLine($"名称: \"{Title}\", 作者: {Author}, 年份: {Year}");
}
}
手动序列化:方法 SaveBookToTextFile
创建一个方法,把书的属性逐行保存到文本文件。
void SaveBookToTextFile(Book book, string filePath)
{
using var writer = new StreamWriter(filePath);
writer.WriteLine(book.Title);
writer.WriteLine(book.Author);
writer.WriteLine(book.Year);
}
发生了什么?我们打开了 StreamWriter,按顺序写入 Title, Author, Year —— 这就是最简单的序列化方案。
如果运行代码,文件内容会像这样:
银河系漫游指南
道格拉斯·亚当斯
1979
手动反序列化:方法 LoadBookFromTextFile
写一个方法,读取数据并组装成新的 Book 对象。
Book LoadBookFromTextFile(string filePath)
{
using var reader = new StreamReader(filePath);
string title = reader.ReadLine();
string author = reader.ReadLine();
int year = int.Parse(reader.ReadLine());
return new Book(title, author, year);
}
并在 Main 中使用这些方法:
// 创建对象
var myBook = new Book("银河系漫游指南", "道格拉斯·亚当斯", 1979);
string filePath = "my_favorite_book.txt";
// 把它保存到文件
SaveBookToTextFile(myBook, filePath);
// 从文件读取
Book loadedBook = LoadBookFromTextFile(filePath);
在 LoadBookFromTextFile 中发生了什么?我们打开 StreamReader 并按相同顺序读取行:先是标题,再是作者,最后把年份通过 int.Parse 转成整数。然后我们用这些值创建一个新的 Book 实例。
在实际代码中,应该加上检查(比如 File.Exists)和用 try-catch 做错误处理,但这里我们专注于核心思路。
为什么“手动”序列化不太好(以及为什么需要库)?
- 手动代码多。 每个属性都要显式写入和读取。如果对象和字段很多,代码会膨胀。
- 对修改很脆弱。 增加了新属性 Pages(int)?你得同时改写入和读取,还要严格保证顺序。
- 复杂结构难处理。 嵌套集合和对象(比如 List<Chapter>)会把代码搞成“意大利面”式的混乱。
- 格式与效率。 文本格式简单但不总是紧凑或安全;处理二进制时需要手动管理字节、用 BinaryWriter/BinaryReader 等。
- 没有元数据。 文件本身不知道第一行是 Title、第三行是 Year。专用的序列化器可以保存元数据,对模型变化更有韧性。
这就是为什么在真实项目里我们会用现成的序列化库,它们能自动解析/组装对象,支持 JSON、XML 和二进制格式,并能更稳健地应对模型变化。下节课我们会继续讲这个内容!
实际应用:序列化的用途
- 保存和加载数据。 设置、游戏进度、配置。
- 通过网络传输数据。 服务之间交换复杂对象(经常用 JSON)。
- 缓存。 快速重用之前获取的数据。
- 记录复杂对象的日志。 方便调试和审计。
- 深拷贝对象。 用序列化+反序列化作为克隆对象图的一种方式。
序列化是现代软件的基石之一:从保存游戏进度到和 web 服务打交道,它无处不在!
GO TO FULL VERSION