今天我們談談C#中的對象拷貝問題;
所謂的對象拷貝,其實就是為對象創建副本,C#中將拷貝分為兩種,分別為淺拷貝和深拷貝;
所謂淺拷貝就是將對象中的所有字段復制到新的副本對象中;淺拷貝對于值類型與引用類型的方式有區別,值類型字段的值被復制到副本中后,在副本中的修改不會影響源對象對應的值;然而對于引用類型的字段被復制到副本中的卻是引用類型的引用,而不是引用的對象,在副本中對引用類型的字段值被修改后,源對象的值也將被修改。
深拷貝也同樣是將對象中的所有字段復制到副本對象中,但是,無論對象的值類型字段或者引用類型字段,都會被重新創建并復制,對于副本的修改,不會影響到源對象的本身;
當然,無論是哪種拷貝,微軟都建議使用類型繼承ICloneable接口的方式明確告訴調用者,該對象是否可用被拷貝。當然了,ICloneable接口只提供了一個聲明為Clone的方法,我們可以根據需求在Clone的方法內實現淺拷貝或者是深拷貝
淺拷貝和深拷貝的區別
淺拷貝是指將對象中的數值類型的字段拷貝到新的對象中,而對象中的引用型字段則指復制它的一個引用到目標對象。如果改變目標對象 中引用型字段的值他將反映在原是對象中,也就是說原始對象中對應的字段也會發生變化。深拷貝與淺拷貝不同的是對于引用的處理,深拷貝將會在新對象中創建一 個新的和原是對象中對應字段相同(內容相同)的字段,也就是說這個引用和原是對象的引用是不同的,我們在改變新對象中的這個字段的時候是不會影響到原始對 象中對應字段的內容。所以對于原型模式也有不同的兩種處理方法:對象的淺拷貝和深拷貝。
深淺拷貝的幾種實現方式
上面已經明白了深淺拷貝的定義,至于他們之間的區別也在定義中也有所體現。介紹完了它們的定義和區別之后,自然也就有了如何去實現它們呢?
對于,淺拷貝的實現方式很簡單,.NET自身也提供了實現。我們知道,所有對象的父對象都是System.Object對象,這個父對象中有一個MemberwiseClone方法,該方法就可以用來實現淺拷貝,下面具體看看淺拷貝的實現方式,具體演示代碼如下所示:
// 繼承ICloneable接口,重新其Clone方法
class ShallowCopyDemoClass : ICloneable
{
public int intValue = 1;
public string strValue = “1”;
public PersonEnum pEnum = PersonEnum.EnumA;
public PersonStruct pStruct = new PersonStruct() { StructValue = 1};
public Person pClass = new Person(“1”);
public int[] pIntArray = new int[] { 1 };
public string[] pStringArray = new string[] { “1” };
#region ICloneable成員
public object Clone()
{
return this.MemberwiseClone();
}
#endregion
}
class Person
{
public string Name;
public Person(string name)
{
Name = name;
}
}
public enum PersonEnum
{
EnumA = 0,
EnumB = 1
}
public struct PersonStruct
{
public int StructValue;
}
上面類中重寫了IConeable接口的Clone方法,其實現直接調用了Object的MemberwiseClone方法來完成淺拷貝,如果想實現深拷貝,也可以在Clone方法中實現深拷貝的邏輯。接下來就是對上面定義的類進行淺拷貝測試了,看看是否是實現的淺拷貝,具體演示代碼如下所示:
class Program
{
static void Main(string[] args)
{
ShallowCopyDemo();
// List淺拷貝的演示
ListShallowCopyDemo();
}
public static void ListShallowCopyDemo()
{
List《PersonA》 personList = new List《PersonA》()
{
new PersonA() { Name=“PersonA”, Age= 10, ClassA= new A() { TestProperty = “AProperty”} },
new PersonA() { Name=“PersonA2”, Age= 20, ClassA= new A() { TestProperty = “AProperty2”} }
};
// 下面2種方式實現的都是淺拷貝
List《PersonA》 personsCopy = new List《PersonA》(personList);
PersonA[] personCopy2 = new PersonA[2];
personList.CopyTo(personCopy2);
// 由于實現的是淺拷貝,所以改變一個對象的值,其他2個對象的值都會發生改變,因為它們都是使用的同一份實體,即它們指向內存中同一個地址
personsCopy.First().ClassA.TestProperty = “AProperty3”;
WriteLog(string.Format(“personCopy2.First().ClassA.TestProperty is {0}”, personCopy2.First().ClassA.TestProperty));
WriteLog(string.Format(“personList.First().ClassA.TestProperty is {0}”, personList.First().ClassA.TestProperty));
WriteLog(string.Format(“personsCopy.First().ClassA.TestProperty is {0}”, personsCopy.First().ClassA.TestProperty));
Console.Read();
}
public static void ShallowCopyDemo()
{
ShallowCopyDemoClass DemoA = new ShallowCopyDemoClass();
ShallowCopyDemoClass DemoB = DemoA.Clone() as ShallowCopyDemoClass ;
DemoB.intValue = 2;
WriteLog(string.Format(“ int-》[A:{0}] [B:{1}]”, DemoA.intValue, DemoB.intValue));
DemoB.strValue = “2”;
WriteLog(string.Format(“ string-》[A:{0}] [B:{1}]”, DemoA.strValue, DemoB.strValue));
DemoB.pEnum = PersonEnum.EnumB;
WriteLog(string.Format(“ Enum-》[A: {0}] [B:{1}]”, DemoA.pEnum, DemoB.pEnum));
DemoB.pStruct.StructValue = 2;
WriteLog(string.Format(“ struct-》[A: {0}] [B: {1}]”, DemoA.pStruct.StructValue, DemoB.pStruct.StructValue));
DemoB.pIntArray[0] = 2;
WriteLog(string.Format(“ intArray-》[A:{0}] [B:{1}]”, DemoA.pIntArray[0], DemoB.pIntArray[0]));
DemoB.pStringArray[0] = “2”;
WriteLog(string.Format(“stringArray-》[A:{0}] [B:{1}]”, DemoA.pStringArray[0], DemoB.pStringArray[0]));
DemoB.pClass.Name = “2”;
WriteLog(string.Format(“ Class-》[A:{0}] [B:{1}]”, DemoA.pClass.Name, DemoB.pClass.Name));
Console.WriteLine();
}
private static void WriteLog(string msg) { Console.WriteLine(msg); } } }
上面代碼的運行結果如下圖所示:
從上面運行結果可以看出,.NET中值類型默認是深拷貝的,而對于引用類型,默認實現的是淺拷貝。所以對于類中引用類型的屬性改變時,其另一個對象也會發生改變。
上面已經介紹了淺拷貝的實現方式,那深拷貝要如何實現呢?在前言部分已經介紹了,實現深拷貝的方式有:反射、反序列化和表達式樹。在這里,我只介紹反射和反序列化的方式,對于表達式樹的方式在網上也沒有找到,當時面試官說是可以的,如果大家找到了表達式樹的實現方式,麻煩還請留言告知下。下面我們首先來看看反射的實現方式吧:
// 利用反射實現深拷貝
public static T DeepCopyWithReflection《T》(T obj)
{
Type type = obj.GetType();
// 如果是字符串或值類型則直接返回
if (obj is string || type.IsValueType) return obj;
if (type.IsArray)
{
Type elementType = Type.GetType(type.FullName.Replace(“[]”, string.Empty));
var array = obj as Array;
Array copied = Array.CreateInstance(elementType, array.Length);
for (int i = 0; i 《 array.Length; i++)
{
copied.SetValue(DeepCopyWithReflection(array.GetValue(i)), i);
}
return (T)Convert.ChangeType(copied, obj.GetType());
}
object retval = Activator.CreateInstance(obj.GetType());
PropertyInfo[] properties = obj.GetType().GetProperties(
BindingFlags.Public | BindingFlags.NonPublic
| BindingFlags.Instance | BindingFlags.Static);
foreach (var property in properties)
{
var propertyValue = property.GetValue(obj, null);
if (propertyValue == null)
continue;
property.SetValue(retval, DeepCopyWithReflection(propertyValue), null);
}
return (T)retval;
}
反序列化的實現方式,反序列化的方式也可以細分為3種,具體的實現如下所示:
// 利用XML序列化和反序列化實現
public static T DeepCopyWithXmlSerializer《T》(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
XmlSerializer xml = new XmlSerializer(typeof(T));
xml.Serialize(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
retval = xml.Deserialize(ms);
ms.Close();
}
return (T)retval;
}
// 利用二進制序列化和反序列實現
public static T DeepCopyWithBinarySerialize《T》(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
// 序列化成流
bf.Serialize(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
// 反序列化成對象
retval = bf.Deserialize(ms);
ms.Close();
}
return (T)retval;
}
// 利用DataContractSerializer序列化和反序列化實現
public static T DeepCopy《T》(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
DataContractSerializer ser = new DataContractSerializer(typeof(T));
ser.WriteObject(ms, obj);
ms.Seek(0, SeekOrigin.Begin);
retval = ser.ReadObject(ms);
ms.Close();
}
return (T)retval;
}
// 表達式樹實現
// 。。。。
?
也許會有人這樣解釋C# 中淺拷貝與深拷貝區別:
淺拷貝是對引用類型拷貝地址,對值類型直接進行拷貝。
不能說它完全錯誤,但至少還不夠嚴謹。比如:string 類型咋說?
其實,我們可以通過實踐來尋找答案。
首先,定義以下類型:
int 、string 、enum 、struct 、class 、int[ ] 、string[ ]
代碼如下:
//枚舉
public enum myEnum
{ _1 = 1, _2 = 2 }
//結構體
public struct myStruct
{
public int _int;
public myStruct(int i)
{ _int = i; }
}
//類
class myClass
{
public string _string;
public myClass(string s)
{ _string = s; }
}
//ICloneable:創建作為當前實例副本的新對象。
class DemoClass : ICloneable
{
public int _int = 1;
public string _string = “1”;
public myEnum _enum = myEnum._1;
public myStruct _struct = new myStruct(1);
public myClass _class = new myClass(“1”);
//數組
public int[] arrInt = new int[] { 1 };
public string[] arrString = new string[] { “1” };
//返回此實例副本的新對象
public object Clone()
{
//MemberwiseClone:返回當前對象的淺表副本(它是Object對象的基方法)
return this.MemberwiseClone();
}
}
注意:
ICloneable 接口:支持克隆,即用與現有實例相同的值創建類的新實例。
MemberwiseClone 方法:創建當前 System.Object 的淺表副本。
接下來,構建實例A ,并對實例A 克隆產生一個實例B。
然后,改變實例B 的值,并觀察實例A 的值會不會被改變。
代碼如下:
class 淺拷貝與深拷貝
{
static void Main(string[] args)
{
DemoClass A = new DemoClass();
//創建實例A的副本 --》 新對象實例B
DemoClass B = (DemoClass)A.Clone();
B._int = 2;
Console.WriteLine(“ int \t\t A:{0} B:{1}”, A._int, B._int);
B._string = “2”;
Console.WriteLine(“ string \t A:{0} B:{1}”, A._string, B._string);
B._enum = myEnum._2;
Console.WriteLine(“ enum \t\t A:{0} B:{1}”, (int)A._enum, (int)B._enum);
B._struct._int = 2;
Console.WriteLine(“ struct \t A:{0} B:{1}”,
A._struct._int, B._struct._int);
B._class._string = “2”;
Console.WriteLine(“ class \t\t A:{0} B:{1}”,
A._class._string, B._class._string);
B.arrInt[0] = 2;
Console.WriteLine(“ intArray \t A:{0} B:{1}”,
A.arrInt[0], B.arrInt[0]);
B.arrString[0] = “2”;
Console.WriteLine(“ stringArray \t A:{0} B:{1}”,
A.arrString[0], B.arrString[0]);
Console.ReadKey();
}
}
結果如下:
從最后的輸出結果,我們得知:
對于內部的Class 對象和數組,則Copy 一份地址。[ 改變B 時,A也被改變了 ]
而對于其它內置的int / string / enum / struct / object 類型,則Copy 一份值。
有一位網友說:string 類型雖然是引用類型,但是很多情況下.Net 把string 做值類型來處理,我覺得string 應該也是按照值類型處理的。
這說明他對string 類型還不夠了解。
可以肯定的是:string 一定是引用類型。那它為什么是深拷貝呢?
如果你看一下string 類型的源代碼就知道了:
//表示空字符串。此字段為只讀。
public static readonly string Empty;
答案就在于 string 是 readonly 的,當改變 string 類型的數據值時,將重新分配了內存地址。
下面引用一段網友的代碼:Vseen[ Aloner ] 的個人陋見:
public class Student
{
// 這里用“字段”,其實應當是屬性。
public string Name;
public int Age;
//自定義類 Classroom
public Classroom Class;
}
淺拷貝:Student A 淺拷貝出 Student B,Name和Age擁有新的內存地址,但引用了同一個 Classroom。
深拷貝:Student A 淺拷貝出 Student B,Name和Age擁有新的內存地址,并且A.Classroom 的內存地址不等于 B.Classroom。
其實俗點講,有點像:
public object Clone()
{
Student B = new Student();
B.Name = this.Name;
B.Age = this.Age;
//淺拷貝
B.Class = this.Class;
//深拷貝
B.Class = new Classromm();
B.Class.Name = this.Class.Name;
B.Class.Teacher = this.Class.Teacher;
//根據情況,對Teacher 進行判定要進行的是深拷貝,還是淺拷貝。
}
淺拷貝:給對象拷貝一份新的對象。
淺拷貝的定義 —— 只對值類型(或string)類型分配新的內存地址。
深拷貝:給對象拷貝一份全新的對象。
深拷貝的定義 —— 對值類型分配新的內存地址,引用類型、以及引用類型的內部字段分配的新的地址。
我是這么定義的:淺拷貝,換湯不換藥。
注意:
1、在 .NET 程序中,應該避免使用 ICloneable 接口。
因為通過該接口無法判斷究竟是淺拷貝還是深拷貝,這會造成誤解或誤用。
2、深拷貝應該復制該對象本身及通過該對象所能到達的完整的對象圖,淺拷貝只復制對象本身(就是該對象所表示的在堆中的一塊連續地址中的內容)。
評論
查看更多