CodeGym /課程 /C# SELF /類別結構變動時的相容性

類別結構變動時的相容性

C# SELF
等級 45 , 課堂 4
開放

1. 介紹

想像一下:你寫了一個類別 Person,把物件序列化到檔案,幾個月後你決定要加個地址欄位或改變某些屬性的型別。看起來是很平常的事 —— 但當你試圖讀(反序列化)以前存的舊格式資料時,可能會有驚喜:有東西讀不出來、丟出例外、某些值變空或甚至不正確。

這種行為就是典型的向下相容性破壞。在真實開發中這事比學生忘記分號還常發生(也就是非常常)。

用範例來說明問題

看我們的教學小專案。假設目前我們有這樣一個類別:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
}

把這個類別的實例序列化成 JSON:

Person p = new Person { Name = "Alice", Age = 35 };
string json = JsonSerializer.Serialize(p);
File.WriteAllText("person.json", json);

檔案會得到:

{"Name":"Alice","Age":35}

現在過了一週,我們想讓 App 更時髦,新增一個地址欄位:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; } // 新增欄位
}

然後嘗試載入舊檔案:

string json = File.ReadAllText("person.json");
Person p = JsonSerializer.Deserialize<Person>(json);

會發生什麼?我們的物件裡不會有 Address:Address 會是 null。沒有例外,一切看起來還能運作... 但只要你開始改型別、刪欄位或做更「有趣」的改動,就可能出問題!

2. 常見的變更類型 — 以及它們的影響

類別結構的變更會以不同方式影響序列化。下面拆幾個常見情境來討論。

新增屬性

這是最不危險的一種情況。舊資料(沒有這些欄位)通常可以被正常反序列化:新屬性會等於預設值(參考型別為 null0int 等等)。

注意: 如果你的新屬性是 non-nullable 且沒有合理的預設值,可能會出問題(特別是遇到 C# 11+ 的 required 屬性時)。

刪除屬性

如果你刪掉了屬性,但序列化資料裡還有該欄位——序列化器通常會忽略這些「多餘」的欄位,載入仍會成功。

但這也取決於你用的序列化庫。例如,JsonSerializerNewtonsoft.Json 都相當寬容:它們不會丟例外;不過某些老舊或自訂的序列化器可能行為不同。

重命名屬性

這時就有趣了。如果你把 FirstName 改成 Name,序列化器無法把舊資料的欄位對應到新物件。換句話說,那個欄位會是空(null/0),舊檔案裡的值被忽略。

變更屬性型別

比方說原本你有 public int Age,後來改成 public string Age(也許有人會寫 "長生不老" —— 任何事都有可能)。嘗試把舊資料反序列化會依序列化器與它的型別強型設定,結果可能是發生錯誤(例如 "Cannot convert number to string")或屬性拿到預設值。

改變繼承或結構層級(繼承、嵌套)

如果你調整了基底類別、把屬性移到別處或把一個類包成另一個類包裝——舊的序列化資料可能會完全不相容。尤其是針對 XML 與複雜物件繼承結構時,問題更容易出現。

3. 相容性問題

如何發現相容性問題?

相容性問題常常不會立刻或明顯地爆出:你的應用可能開始「怪怪的」,部分資料消失,或 log 裡出現不太友善的例外。通常問題會在以下情形浮現:

  • 使用者在新版本程式中載入舊檔案。
  • 伺服器從「舊版本」的客戶端收到 JSON/XML
  • 你對外部 API 的介面被不預期地更新了。

症狀很多樣:從反序列化錯誤到某些欄位「莫名其妙」是空的。

序列化器對相容性的影響

不同序列化器行為不一。對 JSON 結構變動最「寬容」的通常是像標準的 System.Text.JsonNewtonsoft.Json。它們通常會略過檔案中不認識的屬性,而且不會把未知的物件欄位序列化回去。

XML 上就稍微嚴格一點:如果根元素或結構改變,可能會出現錯誤。

而在二進位格式裡,如果順序或型別改了,甚至可能直接丟例外!

4. 怎麼降低風險?方法與實務

下面幾個做法可以把問題風險降到最低(有時能完全避免)。

使用類別與資料的版本號

在可序列化物件或檔案中加入一個 Version 欄位。這能讓你知道檔案是用哪個結構版本建立,載入時就能依版本做不同處理(例如跑資料升級)。

public class PersonV2
{
    public int Version { get; set; } = 2;
    public string Name { get; set; }
    public int Age { get; set; }
    public string Address { get; set; }
}

對序列化使用名稱映射屬性

對 JSON 或 XML 可以明確指定序列化時屬性的名稱。重命名屬性時保留舊名稱可以避免中斷:

public class Person
{
    [JsonPropertyName("FirstName")] // 適用於 System.Text.Json
    [JsonProperty("FirstName")]     // 適用於 Newtonsoft.Json
    public string Name { get; set; }
    public int Age { get; set; }
}

使用 nullable 型別與預設值

如果你新增的欄位在舊資料中可能不存在——把它設成 nullable 或指定合理預設值,這樣反序列化才不會出問題:

public class Person
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string? Address { get; set; } = "Unknown";
}

