欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

.NET的深复制方法(以C#语言为例)

程序员文章站 2022-03-26 08:42:02
很多时候我们复制一个对象实例a到实例b,在用实例b去做其他事情的时候,会对实例b进行修改,为保证对b的修改不会影响到a的正常使用,就需要使用到深复制。 我在网上搜到一些深...

很多时候我们复制一个对象实例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”字符串是不能做求和运算的)。