理解淺拷貝和深拷貝
1、淺拷貝與深拷貝的定義
什麼是拷貝?拷貝即為常說的複製或者克隆一個物件,並且通過拷貝這些源物件建立新的物件。其中拷貝分為淺拷貝和深拷貝。對於拷貝出來的物件,在使用上有很大的差異,特別是在引用型別上。
淺拷貝:將物件中的所有欄位複製到新的物件中。其中, 值型別 欄位被複制到新物件中後,在新物件中的修改不會影響到原先物件的值。而新物件的 引用型別 則是原先物件引用型別的引用,不是引用自己物件本身。 注 :在新物件中修改引用型別的值會影響到原先物件; 理論上String也是引用型別,但是由於由於該型別比較特殊,Object.MemberwiseClone()方法依舊為其新物件開闢了新的記憶體空間儲存String的值,在淺拷貝中把String型別當作'值型別'即可。
深拷貝:同樣也是拷貝,但是與淺拷貝不同的是,深拷貝會對 引用型別 重新在創新一次(包括 值型別 ),在新物件做的任何修改都不會影響到源物件本身。
2、實現淺拷貝與深拷貝
在 .Net平臺開發中,要實現拷貝,微軟官方建議繼承ICloneable介面,該介面位於 ofollow,noindex" target="_blank">System 名稱空間下,該介面只實現一個Clone方法,我們可以根據具體專案需求在該方法內實現淺拷貝或者深拷貝。先實現一個淺拷貝,具體程式碼如下:
//Equal探索 static void Main() { //建立源物件 Teacher Source = new Teacher("Fode",18,DateTime.Now,22); Source.Print("源物件"); //淺拷貝物件 Teacher Target = Source.Clone() as Teacher; /* 理論上String也是引用型別,但是由於由於該型別比較特殊, Object.MemberwiseClone()方法依舊為其新物件開闢了新的記憶體空間儲存String的值, 在淺拷貝中把String型別當作'值型別'即可 */ Target.Name = "JJ"; Target.Student.Count = 11; Console.WriteLine("新物件的引用型別的值發生變化"); Target.Print("新物件"); Source.Print("源物件"); Console.ReadKey(); } class Teacher : ICloneable { public Teacher(String name, Int32 age, DateTime birthday, Int32 count) { this._name = name; this._age = age; this._birthday = birthday; this.Student = new Student() { Count = count }; } private Int32 _age; public Int32 Age { get { return _age; } set { _age = value; } } private String _name; public String Name { get { return _name; } set { _name = value; } } private DateTime _birthday; public DateTime Birthday { get { return _birthday; } set { _birthday = value; } } public Student Student { get; set; } public void Print(String title) { Console.WriteLine(title); Console.WriteLine($"基本資訊:姓名:{this.Name},年齡:{this.Age},生日:{this.Birthday.ToString("D")}"); Console.WriteLine($"引用型別的值{Student.ToString()}"); Console.WriteLine(); } //實現淺拷貝 public Object Clone() { return this.MemberwiseClone(); } } class Student { public Int32 Count { get; set; } public override string ToString() { return Count.ToString(); } }
其執行結果如下:
可以發現當新物件的引用型別發生改變後,其源物件的引用型別也發生改變(String型別除外),他們共同引用的是Student這個引用型別物件(即使發生變化的是其裡面的值型別),而新物件的值型別改變並不會到源型別的值型別。
而對於要實現深拷貝則有很多中方法了,比如在拷貝方法裡面 直接一個個屬性欄位賦值,但是一旦為源物件新增屬性或者欄位的時候,容易忘了修改拷貝方法中的值,最好使用序列化的方法進行深拷貝。深拷貝簡單程式碼如下,與淺拷貝同樣的案例,只是重寫了Clone()方法,並在類加了[Serializable]序列化特性標籤:
[Serializable] class Teacher : ICloneable { public Teacher(String name, Int32 age, DateTime birthday, Int32 count) { this._name = name; this._age = age; this._birthday = birthday; this.Student = new Student() { Count = count }; } private Int32 _age; public Int32 Age { get { return _age; } set { _age = value; } } private String _name; public String Name { get { return _name; } set { _name = value; } } private DateTime _birthday; public DateTime Birthday { get { return _birthday; } set { _birthday = value; } } public Student Student { get; set; } public void Print(String title) { Console.WriteLine(title); Console.WriteLine($"基本資訊:姓名:{this.Name},年齡:{this.Age},生日:{this.Birthday.ToString("D")}"); Console.WriteLine($"引用型別的值{Student.ToString()}"); Console.WriteLine(); } //實現深拷貝拷貝 public Object Clone() { System.IO.Stream stream = new MemoryStream(); try { System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); formatter.Serialize(stream, this); stream.Seek(0, SeekOrigin.Begin); return formatter.Deserialize(stream); } finally { stream.Close(); stream.Dispose(); } } } [Serializable] class Student { public Int32 Count { get; set; } public override string ToString() { return Count.ToString(); } } static void Main() { //建立源物件 Teacher Source = new Teacher("Fode", 18, DateTime.Now, 22); Source.Print("源物件"); //深拷貝物件 Teacher Target = Source.Clone() as Teacher; Target.Name = "JJ"; Target.Student.Count = 11; Console.WriteLine("新物件的引用型別的值發生變化"); Target.Print("新物件"); Source.Print("源物件"); Console.ReadKey(); }
其結果如下:
可以發現,此時拷貝後的Target物件與源物件沒有任何關係。修改源物件的引用型別並不會影響對應新物件的值。最後在把程式碼優化一下,在一個類中同時實現深拷貝和淺拷貝:
//實現淺拷貝 public Object Clone() { return this.MemberwiseClone(); } /// <summary> /// 獲得淺拷貝物件 /// </summary> /// <returns></returns> public Teacher ShallowClone() { return this.Clone() as Teacher; } /// <summary> /// 獲得深拷貝物件 /// </summary> /// <returns></returns> public Teacher DeepClone() { System.IO.Stream stream = new MemoryStream(); try { System.Runtime.Serialization.IFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter(); formatter.Serialize(stream, this); stream.Seek(0, SeekOrigin.Begin); return formatter.Deserialize(stream) as Teacher; } finally { stream.Close(); stream.Dispose(); } }
注:ICloneable介面適用於
.NET Core
2.2 2.1 2.0 1.1 1.0
.NET Framework
4.8 4.7.2 4.7.1 4.7 4.6.2 4.6.1 4.6 4.5.2 4.5.1 4.5 4.0 3.5 3.0 2.0 1.1
.NET Standard
2.0 1.6 1.5 1.4 1.3 1.2 1.1 1.0
Xamarin.Android
7.1
Xamarin.iOS
10.8
Xamarin.Mac
3.0
誤區:要區別賦值操作,當物件採用賦值操作"="其實是引用源物件在堆中的地址,他們兩個物件引用的是同一個地址,所以說,當源物件(或新物件)的值型別改變後都會影響到其新物件(或源物件)。具體程式碼如下:
Teacher Source = new Teacher("Fode", 18, DateTime.Now, 22); Teacher Target = Source; Source.Print("源物件"); Console.WriteLine("源物件的值型別發生改變"); Source.Name = "JJ"; Source.Age = 22; Source.Print("源物件"); Target.Print("新物件"); Console.WriteLine("新物件的值型別發生改變"); Target.Name = "范冰冰"; Target.Age = 18; Source.Print("源物件"); Target.Print("新物件"); Console.ReadKey();
輸出結果如下: