.NET的深复制方法(以C#语言为例)
很多时候我们复制一个对象实例a到实例b,在用实例b去做其他事情的时候,会对实例b进行修改,为保证对b的修改不会影响到a的正常使用,就需要使用到深复制。
我在网上搜到一些深复制的方法,同时写了几组例子对这些方法进行测试。
我的操作系统版本为win7旗舰版,.net framework版本是4.5
测试程序
我建了一个c#窗体应用程序(winform),其主窗口formmain的load函数内容如下:
private void formmain_load(object sender, eventargs e) { //测试1:深度复制 自定义类 try { console.writeline("=== 深度复制 自定义类 ==="); testclass test1 = new testclass(); test1.a = 10; test1.b = "hello world!"; test1.c = new string[] { "x", "y", "z" }; testclass test2 = new testclass(); test2.a = 11; test2.b = "hello world2!"; test2.c = new string[] { "i", "j", "k" }; test1.d = test2; console.writeline("---test1_start---"); console.writeline(test1); console.writeline("---test1_end---"); testclass test3 = (testclass)datamanhelper.deepcopyobject(test1); console.writeline("---test3_start---"); console.writeline(test3); console.writeline("---test3_end---"); } catch (exception ex) { console.writeline(ex.tostring()); }
//测试2:深度复制 可序列化的自定义类
try { console.writeline("=== 深度复制 可序列化的自定义类 ==="); testclasswiths test1 = new testclasswiths(); test1.a = 10; test1.b = "hello world!"; test1.c = new string[] { "x", "y", "z" }; testclasswiths test2 = new testclasswiths(); test2.a = 11; test2.b = "hello world2!"; test2.c = new string[] { "i", "j", "k" }; test1.d = test2; console.writeline("---test1_start---"); console.writeline(test1); console.writeline("---test1_end---"); testclasswiths test3 = (testclasswiths)datamanhelper.deepcopyobject(test1); console.writeline("---test3_start---"); console.writeline(test3); console.writeline("---test3_end---"); } catch (exception ex) { console.writeline(ex.tostring()); }
//测试3:深度复制 datatable
try { console.writeline("=== 深度复制 datatable ==="); datatable dtkirov = new datatable("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 (int i = 0; i < dtkirov.columns.count; i++) { console.write(dtkirov.columns[i].columnname + "\t"); } console.writeline("\n-----------------"); for (int i = 0; i < dtkirov.columns.count; i++) { for (int j = 0; j < dtkirov.rows.count; j++) { console.write(dtkirov.rows[i][j].tostring() + "\t"); } console.writeline(); } console.writeline(); datatable dtdreadnought = (datatable)datamanhelper.deepcopyobject(dtkirov); console.writeline("=== 复制后 ==="); for (int i = 0; i < dtdreadnought.columns.count; i++) { console.write(dtdreadnought.columns[i].columnname + "\t"); } console.writeline("\n-----------------"); for (int i = 0; i < dtdreadnought.columns.count; i++) { for (int j = 0; j < dtdreadnought.rows.count; j++) { console.write(dtdreadnought.rows[i][j].tostring() + "\t"); } console.writeline(); } console.writeline(); } catch (exception ex) { console.writeline(ex.tostring()); }
//测试4:深度复制 textbox
try { console.writeline("=== 深度复制 textbox ==="); txttest.text = "1234"; console.writeline("复制前:" + txttest.text); textbox txttmp = new textbox(); txttmp = (textbox)datamanhelper.deepcopyobject(txttest); console.writeline("复制后:" + txttmp.text); } catch (exception ex) { console.writeline(ex.tostring()); }
//测试5:深度复制 datagridview
try { console.writeline("=== 深度复制 datagridview ==="); datagridview dgvtmp = new datagridview(); dgvtmp = (datagridview)datamanhelper.deepcopyobject(dgvtest); } catch (exception ex) { console.writeline(ex.tostring()); } }
其中txttest是一个测试用的textbox,dgvtmp是一个测试用的datagridview,testclass是一个自定义类,testclasswiths是添加了serializable特性的testclass类,它们的具体实现如下:
using system; using system.collections.generic; using system.linq; using system.text; namespace datacopytest { public class testclass { public int a; public string b; public string[] c; public testclass d; public override string tostring() { string s = "a:" + a + "\n"; if (b != null) { s += "b:" + b + "\n"; } if (c != null) { foreach (string tmps in c) { if (!string.isnullorwhitespace(tmps)) { s += "c:" + tmps + "\n"; } } } if (d != null) { s += d.tostring(); } return s; } } //支持序列化的testclass [serializable] public class testclasswiths { public int a; public string b; public string[] c; public testclasswiths d; public override string tostring() { string s = "a:" + a + "\n"; if (b != null) { s += "b:" + b + "\n"; } if (c != null) { foreach (string tmps in c) { if (!string.isnullorwhitespace(tmps)) { s += "c:" + tmps + "\n"; } } } if (d != null) { s += d.tostring(); } return s; } } }
我对每个搜来的深复制方法,都用了这五个类的实例进行深复制测试,这五个类的特征如下:
i、对自定义类testclass进行深复制测试
ii、对自定义类testclasswiths进行深复制测试,testclasswiths是添加了serializable特性的testclass类
iii、对datatable进行深复制测试
iv、对控件textbox进行深复制测试
v、对控件datagridview进行深复制测试
我们通过实现方法datamanhelper.deepcopyobject来进行测试
测试深复制方法1
使用二进制流的序列化与反序列化深度复制对象
public static object deepcopyobject(object obj) { binaryformatter formatter = new binaryformatter(null, new streamingcontext(streamingcontextstates.clone)); memorystream stream = new memorystream(); formatter.serialize(stream, obj); stream.position = 0; object clonedobj = formatter.deserialize(stream); stream.close(); return clonedobj; }
五个场景的测试结果为:
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(runtimetype type)
在 system.runtime.serialization.formatterservices.getserializablemembers(type type, streamingcontext context)
在 system.runtime.serialization.formatters.binary.writeobjectinfo.initmemberinfo()
在 system.runtime.serialization.formatters.binary.writeobjectinfo.initserialize(object obj, isurrogateselector surrogateselector, streamingcontext context, serobjectinfoinit serobjectinfoinit, iformatterconverter converter, objectwriter objectwriter, serializationbinder binder)
在 system.runtime.serialization.formatters.binary.writeobjectinfo.serialize(object obj, isurrogateselector surrogateselector, streamingcontext context, serobjectinfoinit serobjectinfoinit, iformatterconverter converter, objectwriter objectwriter, 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”
erializationbinder binder)
在 system.runtime.serialization.formatters.binary.objectwriter.serialize(object graph, header[] inheaders, __binarywriter serwriter, boolean fcheck)
在 system.runtime.serialization.formatters.binary.binaryformatter.serialize(stream serializationstream, object graph, header[] headers, boolean fcheck)
在 system.runtime.serialization.formatters.binary.binaryformatter.serialize(stream serializationstream, object graph)
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 24
在 datacopytest.formmain.formmain_load(object sender, eventargs e) 位置 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(runtimetype type)
在 system.runtime.serialization.formatterservices.getserializablemembers(type type, streamingcontext context)
在 system.runtime.serialization.formatters.binary.writeobjectinfo.initmemberinfo()
在 system.runtime.serialization.formatters.binary.writeobjectinfo.initserialize(object obj, isurrogateselector surrogateselector, streamingcontext context, serobjectinfoinit serobjectinfoinit, iformatterconverter converter, objectwriter objectwriter, serializationbinder binder)
在 system.runtime.serialization.formatters.binary.writeobjectinfo.serialize(object obj, isurrogateselector surrogateselector, streamingcontext context, serobjectinfoinit serobjectinfoinit, iformatterconverter converter, objectwriter objectwriter, serializationbinder binder)
在 system.runtime.serialization.formatters.binary.objectwriter.serialize(object graph, header[] inheaders, __binarywriter serwriter, boolean fcheck)
在 system.runtime.serialization.formatters.binary.binaryformatter.serialize(stream serializationstream, object graph, header[] headers, boolean fcheck)
在 system.runtime.serialization.formatters.binary.binaryformatter.serialize(stream serializationstream, object graph)
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 24
在 datacopytest.formmain.formmain_load(object sender, eventargs e) 位置 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(runtimetype type)
在 system.runtime.serialization.formatterservices.getserializablemembers(type type, streamingcontext context)
在 system.runtime.serialization.formatters.binary.writeobjectinfo.initmemberinfo()
在 system.runtime.serialization.formatters.binary.writeobjectinfo.initserialize(object obj, isurrogateselector surrogateselector, streamingcontext context, serobjectinfoinit serobjectinfoinit, iformatterconverter converter, objectwriter objectwriter, serializationbinder binder)
在 system.runtime.serialization.formatters.binary.writeobjectinfo.serialize(object obj, isurrogateselector surrogateselector, streamingcontext context, serobjectinfoinit serobjectinfoinit, iformatterconverter converter, objectwriter objectwriter, serializationbinder binder)
在 system.runtime.serialization.formatters.binary.objectwriter.serialize(object graph, header[] inheaders, __binarywriter serwriter, boolean fcheck)
在 system.runtime.serialization.formatters.binary.binaryformatter.serialize(stream serializationstream, object graph, header[] headers, boolean fcheck)
在 system.runtime.serialization.formatters.binary.binaryformatter.serialize(stream serializationstream, object graph)
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 24
在 datacopytest.formmain.formmain_load(object sender, eventargs e) 位置 d:\myprograms\datacopytest\datacopytest\formmain.cs:行号 141
结论:利用序列化与反序列化到二进制流的方法深复制对象,只有在该对象支持serializable特性时才可使用
测试深复制方法2
public static object deepcopyobject(object obj) { type t = obj.gettype(); propertyinfo[] properties = t.getproperties(); object p = t.invokemember("", system.reflection.bindingflags.createinstance, null, obj, null); foreach (propertyinfo pi in properties) { if (pi.canwrite) { object value = pi.getvalue(obj, null); pi.setvalue(p, value, null); } } return p; }
五个场景的测试结果为:
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(int32 value)
--- 内部异常堆栈跟踪的结尾 ---
在 system.runtimemethodhandle.invokemethod(object target, object[] arguments, signature sig, boolean constructor)
在 system.reflection.runtimemethodinfo.unsafeinvokeinternal(object obj, object[] parameters, object[] arguments)
在 system.reflection.runtimemethodinfo.invoke(object obj, bindingflags invokeattr, binder binder, object[] parameters, cultureinfo culture)
在 system.reflection.runtimepropertyinfo.setvalue(object obj, object value, bindingflags invokeattr, binder binder, object[] index, cultureinfo culture)
在 system.reflection.runtimepropertyinfo.setvalue(object obj, object value, object[] index)
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 29
在 datacopytest.formmain.formmain_load(object sender, eventargs e) 位置 d:\myprograms\datacopytest\datacopytest\formmain.cs:行号 141
结论:使用这种方法进行所谓深复制,完全是自寻死路!
测试深复制方法3
public static object deepcopyobject(object obj) { if (obj != null) { object result = activator.createinstance(obj.gettype()); foreach (fieldinfo field in obj.gettype().getfields()) { if (field.fieldtype.getinterface("ilist", false) == null) { field.setvalue(result, field.getvalue(obj)); } else { ilist listobject = (ilist)field.getvalue(result); if (listobject != null) { foreach (object item in ((ilist)field.getvalue(obj))) { listobject.add(deepcopyobject(item)); } } } } return result; } else { return null; } }
五个场景的测试结果为:
i、可正常复制(√)
ii、可正常复制(√)
iii、未触发异常, 复制后datatable无行列
iv、未触发异常,text字段未赋值
v、未触发异常
结论:这个方法只适用于深复制具备简单结构的类(如类中只有基础字段、数组等),对于不支持序列化的对象也可以进行深复制。
测试深复制方法4
这段代码来源同方法3
public static object deepcopyobject(object obj) { if (obj == null) return null; type type = obj.gettype(); if (type.isvaluetype || type == typeof(string)) { return obj; } else 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(deepcopyobject(array.getvalue(i)), i); } return convert.changetype(copied, obj.gettype()); } else if (type.isclass) { object toret = activator.createinstance(obj.gettype()); fieldinfo[] fields = type.getfields(bindingflags.public | bindingflags.nonpublic | bindingflags.instance); foreach (fieldinfo field in fields) { object fieldvalue = field.getvalue(obj); if (fieldvalue == null) continue; field.setvalue(toret, deepcopyobject(fieldvalue)); } return toret; } else throw new argumentexception("unknown type"); }
五个场景的测试结果为:
i、可正常复制(√)
ii、可正常复制(√)
iii、触发异常missingmethodexception
“system.missingmethodexception”类型的第一次机会异常在 mscorlib.dll 中发生
system.missingmethodexception: 没有为该对象定义无参数的构造函数。
在 system.runtimetypehandle.createinstance(runtimetype type, boolean publiconly, boolean nocheck, boolean& canbecached, runtimemethodhandleinternal& ctor, boolean& bneedsecuritycheck)
在 system.runtimetype.createinstanceslow(boolean publiconly, boolean skipcheckthis, boolean fillcache, stackcrawlmark& stackmark)
在 system.runtimetype.createinstancedefaultctor(boolean publiconly, boolean skipcheckthis, boolean fillcache, stackcrawlmark& stackmark)
在 system.activator.createinstance(type type, boolean nonpublic)
在 system.activator.createinstance(type type)
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 45
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 53
在 datacopytest.formmain.formmain_load(object sender, eventargs e) 位置 d:\myprograms\datacopytest\datacopytest\formmain.cs:行号 99
iv、未触发异常,但text字段也未赋值成功
v、触发异常missingmethodexception
“system.missingmethodexception”类型的第一次机会异常在 mscorlib.dll 中发生
system.missingmethodexception: 没有为该对象定义无参数的构造函数。
在 system.runtimetypehandle.createinstance(runtimetype type, boolean publiconly, boolean nocheck, boolean& canbecached, runtimemethodhandleinternal& ctor, boolean& bneedsecuritycheck)
在 system.runtimetype.createinstanceslow(boolean publiconly, boolean skipcheckthis, boolean fillcache, stackcrawlmark& stackmark)
在 system.runtimetype.createinstancedefaultctor(boolean publiconly, boolean skipcheckthis, boolean fillcache, stackcrawlmark& stackmark)
在 system.activator.createinstance(type type, boolean nonpublic)
在 system.activator.createinstance(type type)
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 45
在 datacopytest.datamanhelper.deepcopyobject(object obj) 位置 d:\myprograms\datacopytest\datacopytest\datamanhelper.cs:行号 53
在 datacopytest.formmain.formmain_load(object sender, eventargs e) 位置 d:\myprograms\datacopytest\datacopytest\formmain.cs:行号 141
结论:这个方法的作用类似方法3,只能深复制基本数据类型组成的类
具体问题具体分析
从上面的例子可以看出,想找一个放之四海而皆准的方式去深复制所有对象是很困难的。一些使用高级语言特性(反射)的深复制方法,即使可以在部分类上试用成功,也无法对所有的类都具备十足的把握。因此我认为应该采取下面的方式处理对象的深复制问题:
1、对于由基本数据类型组成的类,为之打上serializable标签,直接使用序列化与反序列化的方法进行深复制
2、其他较为复杂的类型如datagridview,可根据自身情况写一个方法进行深复制,之所以在这里说要根据自身情况写方法,是因为在对很多类进行复制时,你只需要复制对你有用的属性就行了。如textbox控件中,只有text一个属性对你是有用的,如果你需要在复制后的对象中用到readonly等属性的值,那么在你自己实现的复制方法中,也加上对这些属性的赋值即可。这样做还有一个好处,就是方便进行一些定制化的开发。
如下面这段代码,就是对datagridview的一个近似的深复制,这段代码将一个datagridview(dgv)的内容复制到另一个datagridview(dgvtmp)中,然后将dgvtmp传递给相关函数用于将datagridview中的内容输出到excel文档:
datagridview dgvtmp = new datagridview(); dgvtmp.allowusertoaddrows = false; //不允许用户生成行,否则导出后会多出最后一行 for (int i = 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 (int i = 0; i < dgv.rows.count; i++) { object[] objlist = new object[dgv.rows[i].cells.count]; for (int j = 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”字符串是不能做求和运算的)。
推荐阅读