【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包时微软自己的。 

这个深拷贝的道理是一样的,即先序列化成字符串,然后再反序列化成对象,来实现深拷贝

posted @ 2022-04-23 16:55  宋桓公  阅读(38)  评论(0编辑  收藏  举报