物件拷貝是 C# 開發常見需求,但「賦值 (=)」、「淺拷貝」、「深拷貝」三者行為差異巨大,搞混會導致資料共享異常。本文系統整理四種主流深拷貝實作模式,並釐清常見誤區。
| 方式 | 值型別欄位 | 參考型別欄位 | 適用場景 |
|---|---|---|---|
| 賦值 (=) | 複製值 | 複製**參考** (雙方指向同一物件) | 刻意共享參考 |
| 淺拷貝 | 逐位複製 | 複製**參考** (雙方指向同一子物件) | 簡單 DTO、不可變子物件 |
| 深拷貝 | 逐位複製 | **遞迴建立新子物件** (完全獨立) | 複雜物件圖、需隔離修改 |
特例:string 雖為參考型別,但不可變,淺拷貝行為等同深拷貝。
public class Person
{
public int Age { get; set; }
public string Address { get; set; } // string 不可變,行同深拷貝
public Name Name { get; set; } // 參考型別,淺拷貝共享此實例
public object ShallowClone()
{
return this.MemberwiseClone(); // protected 方法,需公開包裝
}
}
public class Name
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
優點:效能極高 (CLR 內建)、程式碼極少。缺點:參考型別欄位共享,修改子物件會互相影響。
public class Person : ICloneable
{
public int Age { get; set; }
public string Address { get; set; }
public Name Name { get; set; }
public object Clone()
{
var clone = new Person
{
Age = this.Age,
Address = this.Address, // string 不可變可直接指派
Name = new Name // 手動建立新子物件
{
FirstName = this.Name.FirstName,
LastName = this.Name.LastName
}
};
return clone;
}
}
優點:完全可控、無反射/序列化開銷、效能最好。缺點:維護成本高,欄位增減需同步修改 Clone 方法,巢狀層級深時程式碼膨脹。
[Serializable]
public class Person : ICloneable
{
public int Age { get; set; }
public string Address { get; set; }
public Name Name { get; set; }
public object Clone()
{
using (var ms = new MemoryStream())
{
var bf = new BinaryFormatter();
bf.Serialize(ms, this);
ms.Position = 0;
return bf.Deserialize(ms);
}
}
}
[Serializable]
public class Name { ... }
優點:一次實作支援任意複雜物件圖、自動處理循環參考。缺點:
[Serializable],所有成員型別皆需可序列化。BinaryFormatter **已被標記為不安全/過時** (.NET 5+ 警告),建議改用 System.Text.Json、protobuf-net、MessagePack 等現代序列化器。
public T DeepClone(T source)
{
var json = System.Text.Json.JsonSerializer.Serialize(source);
return System.Text.Json.JsonSerializer.Deserialize(json);
}
public static class DeepCloner
{
public static T Clone(T source) where T : class, new()
{
if (source == null) return null;
var clone = new T();
CloneObject(source, clone);
return clone;
}
private static void CloneObject(object source, object target)
{
var type = source.GetType();
foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
if (!prop.CanRead || !prop.CanWrite) continue;
// 可加入 [Cloneable(CloneableState.Exclude)] 等屬性判斷
var value = prop.GetValue(source);
if (value == null) { prop.SetValue(target, null); continue; }
if (prop.PropertyType.IsValueType || prop.PropertyType == typeof(string))
{
prop.SetValue(target, value); // 值型別/string 直接指派
}
else if (typeof(IEnumerable).IsAssignableFrom(prop.PropertyType))
{
// 集合類型需遞迴複製元素 (略)
}
else
{
// 遞迴複製參考型別
var clonedValue = CloneObjectRecursive(value);
prop.SetValue(target, clonedValue);
}
}
}
// 遞迴邏輯略...
}
優點:無需修改原始類別、可透過 Attribute 控制拷貝行為 (排除、深/淺)。缺點:反射效能開銷大、需處理循環參考、泛型集合複製複雜。
| 方法 | 相對耗時 | 記憶體配置 | 適用性 |
|---|---|---|---|
| MemberwiseClone | 1x (基準) | 極低 | 淺拷貝 |
| 手動 ICloneable | 1.5x ~ 3x | 低 | 深拷貝、熱路徑 |
| JSON 序列化 | 50x ~ 200x | 高 | 深拷貝、非熱路徑、跨進程 |
| BinaryFormatter | 100x ~ 500x | 高 | **不建議新專案使用** |
| 反射通用 | 20x ~ 100x | 中高 | 深拷貝、動態型別、Plugin 架構 |
ICloneable 或建構函式複製 new Person(original)。System.Text.Json 序列化深拷貝,簡潔安全。BinaryFormatter、ICloneable 介面本身 (文檔建議勿實作,改用具體方法如 DeepClone())。