.NET 僅提供 MemberwiseClone 淺複製,深複製需自行實作 ICloneable,但手寫 Clone 方法在物件圖複雜、屬性眾多、存在循環引用時極其繁瑣。本文提出反射通用基類 BaseCopyObject,一次解決:
DataTable) 可擴充點處理Type 與公用建構子,透過 Activator.CreateInstance 建立新實例 (子類別可覆寫 GetConstructParams 提供建構參數)。BindingFlags.Instance | Public | NonPublic 列舉所有實例欄位。GetValue/SetValue。CopyObject,傳入 new object[] { this, newObj } 供循環引用判斷。CopyDefineElement,預設回傳 false 表示無法處理,子類別可覆寫實作自訂複製邏輯 (如 DataTable.Copy())。objs[0] (原始根物件),則直接指向 objs[1] (新根物件),打破循環。public virtual object CopyObject(params object[] objs)
{
Type t = this.GetType();
ConstructorInfo[] ctors = t.GetConstructors();
object newObj = (ctors.Length > 0) ? Activator.CreateInstance(t, GetConstructParams()) : null;
foreach (FieldInfo fi in t.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
if (t.GetEvent(fi.Name) != null) continue; // 略過事件
object val = fi.GetValue(this);
if (val != null)
{
Type ft = fi.FieldType;
if (ft != typeof(string) && !ft.IsValueType)
{
if (IsBaseCopyObjectType(ft))
{
if (!CopyDefineElement(ft, ref val))
{
val = ((BaseCopyObject)val).CopyObject(this, newObj);
}
}
else
{
CopyDefineElement(ft, ref val); // 子類別處理 DataTable 等
}
}
fi.SetValue(newObj, val);
}
}
return newObj;
}
[TestMethod]
public void TestClone()
{
var a = new ITEmployeeObj();
var b = new EmployeeObj { EmployeeName = "Tom", ParentObj = a };
a.Employee = b;
a.DealEvent += Handler; // 事件欄位
var clone = (ITEmployeeObj)a.CopyObject();
Assert.AreNotSame(a, clone);
Assert.AreNotSame(a.Employee, clone.Employee);
Assert.AreEqual(a.Employee.EmployeeName, clone.Employee.EmployeeName);
Assert.AreSame(clone, clone.Employee.ParentObj); // 循環引用正確指向新根
}
GetConstructParams():提供建構子參數,預設 null。CopyDefineElement(Type, ref object):處理非 BaseCopyObject 參考型別,回傳 true 表示已處理。IsBaseCopyObjectType:遞迴檢查型別繼承鏈是否含 BaseCopyObject。
此方案將深複製從「每類手寫」轉為「繼承基類 + 少量覆寫」,大幅降低維護成本,適用於 WinForm 撤銷/重做、物件快照、DTO 複製等高頻場景。