【C#】 彻底搞明白深拷贝
每一种语言都有深拷贝这个概念,其实本质上都是一样的,这次从C#的角度解释。
C# 有值类型和引用类型。值类型,如int之类的不存在浅和深的问题,直接赋值就完了。
引用类型,比如我们自定义的类。通过new的方式创建,new返回的其实是个“指针”,
或者说是引用,他自己在栈上,但是new分配的空间在堆上。 如果直接对引用赋值的话,
相当于是改变“指针”的指向。之前被他管理的堆内存,就被悬空了,最后没人指向它,就会被垃圾回收机制给回收了。
比如,我自定义一个类。
public class Box
{
public double height; // 高度
public double width; // 宽度
}
然后new两个对象,并将b1直接赋值给b2
Box b1 = new Box();
Box b2 = new Box();
b2 = b1;
b2.width = 5; //改变b2 影响b1
Console.WriteLine(b1.width);
此时b2和b1指向了同一块内存,b2之前分配的内存被悬空了,最后被GC回收。
那如果不想这样,我想拷贝一个独立的b2(b2的改变不影响b1),那么唯一的办法就是逐个拷贝。
public static Box FixDeepCopy(double height, double width)
{
Box b = new Box();
b.height = height;
b.width = width;
return b;
}
这里我们注意到,我们没有改变b的指向,所以b指向的还是最开始自己new出的内存。由于逐一的赋值,所以此时算是深拷贝成功了。
但是这样过于麻烦,不同类,要写不同的深拷贝函数,而且随着类越复杂,方法越繁琐。
那么下面三种方法,帮你解决,仔细观察,其实这三种方法,其实也是将成员变量逐一拷贝的,
只是做了泛化处理。
不多说了,直接上代码了:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
namespace DeepCopyTest
{
internal class Program
{
[Serializable]
public class Box
{
public double height; // 高度
public double width; // 宽度
}
public static Box FixDeepCopy(double height, double width)
{
Box b = new Box();
b.height = height;
b.width = width;
return b;
}
public static T DeepCopyByBin<T>(T obj)
{
object retval;
using (MemoryStream ms = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
//序列化成流
bf.Serialize(ms, obj);
//将当前流中的位置设置为指定值。也就是从SeekOrigin.Begin偏移0个位置。
ms.Seek(0, SeekOrigin.Begin);
//反序列化成对象
retval = bf.Deserialize(ms);
ms.Close();
}
return (T)retval;
}
/// <summary>
/// 注意需要给类加个[Serializable]
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="obj"></param>
/// <returns></returns>
public static T DeepCopyByReflect<T>(T obj)
{
//如果是字符串或值类型则直接返回
if (obj is string || obj.GetType().IsValueType) return obj;
object retval = Activator.CreateInstance(obj.GetType());
FieldInfo[] fields = obj.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static);
foreach (FieldInfo field in fields)
{
try { field.SetValue(retval, DeepCopyByReflect(field.GetValue(obj))); }
catch { }
}
return (T)retval;
}
static void Main(string[] args)
{
Box b1 = new Box();
Box b2 = new Box();
Box b3 = new Box();
Box b4 = new Box();
Console.WriteLine(b1.width);
//直接赋值的方式
b2 = b1;
b2.width = 5; //改变b2 影响b1
Console.WriteLine(b1.width);
Console.WriteLine("--------------方式1----------------------");
//深拷贝
b2 = DeepCopyByBin(b1);
b2.width = 6; //改变b2 不影响b1
Console.WriteLine(b1.width);
Console.WriteLine("---------------方式2---------------------");
//深拷贝
b3 = DeepCopyByReflect(b1);
b3.width = 7; //改变b5 不影响b1
Console.WriteLine(b1.width);
Console.WriteLine("----------------方式3--------------------");
//深拷贝(需要安装包:Newtonsoft.Json)
b4 = JsonConvert.DeserializeObject<Box>(JsonConvert.SerializeObject(b1)); //深克隆
b4.width = 789; //改变b4 不影响b1
Console.WriteLine(b1.width);
Console.ReadKey();
}
}
}
小结:
1 感觉用 JsonConvert最方便,需要自己安装一个包Newtonsoft.Json(这个包太常用了)
JsonConvert.DeserializeObject<Box>(JsonConvert.SerializeObject(b1));
先将对象其序列化成字符串,在将字符串反序列化成对象。发现没有, 这不就是
我们最开始讲的,对类的成员变量逐一赋值的泛化过程吗?其他两种方式也是一样。
2 使用 BinaryFormatter的方式,注意需要给类加个可序列化的特性[Serializable],不然运行报错。
它比较直接,直接拷贝整个内存。
3 最后反射这种方式,要注意一下,因为可以看到,它用到了递归的方式,去检索每一个对象。
如果对象过于复杂,可能有点问题。
2022年11月30日----------------------新增
var new_obj = System.Text.Json.JsonSerializer.Deserialize<ToolsNodeItem>(JsonSerializer.Serialize(obj));
这个和方式3时一样的,不过这个json包时微软自己的。
这个深拷贝的道理是一样的,即先序列化成字符串,然后再反序列化成对象,来实现深拷贝。
作者:宋桓公
出处:http://www.cnblogs.com/douzi2/
如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”将是我最大的写作动力!欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利。