C# 參考型別預設為淺拷貝(複製參考),值型別為深拷貝(複製值)。理解兩者差異並掌握正確深拷貝技術,是避免共享狀態 Bug 的關鍵。
| 特性 | 淺拷貝 | 深拷貝 |
|---|---|---|
| 定義 | 複製物件參考,指向同一記憶體 | 遞迴建立全新物件圖,無共享參考 |
| 值型別 | 複製值 (隱含深拷貝) | 複製值 |
| 參考型別 | 複製參考 | 建立新實例並複製所有欄位 |
| Array.Clone() | ✅ 淺拷貝 | ❌ |
| List 构造函數 | ✅ 淺拷貝 | ❌ |
using System;
public class Person : ICloneable
{
public int Id { get; set; }
public int Age { get; set; }
public Address Address { get; set; } // 參考型別欄位
public object Clone()
{
// 手動深拷貝:值型別直接賦值,參考型別遞迴 Clone
return new Person
{
Id = this.Id,
Age = this.Age,
Address = this.Address?.Clone() as Address // 假設 Address 也實作 ICloneable
};
}
public void Show() => Console.WriteLine($"Id: {Id}, Age: {Age}, Addr: {Address?.City}");
}
public class Address : ICloneable
{
public string City { get; set; }
public string Street { get; set; }
public object Clone() => new Address { City = this.City, Street = this.Street };
}
public static void Demo()
{
var original = new Person
{
Id = 1,
Age = 30,
Address = new Address { City = "Taipei", Street = "Renai Rd" }
};
// 1. 淺拷貝:Array.Clone()
Person[] shallowArray = (Person[])new Person[] { original }.Clone();
// 2. 深拷貝:手動 Clone
Person[] deepArray = new Person[] { (Person)original.Clone() };
Console.WriteLine("=== 修改前 ===");
original.Show(); // Id: 1, Age: 30, Addr: Taipei
shallowArray[0].Show(); // Id: 1, Age: 30, Addr: Taipei
deepArray[0].Show(); // Id: 1, Age: 30, Addr: Taipei
// 修改原物件的參考型別欄位
original.Address.City = "Kaohsiung";
Console.WriteLine("\n=== 修改原物件 Address.City 後 ===");
original.Show(); // Id: 1, Age: 30, Addr: Kaohsiung ← 變了
shallowArray[0].Show(); // Id: 1, Age: 30, Addr: Kaohsiung ← 同步變了!(淺拷貝)
deepArray[0].Show(); // Id: 1, Age: 30, Addr: Taipei ← 未變 (深拷貝)
}
| 方案 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|
| 手動 Clone | 效能最好、完全可控、無反射 | 樣板代碼多、維護成本高 | 核心領域模型、高頻呼叫 |
| BinaryFormatter 序列化 | 一行代碼、支援任意物件圖 | 已過時/不安全、效能差、需 [Serializable] | ❌ 棄用 |
| JSON 序列化 (System.Text.Json) | 安全、跨平台、可控制選項 | 需無參建構函式、循環參考需處理、私有欄位需配置 | ✅ 推薦通用方案 |
| MessagePack / Protobuf | 二進制、高效能、嚴格合約 | 需定義 Schema、額外依賴 | 高效能、跨語言 |
| AutoMapper / Mapster | 慣例配置、投影查詢優化 | 映射邏輯隱式、效能開銷 | DTO 轉換、領域模型映射 |
| Record / with 運算式 (C# 9+) | 語言級支援、不可變、結構化 | 僅適用 record 型別、淺拷貝 (with 只複製參考) | 不可變資料模型 |
using System.Text.Json;
public static class DeepCloneExtensions
{
private static readonly JsonSerializerOptions _options = new()
{
// 處理循環參考
ReferenceHandler = ReferenceHandler.Preserve,
// 包含私有欄位 (需 .NET 7+)
IncludeFields = true,
// 忽略 null 可選
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
public static T DeepClone(this T source)
{
if (source == null) return default;
string json = JsonSerializer.Serialize(source, _options);
return JsonSerializer.Deserialize(json, _options);
}
}
// 使用
var deepCopy = original.DeepClone();
// Record 預設不可變,with 建立新實例並修改指定屬性
public record PersonRecord(int Id, int Age, AddressRecord Address);
public record AddressRecord(string City, string Street);
var p1 = new PersonRecord(1, 30, new AddressRecord("Taipei", "Renai Rd"));
// 淺拷貝並修改 Age
var p2 = p1 with { Age = 31 };
// p2.Address 仍指向同一 AddressRecord 實例 (淺拷貝)
// 深拷貝需手動
var p3 = p1 with { Age = 31, Address = p1.Address with { City = "Kaohsiung" } };
| 方法 | Mean (ns) | Allocated (B) |
|------------------------|-----------|---------------|
| Manual Clone | 45 | 0 |
| JSON (System.Text.Json)| 1,200 | 1,200 |
| BinaryFormatter (Obsolete)| 2,500 | 2,800 |
| AutoMapper | 3,800 | 3,500 |
Clone() 或使用 record + with 組合System.Text.Json 序列化深拷貝list.Select(x => x.DeepClone()).ToList()BinaryFormatter、MemberwiseClone (protected、淺拷貝)、Object.MemberwiseClonerecord、readonly、init 讓拷貝需求自然減少淺拷貝複製參考,深拷貝複製物件圖。現代 C# 推薦:領域模型用 record + 手動 with 組合;通用場景用 System.Text.Json 序列化;高效能熱路徑手寫 Clone。理解記憶體語義,選對工具,才能寫出正確且高效的代碼。