處理「未知欄位」事件

Newtonsoft.Json 裡可以訂閱未知欄位的處理函式,比方說記 log 或做其他自訂處理,避免默默地丟失資訊。

var settings = new JsonSerializerSettings
{
    MissingMemberHandling = MissingMemberHandling.Error
};
try
{
    var person = JsonConvert.DeserializeObject<Person>(json, settings);
}
catch (JsonSerializationException ex)
{
    Console.WriteLine("無法反序列化: " + ex.Message);
}

資料遷移

若改動很大,最好設計遷移步驟:先把資料載入「舊」結構,再轉換成新結構:

// 假設 PersonV1 類沒有 Address
public class PersonV1 { public string Name; public int Age; }

// 新的有 Address
public class PersonV2 { public string Name; public int Age; public string Address; }

// 遷移:
string oldJson = File.ReadAllText("person.json");
PersonV1 oldPerson = JsonSerializer.Deserialize<PersonV1>(oldJson);

PersonV2 migrated = new PersonV2
{
    Name = oldPerson.Name,
    Age = oldPerson.Age,
    Address = "Unknown"
};

5. 複雜狀況與意外錯誤

欄位不變性與 required 屬性

從 C# 11 開始有了 required 屬性。如果欄位被標為 required,反序列化在資料沒有該欄位時可能會丟出錯誤:

public class Person
{
    public string Name { get; set; }
    [JsonPropertyName("Age")]
    public required int Age { get; set; }
    public string Address { get; set; }
}

如果舊資料裡沒有 Age 欄位——會拋出結構不符合的例外。

型別變更:int string

// 原本是:
public class Record { public int Count; }
// 現在變成:
public class Record { public string Count; }

如果資料裡是 "Count":42,反序列化到 string 可能會成功(視情況有智慧型轉換),但反過來就可能丟例外。

移除基底類別

如果序列化的物件有繼承關係,但你改了繼承結構——舊檔案的反序列化可能會失敗,有時是「靜默」出問題、有時會明確丟例外。

6. 常見錯誤

錯誤 №1:不假思索地變動現有屬性。
重命名或改變屬性型別卻沒考慮現有的序列化資料,會導致反序列化時資訊遺失。

錯誤 №2:忘記為新欄位使用 nullable。
新屬性應該要麼是 nullable,要麼有合理的預設值。

錯誤 №3:不測試向下相容性。
改動類別後一定要測試:舊檔案/資料能否正確載入。

錯誤 №4:混用不同套件的屬性標註。
不要在同一個屬性上同時使用 JsonPropertyNameJsonProperty

2
任務
C# SELF, 等級 45, 課堂 4
上鎖
處理反序列化時缺少欄位的情況
處理反序列化時缺少欄位的情況
1
問卷/小測驗
序列化設定,等級 45,課堂 4
未開放
序列化設定
物件序列化設定
留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION