.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","z" }; TestClassWithS test2 = new TestClassWithS(); test2.a = 11; test2.b = "hello world2!"; test2.c = new string[] { "i","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 中发生 III、可正常复制 (√) IV、触发异常SerializationException,原因是该类不支持序列化 “System.Runtime.Serialization.SerializationException”类型的第一次机会异常在 mscorlib.dll 中发生 “System.Runtime.Serialization.SerializationException”类型的第一次机会异常在 mscorlib.dll 中发生 测试深复制方法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 中发生 测试深复制方法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 中发生 V、触发异常MissingMethodException “System.MissingMethodException”类型的第一次机会异常在 mscorlib.dll 中发生 具体问题具体分析 从上面的例子可以看出,想找一个放之四海而皆准的方式去深复制所有对象是很困难的。一些使用高级语言特性(反射)的深复制方法,即使可以在部分类上试用成功,也无法对所有的类都具备十足的把握。因此我认为应该采取下面的方式处理对象的深复制问题: 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”字符串是不能做求和运算的)。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |