1. 介紹
我們已經習慣在程式裡建立物件。記得嗎,我們馬上就要更深入學習 class 和 object,但現在就能理解,變數、list、甚至簡單的字串不是隨便的東西,它們在程式裡是「實體」。例如,我們可以建立一個變數 int age = 30; 或 string name = "瓦西亞";。 但如果我們要把一個完整使用者的資訊儲存到檔案,這個使用者有名字、年齡、位址、最愛書籍的清單等等,該怎麼辦?
想像你在寫一個遊戲。你有一個 Player 物件,裡面很多屬性:health、level、inventory(道具清單)、在地圖上的座標等等。玩家玩、升等、撿到神器。然後他決定離開遊戲。會發生什麼?所有存在記憶體中的資料會消失!好可惜!為了避免這種情況,我們得把 Player 的狀態儲存到檔案,等玩家回來時再把它載回來。
這時候就輪到序列化上場。序列化就是在記憶體中的「活」物件和硬碟上「死但持久」的資料之間搭橋。
2. 序列化過程(從物件到檔案)
我們來拆解這個把物件變成可寫入檔案的位元組序列的「魔術」。
假設有這樣一個 class 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:把物件轉成資料流。
序列化器拿到 our myFavoriteBook,檢視它的屬性(Title, Author, Year)並把每個屬性轉成可寫入的格式。所有這些位元組(或文字)會被串成單一的資料流 —— 一條長長的資訊「帶」。
步驟 4:把資料流寫入檔案。
當我們有了這條「位元組帶」,我們會用熟悉的 FileStream,如果是文字格式(像 JSON)則可能搭配 StreamWriter,或者純二進位資料就用 FileStream 直接寫入。這樣資料就保存在磁碟上了。
3. 反序列化過程(從檔案到物件)
步驟 1:從檔案讀取資料流。
我們再次使用 FileStream,如果需要讀文字則用 StreamReader,把檔案內容讀出。資料會以位元組或文字的「帶」回來。
步驟 2:選擇工具(反序列化器)。
需要一個反向工具——deserializer。它要知道如何解讀拿到的位元組/文字並正確重建物件結構。非常重要的是:反序列化通常要用與序列化時相同的 serializer/同一個函式庫,否則你的「組裝說明」可能會被誤解。
步驟 3:把資料流轉回物件。
deserializer 會讀資料,找出哪裡是 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 等工具。
- 缺乏metadata。我們的檔案不會「知道」第一行是 Title、第三行是 Year。專業的序列化器可以儲存 metadata,對模型變更更有韌性。
所以在真實專案中通常會用現成的 序列化函式庫,它們能自動拆解/重建物件、支援 JSON、XML、二進位格式,並且對模型變更更穩健。下一堂課會談到這些內容!
實務應用:為什麼需要序列化?
- 儲存與載入資料。設定、遊戲存檔、組態檔。
- 透過網路傳輸資料。不同服務之間交換複雜物件(通常用 JSON)。
- 快取。快速重用先前取得的資料。
- 紀錄複雜物件。方便除錯與稽核。
- 深拷貝物件。序列化 + 反序列化可以用來克隆物件圖。
序列化是現代軟體的基石之一:從遊戲存檔到 web services,它無處不在!
GO TO FULL VERSION