今天我們談?wù)?a href="http://ttokpm.com/tags/c#/" target="_blank">C#中的對(duì)象拷貝問(wèn)題;
所謂的對(duì)象拷貝,其實(shí)就是為對(duì)象創(chuàng)建副本,C#中將拷貝分為兩種,分別為淺拷貝和深拷貝;
所謂淺拷貝就是將對(duì)象中的所有字段復(fù)制到新的副本對(duì)象中;淺拷貝對(duì)于值類型與引用類型的方式有區(qū)別,值類型字段的值被復(fù)制到副本中后,在副本中的修改不會(huì)影響源對(duì)象對(duì)應(yīng)的值;然而對(duì)于引用類型的字段被復(fù)制到副本中的卻是引用類型的引用,而不是引用的對(duì)象,在副本中對(duì)引用類型的字段值被修改后,源對(duì)象的值也將被修改。
深拷貝也同樣是將對(duì)象中的所有字段復(fù)制到副本對(duì)象中,但是,無(wú)論對(duì)象的值類型字段或者引用類型字段,都會(huì)被重新創(chuàng)建并復(fù)制,對(duì)于副本的修改,不會(huì)影響到源對(duì)象的本身;
當(dāng)然,無(wú)論是哪種拷貝,微軟都建議使用類型繼承ICloneable接口的方式明確告訴調(diào)用者,該對(duì)象是否可用被拷貝。當(dāng)然了,ICloneable接口只提供了一個(gè)聲明為Clone的方法,我們可以根據(jù)需求在Clone的方法內(nèi)實(shí)現(xiàn)淺拷貝或者是深拷貝
淺拷貝和深拷貝的區(qū)別
淺拷貝是指將對(duì)象中的數(shù)值類型的字段拷貝到新的對(duì)象中,而對(duì)象中的引用型字段則指復(fù)制它的一個(gè)引用到目標(biāo)對(duì)象。如果改變目標(biāo)對(duì)象 中引用型字段的值他將反映在原是對(duì)象中,也就是說(shuō)原始對(duì)象中對(duì)應(yīng)的字段也會(huì)發(fā)生變化。深拷貝與淺拷貝不同的是對(duì)于引用的處理,深拷貝將會(huì)在新對(duì)象中創(chuàng)建一 個(gè)新的和原是對(duì)象中對(duì)應(yīng)字段相同(內(nèi)容相同)的字段,也就是說(shuō)這個(gè)引用和原是對(duì)象的引用是不同的,我們?cè)诟淖冃聦?duì)象中的這個(gè)字段的時(shí)候是不會(huì)影響到原始對(duì) 象中對(duì)應(yīng)字段的內(nèi)容。所以對(duì)于原型模式也有不同的兩種處理方法:對(duì)象的淺拷貝和深拷貝。
深淺拷貝的幾種實(shí)現(xiàn)方式
上面已經(jīng)明白了深淺拷貝的定義,至于他們之間的區(qū)別也在定義中也有所體現(xiàn)。介紹完了它們的定義和區(qū)別之后,自然也就有了如何去實(shí)現(xiàn)它們呢?
對(duì)于,淺拷貝的實(shí)現(xiàn)方式很簡(jiǎn)單,.NET自身也提供了實(shí)現(xiàn)。我們知道,所有對(duì)象的父對(duì)象都是System.Object對(duì)象,這個(gè)父對(duì)象中有一個(gè)MemberwiseClone方法,該方法就可以用來(lái)實(shí)現(xiàn)淺拷貝,下面具體看看淺拷貝的實(shí)現(xiàn)方式,具體演示代碼如下所示:
// 繼承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;
}
上面類中重寫(xiě)了IConeable接口的Clone方法,其實(shí)現(xiàn)直接調(diào)用了Object的MemberwiseClone方法來(lái)完成淺拷貝,如果想實(shí)現(xiàn)深拷貝,也可以在Clone方法中實(shí)現(xiàn)深拷貝的邏輯。接下來(lái)就是對(duì)上面定義的類進(jìn)行淺拷貝測(cè)試了,看看是否是實(shí)現(xiàn)的淺拷貝,具體演示代碼如下所示:
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種方式實(shí)現(xiàn)的都是淺拷貝
List《PersonA》 personsCopy = new List《PersonA》(personList);
PersonA[] personCopy2 = new PersonA[2];
personList.CopyTo(personCopy2);
// 由于實(shí)現(xiàn)的是淺拷貝,所以改變一個(gè)對(duì)象的值,其他2個(gè)對(duì)象的值都會(huì)發(fā)生改變,因?yàn)樗鼈兌际鞘褂玫耐环輰?shí)體,即它們指向內(nèi)存中同一個(gè)地址
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); } } }
上面代碼的運(yùn)行結(jié)果如下圖所示:
從上面運(yùn)行結(jié)果可以看出,.NET中值類型默認(rèn)是深拷貝的,而對(duì)于引用類型,默認(rèn)實(shí)現(xiàn)的是淺拷貝。所以對(duì)于類中引用類型的屬性改變時(shí),其另一個(gè)對(duì)象也會(huì)發(fā)生改變。
上面已經(jīng)介紹了淺拷貝的實(shí)現(xiàn)方式,那深拷貝要如何實(shí)現(xiàn)呢?在前言部分已經(jīng)介紹了,實(shí)現(xiàn)深拷貝的方式有:反射、反序列化和表達(dá)式樹(shù)。在這里,我只介紹反射和反序列化的方式,對(duì)于表達(dá)式樹(shù)的方式在網(wǎng)上也沒(méi)有找到,當(dāng)時(shí)面試官說(shuō)是可以的,如果大家找到了表達(dá)式樹(shù)的實(shí)現(xiàn)方式,麻煩還請(qǐng)留言告知下。下面我們首先來(lái)看看反射的實(shí)現(xiàn)方式吧:
// 利用反射實(shí)現(xiàn)深拷貝
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;
}
反序列化的實(shí)現(xiàn)方式,反序列化的方式也可以細(xì)分為3種,具體的實(shí)現(xiàn)如下所示:
// 利用XML序列化和反序列化實(shí)現(xiàn)
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;
}
// 利用二進(jìn)制序列化和反序列實(shí)現(xiàn)
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);
// 反序列化成對(duì)象
retval = bf.Deserialize(ms);
ms.Close();
}
return (T)retval;
}
// 利用DataContractSerializer序列化和反序列化實(shí)現(xiàn)
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;
}
// 表達(dá)式樹(shù)實(shí)現(xiàn)
// 。。。。
?
也許會(huì)有人這樣解釋C# 中淺拷貝與深拷貝區(qū)別:
淺拷貝是對(duì)引用類型拷貝地址,對(duì)值類型直接進(jìn)行拷貝。
不能說(shuō)它完全錯(cuò)誤,但至少還不夠嚴(yán)謹(jǐn)。比如:string 類型咋說(shuō)?
其實(shí),我們可以通過(guò)實(shí)踐來(lái)尋找答案。
首先,定義以下類型:
int 、string 、enum 、struct 、class 、int[ ] 、string[ ]
代碼如下:
//枚舉
public enum myEnum
{ _1 = 1, _2 = 2 }
//結(jié)構(gòu)體
public struct myStruct
{
public int _int;
public myStruct(int i)
{ _int = i; }
}
//類
class myClass
{
public string _string;
public myClass(string s)
{ _string = s; }
}
//ICloneable:創(chuàng)建作為當(dāng)前實(shí)例副本的新對(duì)象。
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”);
//數(shù)組
public int[] arrInt = new int[] { 1 };
public string[] arrString = new string[] { “1” };
//返回此實(shí)例副本的新對(duì)象
public object Clone()
{
//MemberwiseClone:返回當(dāng)前對(duì)象的淺表副本(它是Object對(duì)象的基方法)
return this.MemberwiseClone();
}
}
注意:
ICloneable 接口:支持克隆,即用與現(xiàn)有實(shí)例相同的值創(chuàng)建類的新實(shí)例。
MemberwiseClone 方法:創(chuàng)建當(dāng)前 System.Object 的淺表副本。
接下來(lái),構(gòu)建實(shí)例A ,并對(duì)實(shí)例A 克隆產(chǎn)生一個(gè)實(shí)例B。
然后,改變實(shí)例B 的值,并觀察實(shí)例A 的值會(huì)不會(huì)被改變。
代碼如下:
class 淺拷貝與深拷貝
{
static void Main(string[] args)
{
DemoClass A = new DemoClass();
//創(chuàng)建實(shí)例A的副本 --》 新對(duì)象實(shí)例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();
}
}
結(jié)果如下:
從最后的輸出結(jié)果,我們得知:
對(duì)于內(nèi)部的Class 對(duì)象和數(shù)組,則Copy 一份地址。[ 改變B 時(shí),A也被改變了 ]
而對(duì)于其它內(nèi)置的int / string / enum / struct / object 類型,則Copy 一份值。
有一位網(wǎng)友說(shuō):string 類型雖然是引用類型,但是很多情況下.Net 把string 做值類型來(lái)處理,我覺(jué)得string 應(yīng)該也是按照值類型處理的。
這說(shuō)明他對(duì)string 類型還不夠了解。
可以肯定的是:string 一定是引用類型。那它為什么是深拷貝呢?
如果你看一下string 類型的源代碼就知道了:
//表示空字符串。此字段為只讀。
public static readonly string Empty;
答案就在于 string 是 readonly 的,當(dāng)改變 string 類型的數(shù)據(jù)值時(shí),將重新分配了內(nèi)存地址。
下面引用一段網(wǎng)友的代碼:Vseen[ Aloner ] 的個(gè)人陋見(jiàn):
public class Student
{
// 這里用“字段”,其實(shí)應(yīng)當(dāng)是屬性。
public string Name;
public int Age;
//自定義類 Classroom
public Classroom Class;
}
淺拷貝:Student A 淺拷貝出 Student B,Name和Age擁有新的內(nèi)存地址,但引用了同一個(gè) Classroom。
深拷貝:Student A 淺拷貝出 Student B,Name和Age擁有新的內(nèi)存地址,并且A.Classroom 的內(nèi)存地址不等于 B.Classroom。
其實(shí)俗點(diǎn)講,有點(diǎn)像:
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;
//根據(jù)情況,對(duì)Teacher 進(jìn)行判定要進(jìn)行的是深拷貝,還是淺拷貝。
}
淺拷貝:給對(duì)象拷貝一份新的對(duì)象。
淺拷貝的定義 —— 只對(duì)值類型(或string)類型分配新的內(nèi)存地址。
深拷貝:給對(duì)象拷貝一份全新的對(duì)象。
深拷貝的定義 —— 對(duì)值類型分配新的內(nèi)存地址,引用類型、以及引用類型的內(nèi)部字段分配的新的地址。
我是這么定義的:淺拷貝,換湯不換藥。
注意:
1、在 .NET 程序中,應(yīng)該避免使用 ICloneable 接口。
因?yàn)橥ㄟ^(guò)該接口無(wú)法判斷究竟是淺拷貝還是深拷貝,這會(huì)造成誤解或誤用。
2、深拷貝應(yīng)該復(fù)制該對(duì)象本身及通過(guò)該對(duì)象所能到達(dá)的完整的對(duì)象圖,淺拷貝只復(fù)制對(duì)象本身(就是該對(duì)象所表示的在堆中的一塊連續(xù)地址中的內(nèi)容)。
評(píng)論
查看更多