最近踩到一個legacy code 在C#物件複製的陳年小雷,拆解炸彈的同時也寫筆記!
有時我們會在類別(class)中加入Object.MemberwiseClone方法來提供物件的複製(clone),舊程式使用新物件裡的屬性剛好都是用new關鍵字建立,大概像下面的方式使用屬性:
p2.IdInfo = new IdInfo(17);
很幸運一直沒發生參考問題,最近改用直接指派,類似下面的寫法:
p2.IdInfo.IdNumber = 17;
測試時大驚!原始物件p1的值竟然被覆蓋了,花了時間才發現自己對MemberwiseClone的定義不夠清楚。
測試步驟
1.實作Object.MemberwiseClone的淺層複製。
2.實驗原始物件值未被覆蓋(new 關鍵字)
3.實驗原始物件值被覆蓋(指派)
4.深層複製的寫法之一。
先在測試專案中新增測試用的類別(Class)並且新增淺層複製(Shallow Copy)的方法
public class Person
{
public IdInfo IdInfo;
public int Age { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public List<string> Phones { get; set; } = new List<string>();
public Person ShallowCopy()
{
return (Person)this.MemberwiseClone();
}
}
public class IdInfo
{
public int IdNumber;
public IdInfo(int IdNumber)
{
this.IdNumber = IdNumber;
}
}
測試Object.MemberwiseClone的淺層複製的效果
輸入以下測試程式碼
[TestMethod]
public void TestShallowCopy()
{
var person1 = new Person
{
Name = "長澤雅美",
Age = 30,
Address = "日本静岡縣磐田市",
Phones = new List<string> { "9", "1", "1" },
IdInfo = new IdInfo(1)
};
var person2 = person1.ShallowCopy() as Person;
Console.WriteLine($"person1 id={person1.IdInfo.IdNumber} Name={person1.Name} , Address={person1.Address} , Age={person1.Age} ,
phone={person1.Phones.Count}");
Console.WriteLine($"person2 id={person2.IdInfo.IdNumber} Name={person2.Name} , Address={person2.Address} , Age={person2.Age}, phone={person2.Phones.Count}");
}
測試結果:
淺層的物件複製就可以將每個屬性都clone過來了!
實驗原始物件值未被覆蓋(new 關鍵字)
輸入以下程式碼
[TestMethod]
public void TestShallowCopyReplace1()
{
var p1 = new Person
{
Name = "長澤雅美",
Age = 30,
Address = "日本静岡縣磐田市",
Phones = new List<string> { "9", "1", "1" },
IdInfo = new IdInfo(1)
};
var p2 = p1.ShallowCopy() as Person;
p2.Name = "史丹利";
p2.Age = 36;
p2.Address = "台灣台北市內湖區";
p2.Phones = new List<string>();
p2.IdInfo = new IdInfo(17);
Console.WriteLine($"person1 Name={p1.Name},Age={p1.Age},Address={p1.Address},phone={p1.Phones.Count},id={p1.IdInfo.IdNumber}");
Console.WriteLine($"person2 Name={p2.Name},Age={p2.Age},Address={p2.Address},phone={p2.Phones.Count},id={p2.IdInfo.IdNumber}");
}
測試結果:
New關鍵字!幸運沒事!雅美的電話和id並沒有被覆蓋!
實驗原始物件值被覆蓋(指派)
輸入以下程式碼,尤其是以下這兩行屬性值的修改
p2.Phones.Clear();
p2.IdInfo.IdNumber = 17;
p2.IdInfo.IdNumber = 17;
public void TestShallowCopyReplace2()
{
var p1 = new Person
{
Name = "長澤雅美",
Age = 30,
Address = "日本静岡縣磐田市",
Phones = new List<string> { "9", "1", "1" },
IdInfo = new IdInfo(1)
};
var p2 = p1.ShallowCopy() as Person;
p2.Name = "史丹利";
p2.Age = 36;
p2.Address = "台灣台北市內湖區";
p2.Phones.Clear();
p2.IdInfo.IdNumber = 17;
Console.WriteLine($"person1 Name={p1.Name},Age={p1.Age},Address={p1.Address},phone={p1.Phones.Count},id={p1.IdInfo.IdNumber}");
Console.WriteLine($"person2 Name={p2.Name},Age={p2.Age},Address={p2.Address},phone={p2.Phones.Count},id={p2.IdInfo.IdNumber}");
}
執行結果:
電話和ID兩個屬性果然被覆蓋了!
重新看一下MemberwiseClone的說明
If a field is a value type, a bit-by-bit copy of the field is performed. If a field is a reference type, the reference is copied but the referred object is not; therefore, the original object and its clone refer to the same object.
如果欄位是實值型別,則會複製出欄位的複本。 如果欄位是參考型別,將只會複製參考!
傷腦筋!幸好馬上搜尋到余小張大大的文章,馬上從淺複製升級到深複製!
深層複製的寫法之一
首先要修改一下原始的類別,新增Deep Copy方法
public class Person
{
public IdInfo IdInfo;
public int Age { get; set; }
public string Name { get; set; }
public string Address { get; set; }
public List<string> Phones { get; set; } = new List<string>();
public Person ShallowCopy()
{
return (Person)this.MemberwiseClone();
}
public Person DeepCopy()
{
Person other = (Person)this.MemberwiseClone();
other.IdInfo = new IdInfo(this.IdInfo.IdNumber);
other.Name = string.Copy(this.Name);
other.Address = string.Copy(this.Address);
other.Phones = new List<string>(this.Phones);
return other;
}
}
重新執行測試程式
[TestMethod]
public void TestDeepCopy()
{
var p1 = new Person
{
Name = "長澤雅美",
Age = 30,
Address = "日本静岡縣磐田市",
Phones = new List<string> { "9", "1", "1" },
IdInfo = new IdInfo(1)
};
var p2 = p1.DeepCopy() as Person;
p2.Name = "史丹利";
p2.Age = 36;
p2.Address = "台灣台北市內湖區";
p2.Phones.Clear();
p2.IdInfo.IdNumber = 17;
Console.WriteLine($"person1 Name={p1.Name},Age={p1.Age},Address={p1.Address},phone={p1.Phones.Count},id={p1.IdInfo.IdNumber}");
Console.WriteLine($"person2 Name={p2.Name},Age={p2.Age},Address={p2.Address},phone={p2.Phones.Count},id={p2.IdInfo.IdNumber}");
}
執行結果:
成功分開本來就是平行線的兩個人,偶像還是偶像,工程師還是工程師!來看日劇!
小結:
- 序列化(Serialize)及反射(reflection)都是其他深層複製的方法。
- 因為這次程式的改法是屬性一個一個自己複製,會有維護性的風險,推薦參考余小張大的[C#.NET]利用序列化進行類別深複製 。
- 字串String沒受影響是因為字串變更時會自動重新配置記憶體。
參考: