.NET的深复制方法(以C#语言为例)
很多时候我们复制一个对象实例A到实例B,在用实例B去做其他事情的时候,会对实例B进行修改,为保证对B的修改不会影响到A的正常使用,就需要使用到深复制。
我在网上搜到一些深复制的方法,同时写了几组例子对这些方法进行测试。
我的操作系统版本为Win7旗舰版,.NETFramework版本是4.5
测试程序
我建了一个C#窗体应用程序(Winform),其主窗口FormMain的Load函数内容如下:
privatevoidFormMain_Load(objectsender,EventArgse) { //测试1:深度复制自定义类 try { Console.WriteLine("===深度复制自定义类==="); TestClasstest1=newTestClass(); test1.a=10; test1.b="helloworld!"; test1.c=newstring[]{"x","y","z"}; TestClasstest2=newTestClass(); test2.a=11; test2.b="helloworld2!"; test2.c=newstring[]{"i","j","k"}; test1.d=test2; Console.WriteLine("---test1_start---"); Console.WriteLine(test1); Console.WriteLine("---test1_end---"); TestClasstest3=(TestClass)DataManHelper.DeepCopyObject(test1); Console.WriteLine("---test3_start---"); Console.WriteLine(test3); Console.WriteLine("---test3_end---"); } catch(Exceptionex) { Console.WriteLine(ex.ToString()); }
//测试2:深度复制可序列化的自定义类
try { Console.WriteLine("===深度复制可序列化的自定义类==="); TestClassWithStest1=newTestClassWithS(); test1.a=10; test1.b="helloworld!"; test1.c=newstring[]{"x","y","z"}; TestClassWithStest2=newTestClassWithS(); test2.a=11; test2.b="helloworld2!"; test2.c=newstring[]{"i","j","k"}; test1.d=test2; Console.WriteLine("---test1_start---"); Console.WriteLine(test1); Console.WriteLine("---test1_end---"); TestClassWithStest3=(TestClassWithS)DataManHelper.DeepCopyObject(test1); Console.WriteLine("---test3_start---"); Console.WriteLine(test3); Console.WriteLine("---test3_end---"); } catch(Exceptionex) { Console.WriteLine(ex.ToString()); }
//测试3:深度复制DataTable
try { Console.WriteLine("===深度复制DataTable==="); DataTabledtKirov=newDataTable("TestTable"); dtKirov.Columns.Add("Col1"); dtKirov.Columns.Add("Col2"); dtKirov.Columns.Add("Col3"); dtKirov.Rows.Add("1-1","1-2","1-3"); dtKirov.Rows.Add("2-1","2-2","2-3"); dtKirov.Rows.Add("3-1","3-2","3-3"); Console.WriteLine("===复制前==="); for(inti=0;i<dtKirov.Columns.Count;i++) { Console.Write(dtKirov.Columns[i].ColumnName+"\t"); } Console.WriteLine("\n-----------------"); for(inti=0;i<dtKirov.Columns.Count;i++) { for(intj=0;j<dtKirov.Rows.Count;j++) { Console.Write(dtKirov.Rows[i][j].ToString()+"\t"); } Console.WriteLine(); } Console.WriteLine(); DataTabledtDreadNought=(DataTable)DataManHelper.DeepCopyObject(dtKirov); Console.WriteLine("===复制后==="); for(inti=0;i<dtDreadNought.Columns.Count;i++) { Console.Write(dtDreadNought.Columns[i].ColumnName+"\t"); } Console.WriteLine("\n-----------------"); for(inti=0;i<dtDreadNought.Columns.Count;i++) { for(intj=0;j<dtDreadNought.Rows.Count;j++) { Console.Write(dtDreadNought.Rows[i][j].ToString()+"\t"); } Console.WriteLine(); } Console.WriteLine(); } catch(Exceptionex) { Console.WriteLine(ex.ToString()); }
//测试4:深度复制TextBox
try { Console.WriteLine("===深度复制TextBox==="); txtTest.Text="1234"; Console.WriteLine("复制前:"+txtTest.Text); TextBoxtxtTmp=newTextBox(); txtTmp=(TextBox)DataManHelper.DeepCopyObject(txtTest); Console.WriteLine("复制后:"+txtTmp.Text); } catch(Exceptionex) { Console.WriteLine(ex.ToString()); }
//测试5:深度复制DataGridView
try { Console.WriteLine("===深度复制DataGridView==="); DataGridViewdgvTmp=newDataGridView(); dgvTmp=(DataGridView)DataManHelper.DeepCopyObject(dgvTest); } catch(Exceptionex) { Console.WriteLine(ex.ToString()); } }
其中txtTest是一个测试用的TextBox,dgvTmp是一个测试用的DataGridView,TestClass是一个自定义类,TestClassWithS是添加了Serializable特性的TestClass类,它们的具体实现如下:
usingSystem; usingSystem.Collections.Generic; usingSystem.Linq; usingSystem.Text; namespaceDataCopyTest { publicclassTestClass { publicinta; publicstringb; publicstring[]c; publicTestClassd; publicoverridestringToString() { strings="a:"+a+"\n"; if(b!=null) { s+="b:"+b+"\n"; } if(c!=null) { foreach(stringtmpsinc) { if(!string.IsNullOrWhiteSpace(tmps)) { s+="c:"+tmps+"\n"; } } } if(d!=null) { s+=d.ToString(); } returns; } } //支持序列化的TestClass [Serializable] publicclassTestClassWithS { publicinta; publicstringb; publicstring[]c; publicTestClassWithSd; publicoverridestringToString() { strings="a:"+a+"\n"; if(b!=null) { s+="b:"+b+"\n"; } if(c!=null) { foreach(stringtmpsinc) { if(!string.IsNullOrWhiteSpace(tmps)) { s+="c:"+tmps+"\n"; } } } if(d!=null) { s+=d.ToString(); } returns; } } }
我对每个搜来的深复制方法,都用了这五个类的实例进行深复制测试,这五个类的特征如下:
I、对自定义类TestClass进行深复制测试
II、对自定义类TestClassWithS进行深复制测试,TestClassWithS是添加了Serializable特性的TestClass类
III、对DataTable进行深复制测试
IV、对控件TextBox进行深复制测试
V、对控件DataGridView进行深复制测试
我们通过实现方法DataManHelper.DeepCopyObject来进行测试
测试深复制方法1
使用二进制流的序列化与反序列化深度复制对象
publicstaticobjectDeepCopyObject(objectobj) { BinaryFormatterFormatter=newBinaryFormatter(null, newStreamingContext(StreamingContextStates.Clone)); MemoryStreamstream=newMemoryStream(); Formatter.Serialize(stream,obj); stream.Position=0; objectclonedObj=Formatter.Deserialize(stream); stream.Close(); returnclonedObj; }
五个场景的测试结果为:
I、触发异常SerializationException,原因是该类不支持序列化
“System.Runtime.Serialization.SerializationException”类型的第一次机会异常在mscorlib.dll中发生
System.Runtime.Serialization.SerializationException:程序集“DataCopyTest,Version=1.0.0.0,Culture=neutral,PublicKeyToken=null”中的类型“DataCopyTest.TestClass”未标记为可序列化。
在System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers(RuntimeTypetype)
在System.Runtime.Serialization.FormatterServices.GetSerializableMembers(Typetype,StreamingContextcontext)
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo()
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Objectobj,ISurrogateSelectorsurrogateSelector,StreamingContextcontext,SerObjectInfoInitserObjectInfoInit,IFormatterConverterconverter,ObjectWriterobjectWriter,SerializationBinderbinder)
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize(Objectobj,ISurrogateSelectorsurrogateSelector,StreamingContextcontext,SerObjectInfoInitserObjectInfoInit,IFormatterConverterconverter,ObjectWriterobjectWriter,S“DataCopyTest.vshost.exe”(托管(v4.0.30319)):已加载“C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Numerics\v4.0_4.0.0.0__b77a5c561934e089\System.Numerics.dll”
erializationBinderbinder)
在System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Objectgraph,Header[]inHeaders,__BinaryWriterserWriter,BooleanfCheck)
在System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(StreamserializationStream,Objectgraph,Header[]headers,BooleanfCheck)
在System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(StreamserializationStream,Objectgraph)
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号24
在DataCopyTest.FormMain.FormMain_Load(Objectsender,EventArgse)位置d:\MyPrograms\DataCopyTest\DataCopyTest\FormMain.cs:行号37
II、可正常复制(√)
III、可正常复制(√)
IV、触发异常SerializationException,原因是该类不支持序列化
“System.Runtime.Serialization.SerializationException”类型的第一次机会异常在mscorlib.dll中发生
System.Runtime.Serialization.SerializationException:程序集“System.Windows.Forms,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089”中的类型“System.Windows.Forms.TextBox”未标记为可序列化。
在System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers(RuntimeTypetype)
在System.Runtime.Serialization.FormatterServices.GetSerializableMembers(Typetype,StreamingContextcontext)
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo()
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Objectobj,ISurrogateSelectorsurrogateSelector,StreamingContextcontext,SerObjectInfoInitserObjectInfoInit,IFormatterConverterconverter,ObjectWriterobjectWriter,SerializationBinderbinder)
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize(Objectobj,ISurrogateSelectorsurrogateSelector,StreamingContextcontext,SerObjectInfoInitserObjectInfoInit,IFormatterConverterconverter,ObjectWriterobjectWriter,SerializationBinderbinder)
在System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Objectgraph,Header[]inHeaders,__BinaryWriterserWriter,BooleanfCheck)
在System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(StreamserializationStream,Objectgraph,Header[]headers,BooleanfCheck)
在System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(StreamserializationStream,Objectgraph)
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号24
在DataCopyTest.FormMain.FormMain_Load(Objectsender,EventArgse)位置d:\MyPrograms\DataCopyTest\DataCopyTest\FormMain.cs:行号128
V、触发异常SerializationException,原因是该类不支持序列化
“System.Runtime.Serialization.SerializationException”类型的第一次机会异常在mscorlib.dll中发生
System.Runtime.Serialization.SerializationException:程序集“System.Windows.Forms,Version=4.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089”中的类型“System.Windows.Forms.DataGridView”未标记为可序列化。
在System.Runtime.Serialization.FormatterServices.InternalGetSerializableMembers(RuntimeTypetype)
在System.Runtime.Serialization.FormatterServices.GetSerializableMembers(Typetype,StreamingContextcontext)
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitMemberInfo()
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.InitSerialize(Objectobj,ISurrogateSelectorsurrogateSelector,StreamingContextcontext,SerObjectInfoInitserObjectInfoInit,IFormatterConverterconverter,ObjectWriterobjectWriter,SerializationBinderbinder)
在System.Runtime.Serialization.Formatters.Binary.WriteObjectInfo.Serialize(Objectobj,ISurrogateSelectorsurrogateSelector,StreamingContextcontext,SerObjectInfoInitserObjectInfoInit,IFormatterConverterconverter,ObjectWriterobjectWriter,SerializationBinderbinder)
在System.Runtime.Serialization.Formatters.Binary.ObjectWriter.Serialize(Objectgraph,Header[]inHeaders,__BinaryWriterserWriter,BooleanfCheck)
在System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(StreamserializationStream,Objectgraph,Header[]headers,BooleanfCheck)
在System.Runtime.Serialization.Formatters.Binary.BinaryFormatter.Serialize(StreamserializationStream,Objectgraph)
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号24
在DataCopyTest.FormMain.FormMain_Load(Objectsender,EventArgse)位置d:\MyPrograms\DataCopyTest\DataCopyTest\FormMain.cs:行号141
结论:利用序列化与反序列化到二进制流的方法深复制对象,只有在该对象支持Serializable特性时才可使用
测试深复制方法2
publicstaticobjectDeepCopyObject(objectobj) { Typet=obj.GetType(); PropertyInfo[]properties=t.GetProperties(); Objectp=t.InvokeMember("",System.Reflection.BindingFlags.CreateInstance,null,obj,null); foreach(PropertyInfopiinproperties) { if(pi.CanWrite) { objectvalue=pi.GetValue(obj,null); pi.SetValue(p,value,null); } } returnp; }
五个场景的测试结果为:
I、不会触发异常,但结果完全错误
II、不会触发异常,但结果完全错误
III、不会触发异常,但结果完全错误
IV、Text字段赋值结果正确,但其他内容不能保证
V、触发异常ArgumentOutOfRangeException、TargetInvocationException
“System.ArgumentOutOfRangeException”类型的第一次机会异常在System.Windows.Forms.dll中发生
“System.Reflection.TargetInvocationException”类型的第一次机会异常在mscorlib.dll中发生
System.Reflection.TargetInvocationException:调用的目标发生了异常。--->System.ArgumentOutOfRangeException:指定的参数已超出有效值的范围。
参数名:value
在System.Windows.Forms.DataGridView.set_FirstDisplayedScrollingColumnIndex(Int32value)
---内部异常堆栈跟踪的结尾---
在System.RuntimeMethodHandle.InvokeMethod(Objecttarget,Object[]arguments,Signaturesig,Booleanconstructor)
在System.Reflection.RuntimeMethodInfo.UnsafeInvokeInternal(Objectobj,Object[]parameters,Object[]arguments)
在System.Reflection.RuntimeMethodInfo.Invoke(Objectobj,BindingFlagsinvokeAttr,Binderbinder,Object[]parameters,CultureInfoculture)
在System.Reflection.RuntimePropertyInfo.SetValue(Objectobj,Objectvalue,BindingFlagsinvokeAttr,Binderbinder,Object[]index,CultureInfoculture)
在System.Reflection.RuntimePropertyInfo.SetValue(Objectobj,Objectvalue,Object[]index)
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号29
在DataCopyTest.FormMain.FormMain_Load(Objectsender,EventArgse)位置d:\MyPrograms\DataCopyTest\DataCopyTest\FormMain.cs:行号141
结论:使用这种方法进行所谓深复制,完全是自寻死路!
测试深复制方法3
publicstaticobjectDeepCopyObject(objectobj) { if(obj!=null) { objectresult=Activator.CreateInstance(obj.GetType()); foreach(FieldInfofieldinobj.GetType().GetFields()) { if(field.FieldType.GetInterface("IList",false)==null) { field.SetValue(result,field.GetValue(obj)); } else { IListlistObject=(IList)field.GetValue(result); if(listObject!=null) { foreach(objectitemin((IList)field.GetValue(obj))) { listObject.Add(DeepCopyObject(item)); } } } } returnresult; } else { returnnull; } }
五个场景的测试结果为:
I、可正常复制(√)
II、可正常复制(√)
III、未触发异常,复制后DataTable无行列
IV、未触发异常,Text字段未赋值
V、未触发异常
结论:这个方法只适用于深复制具备简单结构的类(如类中只有基础字段、数组等),对于不支持序列化的对象也可以进行深复制。
测试深复制方法4
这段代码来源同方法3
publicstaticobjectDeepCopyObject(objectobj) { if(obj==null) returnnull; Typetype=obj.GetType(); if(type.IsValueType||type==typeof(string)) { returnobj; } elseif(type.IsArray) { TypeelementType=Type.GetType( type.FullName.Replace("[]",string.Empty)); vararray=objasArray; Arraycopied=Array.CreateInstance(elementType,array.Length); for(inti=0;i<array.Length;i++) { copied.SetValue(DeepCopyObject(array.GetValue(i)),i); } returnConvert.ChangeType(copied,obj.GetType()); } elseif(type.IsClass) { objecttoret=Activator.CreateInstance(obj.GetType()); FieldInfo[]fields=type.GetFields(BindingFlags.Public| BindingFlags.NonPublic|BindingFlags.Instance); foreach(FieldInfofieldinfields) { objectfieldValue=field.GetValue(obj); if(fieldValue==null) continue; field.SetValue(toret,DeepCopyObject(fieldValue)); } returntoret; } else thrownewArgumentException("Unknowntype"); }
五个场景的测试结果为:
I、可正常复制(√)
II、可正常复制(√)
III、触发异常MissingMethodException
“System.MissingMethodException”类型的第一次机会异常在mscorlib.dll中发生
System.MissingMethodException:没有为该对象定义无参数的构造函数。
在System.RuntimeTypeHandle.CreateInstance(RuntimeTypetype,BooleanpublicOnly,BooleannoCheck,Boolean&canBeCached,RuntimeMethodHandleInternal&ctor,Boolean&bNeedSecurityCheck)
在System.RuntimeType.CreateInstanceSlow(BooleanpublicOnly,BooleanskipCheckThis,BooleanfillCache,StackCrawlMark&stackMark)
在System.RuntimeType.CreateInstanceDefaultCtor(BooleanpublicOnly,BooleanskipCheckThis,BooleanfillCache,StackCrawlMark&stackMark)
在System.Activator.CreateInstance(Typetype,BooleannonPublic)
在System.Activator.CreateInstance(Typetype)
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号45
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号53
在DataCopyTest.FormMain.FormMain_Load(Objectsender,EventArgse)位置d:\MyPrograms\DataCopyTest\DataCopyTest\FormMain.cs:行号99
IV、未触发异常,但Text字段也未赋值成功
V、触发异常MissingMethodException
“System.MissingMethodException”类型的第一次机会异常在mscorlib.dll中发生
System.MissingMethodException:没有为该对象定义无参数的构造函数。
在System.RuntimeTypeHandle.CreateInstance(RuntimeTypetype,BooleanpublicOnly,BooleannoCheck,Boolean&canBeCached,RuntimeMethodHandleInternal&ctor,Boolean&bNeedSecurityCheck)
在System.RuntimeType.CreateInstanceSlow(BooleanpublicOnly,BooleanskipCheckThis,BooleanfillCache,StackCrawlMark&stackMark)
在System.RuntimeType.CreateInstanceDefaultCtor(BooleanpublicOnly,BooleanskipCheckThis,BooleanfillCache,StackCrawlMark&stackMark)
在System.Activator.CreateInstance(Typetype,BooleannonPublic)
在System.Activator.CreateInstance(Typetype)
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号45
在DataCopyTest.DataManHelper.DeepCopyObject(Objectobj)位置d:\MyPrograms\DataCopyTest\DataCopyTest\DataManHelper.cs:行号53
在DataCopyTest.FormMain.FormMain_Load(Objectsender,EventArgse)位置d:\MyPrograms\DataCopyTest\DataCopyTest\FormMain.cs:行号141
结论:这个方法的作用类似方法3,只能深复制基本数据类型组成的类
具体问题具体分析
从上面的例子可以看出,想找一个放之四海而皆准的方式去深复制所有对象是很困难的。一些使用高级语言特性(反射)的深复制方法,即使可以在部分类上试用成功,也无法对所有的类都具备十足的把握。因此我认为应该采取下面的方式处理对象的深复制问题:
1、对于由基本数据类型组成的类,为之打上Serializable标签,直接使用序列化与反序列化的方法进行深复制
2、其他较为复杂的类型如DataGridView,可根据自身情况写一个方法进行深复制,之所以在这里说要根据自身情况写方法,是因为在对很多类进行复制时,你只需要复制对你有用的属性就行了。如TextBox控件中,只有Text一个属性对你是有用的,如果你需要在复制后的对象中用到Readonly等属性的值,那么在你自己实现的复制方法中,也加上对这些属性的赋值即可。这样做还有一个好处,就是方便进行一些定制化的开发。
如下面这段代码,就是对DataGridView的一个近似的深复制,这段代码将一个DataGridView(dgv)的内容复制到另一个DataGridView(dgvTmp)中,然后将dgvTmp传递给相关函数用于将DataGridView中的内容输出到Excel文档:
DataGridViewdgvTmp=newDataGridView(); dgvTmp.AllowUserToAddRows=false;//不允许用户生成行,否则导出后会多出最后一行 for(inti=0;i<dgv.Columns.Count;i++) { dgvTmp.Columns.Add(dgv.Columns[i].Name,dgv.Columns[i].HeaderText); if(dgv.Columns[i].DefaultCellStyle.Format.Contains("N"))//使导出Excel文档金额列可做SUM运算 { dgvTmp.Columns[i].DefaultCellStyle.Format=dgv.Columns[i].DefaultCellStyle.Format; } if(!dgv.Columns[i].Visible) { dgvTmp.Columns[i].Visible=false; } } for(inti=0;i<dgv.Rows.Count;i++) { object[]objList=newobject[dgv.Rows[i].Cells.Count]; for(intj=0;j<objList.Length;j++) { if(dgvTmp.Columns[j].DefaultCellStyle.Format.Contains("N")) { objList[j]=dgv.Rows[i].Cells[j].Value;//使导出Excel文档金额列可做SUM运算 } else { objList[j]=dgv.Rows[i].Cells[j].EditedFormattedValue;//数据字典按显示文字导出 } } dgvTmp.Rows.Add(objList); }
这段代码的特点如下:
1、DataGridView的属性AllowUserToAddRows要设置成false,否则导出到Excel文档后,会发现最后会多出一个空行。
2、我们在这里标记了那些列是隐藏列,这样在后面的处理中,如果要删除这些列,那删除的也是dgvTmp的列而不是dgv的列,保护了原数据。
3、对于部分数据字典的翻译,我们传的不是Value而是EditedFormattedValue,这种方式直接使用了dgv在屏幕上显示的翻译后文字,而不是原来的数据字典值。
4、对于部分金额列,需要直接传Value值,同时需要设置该列的DefaultCellStyle.Format,这样可使得这些内容在之后输出到Excel文档后,可做求和运算(Excel中类似“12,345.67”字符串是不能做求和运算的)。