C#|.net core 基础 - 深拷贝的五大类N种实现方式

news/2024/9/25 18:08:09/

在实际应用中经常会有这样的需求:获取一个与原对象数据相同但是独立于原对象的精准副本,简单来说就是克隆一份,拷贝一份,复制一份和原对象一样的对象,但是两者各种修改不能互相影响。这一行为也叫深克隆,深拷贝。

在这里插入图片描述

在C#里拷贝对象是一个看似简单实则相当复杂的事情,因此我不建议自己去做封装方法然后项目上使用的,这里面坑太多,容易出问题。下面给大家分享五大类N种深拷贝方法。

01第一类、针对简单引用类型方式

这类方法只对简单引用类型有效,如果类型中包含引用类型的属性字段,则无效。

1、MemberwiseClone方法

MemberwiseClone是创建当前对象的一个浅拷贝。本质上来说它不是适合做深拷贝,但是如果对于一些简单引用类型即类型里面不包含引用类型属性字段,则可以使用此方法进行深拷贝。因为此方法是Obejct类型的受保护方法,因此只能在类的内部使用。

示例代码如下:

public class MemberwiseCloneModel
{public int Age { get; set; }public string Name { get; set; }public MemberwiseCloneModel Clone(){return (MemberwiseCloneModel)this.MemberwiseClone();}
}
public static void NativeMemberwiseClone()
{var original = new MemberwiseCloneModel();var clone = original.Clone();Console.WriteLine(original == clone);Console.WriteLine(ReferenceEquals(original, clone));
}

2、with表达式

可能大多数人刚看到with表达式还一头雾水,这个和深拷贝有什么关系呢?它和record有关,record是在C# 9引入的当时还只能通过record struct声明值类型记录,在C# 10版本引入了record class可以声明引用类型记录。可能还是有不少人对record不是很了解,简单来说就是用于定义不可变的数据对象,是一个特殊的类型。

with可以应用于记录实例右侧来创建一个新的记录实例,此方式和MemberwiseClone有同样的问题,如果对象里面包含引用类型属性成员则只复制其属性。因此只能对简单的引用类型进行深拷贝。示例代码如下:

public record class RecordWithModel
{public int Age { get; set; }public string Name { get; set; }
}
public static void NativeRecordWith()
{var original = new RecordWithModel();var clone = original with { };Console.WriteLine(original == clone);Console.WriteLine(ReferenceEquals(original, clone));
}

02第二类、手动方式

这类方法都是需要手动处理的,简单又复杂。

1、纯手工

纯手工就是属性字段一个一个赋值,说实话我最喜欢这种方式,整个过程完全可控,排查问题十分方便一目了然,当然如果遇到复杂的多次嵌套类型也是很头疼的。看下代码感受一下。

public class CloneModel
{public int Age { get; set; }public string Name { get; set; }public List<CloneModel> Models { get; set; }
}
public static void ManualPure()
{var original = new CloneModel{Models = new List<CloneModel>{new() {Age= 1,Name="1"}}};var clone = new CloneModel{Age = original.Age,Name = original.Name,Models = original.Models.Select(x => new CloneModel{Age = x.Age,Name = x.Name,}).ToList()};Console.WriteLine(original == clone);Console.WriteLine(ReferenceEquals(original, clone));
}

2、ICloneable接口

首先这是内置接口,也仅仅是定义了接口,具体实现还是需要靠自己实现,所以理论上和纯手工一样的,可以唯一的好处就是有一个统一定义,具体实现看完这篇文章都可以用来实现这个接口,这里就不在赘述了。

03第三类、序列化方式

这类方法核心思想就是先序列化再反序列化,这里面也可以分为三小类:二进制类、Xml类、Json类。

1、二进制序列化器

1.1.BinaryFormatter(已启用)

从.NET5开始此方法已经标为弃用,大家可以忽略这个方案了,在这里给大家提个醒,对于老的项目可以参考下面代码。

public static T SerializeByBinary<T>(T original){using (var memoryStream = new MemoryStream()){var formatter = new BinaryFormatter();formatter.Serialize(memoryStream, original);memoryStream.Seek(0, SeekOrigin.Begin);return (T)formatter.Deserialize(memoryStream);}}

1.2.MessagePackSerializer

需要安装MessagePack包。实现如下:

public static T SerializeByMessagePack<T>(T original)
{var bytes = MessagePackSerializer.Serialize(original);return MessagePackSerializer.Deserialize<T>(bytes);
}

2、Xml序列化器

2.1. DataContractSerializer

对象和成员需要使用[DataContract] 和 [DataMember] 属性定义,示例代码如下:

[DataContract]
public class DataContractModel
{[DataMember]public int Age { get; set; }[DataMember]public string Name { get; set; }[DataMember]public List<DataContractModel> Models { get; set; }
}
public static T SerializeByDataContract<T>(T original)
{using var stream = new MemoryStream();var serializer = new DataContractSerializer(typeof(T));serializer.WriteObject(stream, original);stream.Position = 0;return (T)serializer.ReadObject(stream);
}

2.2. XmlSerializer

public static T SerializeByXml<T>(T original)
{using (var ms = new MemoryStream()){XmlSerializer s = new XmlSerializer(typeof(T));s.Serialize(ms, original);ms.Position = 0;return (T)s.Deserialize(ms);}
}

3、Json序列化器

目前有两个有名的Json序列化器:微软自家的System.Text.Json和Newtonsoft.Json(需安装库)。

public static T SerializeByTextJson<T>(T original)
{var json = System.Text.Json.JsonSerializer.Serialize(original);return System.Text.Json.JsonSerializer.Deserialize<T>(json);
}
public static T SerializeByJsonNet<T>(T original)
{var json = Newtonsoft.Json.JsonConvert.SerializeObject(original);return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(json);
}

04第四类、第三方库方式

这类方法使用简单,方案成熟,比较适合项目上使用。

1、AutoMapper

安装AutoMapper库

public static T ThirdPartyByAutomapper<T>(T original)
{var config = new MapperConfiguration(cfg =>{cfg.CreateMap<T, T>();});var mapper = config.CreateMapper();T clone = mapper.Map<T, T>(original);return clone;
}

2、DeepCloner

安装DeepCloner库

public static T ThirdPartyByDeepCloner<T>(T original)
{return original.DeepClone();
}

3、FastDeepCloner

安装FastDeepCloner库

public static T ThirdPartyByFastDeepCloner<T>(T original)
{return (T)DeepCloner.Clone(original);
}

05第五类、扩展视野方式

这类方法都是半成品方法,仅供参考,提供思路,扩展视野,不适合项目使用,当然你可以把它们完善,各种特殊情况问题都处理好也是可以在项目上使用的。

1、反射

比如下面没有处理字典、元组等类型,还有一些其他特殊情况。

public static T Reflection<T>(T original)
{var type = original.GetType();//如果是值类型、字符串或枚举,直接返回if (type.IsValueType || type.IsEnum || original is string){return original;}//处理集合类型if (typeof(IEnumerable).IsAssignableFrom(type)){var listType = typeof(List<>).MakeGenericType(type.GetGenericArguments()[0]);var listClone = (IList)Activator.CreateInstance(listType);foreach (var item in (IEnumerable)original){listClone.Add(Reflection(item));}return (T)listClone;}//创建新对象var clone = Activator.CreateInstance(type);//处理字段foreach (var field in type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)){var fieldValue = field.GetValue(original);if (fieldValue != null){field.SetValue(clone, Reflection(fieldValue));}}//处理属性foreach (var property in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)){if (property.CanRead && property.CanWrite){var propertyValue = property.GetValue(original);if (propertyValue != null){property.SetValue(clone, Reflection(propertyValue));}}}return (T)clone;
}

2、Emit

Emit的本质是用C#来编写IL代码,这些代码都是比较晦涩难懂,后面找机会单独讲解。另外这里加入了缓存机制,以提高效率。

public class DeepCopyILEmit<T>
{private static Dictionary<Type, Func<T, T>> _cacheILEmit = new();public static T ILEmit(T original){var type = typeof(T);if (!_cacheILEmit.TryGetValue(type, out var func)){var dymMethod = new DynamicMethod($"{type.Name}DoClone", type, new Type[] { type }, true);var cInfo = type.GetConstructor(new Type[] { });var generator = dymMethod.GetILGenerator();var lbf = generator.DeclareLocal(type);generator.Emit(OpCodes.Newobj, cInfo);generator.Emit(OpCodes.Stloc_0);foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)){generator.Emit(OpCodes.Ldloc_0);generator.Emit(OpCodes.Ldarg_0);generator.Emit(OpCodes.Ldfld, field);generator.Emit(OpCodes.Stfld, field);}generator.Emit(OpCodes.Ldloc_0);generator.Emit(OpCodes.Ret);func = (Func<T, T>)dymMethod.CreateDelegate(typeof(Func<T, T>));_cacheILEmit.Add(type, func);}return func(original);}
}

3、表达式树

表达式树是一种数据结构,在运行时会被编译成IL代码,同样的这些代码也是比较晦涩难懂,后面找机会单独讲解。另外这里也加入了缓存机制,以提高效率。

public class DeepCopyExpressionTree<T>
{private static readonly Dictionary<Type, Func<T, T>> _cacheExpressionTree = new();public static T ExpressionTree(T original){var type = typeof(T);if (!_cacheExpressionTree.TryGetValue(type, out var func)){var originalParam = Expression.Parameter(type, "original");var clone = Expression.Variable(type, "clone");var expressions = new List<Expression>();expressions.Add(Expression.Assign(clone, Expression.New(type)));foreach (var prop in type.GetProperties()){var originalProp = Expression.Property(originalParam, prop);var cloneProp = Expression.Property(clone, prop);expressions.Add(Expression.Assign(cloneProp, originalProp));}expressions.Add(clone);var lambda = Expression.Lambda<Func<T, T>>(Expression.Block(new[] { clone }, expressions), originalParam);func = lambda.Compile();_cacheExpressionTree.Add(type, func);}return func(original);}
}

06基准测试

最后我们对后面三类所有方法进行一次基准测试对比,每个方法分别执行三组测试,三组分别测试100、1000、10000个对象。测试模型为:

[DataContract]
[Serializable]
public class DataContractModel
{[DataMember]public int Age { get; set; }[DataMember]public string Name { get; set; }[DataMember]public List<DataContractModel> Models { get; set; }
}

其中Models包含两个元素。最后测试结果如下:

通过结果可以发现:[表达式树]和[Emit] > [AutoMapper]和[DeepCloner] > [MessagePack] > 其他

第一梯队:性能最好的是[表达式树]和[Emit],两者相差无几,根本原因因为最终都是IL代码,减少了各种反射导致的性能损失。因此如果你有极致的性能需求,可以基于这两种方案进行改进以满足自己的需求。

第二梯队:第三方库[AutoMapper]和[DeepCloner] 性能紧随其后,相对来说也不错,而且是成熟的库,因此如果项目上使用可以优先考虑。

第三梯队:[MessagePack]性能比第二梯队差了一倍,当然这个也需要安装第三方库。

第四梯队:[System.Text.Json]如果不想额外安装库,有没有很高的性能要求可以考虑使用微软自身的Json序列化工具。

其他方法就可以忽略不看了。

:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。https://gitee.com/hugogoos/Planner


http://www.ppmy.cn/news/1530397.html

相关文章

把命令的语气改成聊天的方式

​在与人沟通时&#xff0c;没人会喜欢别人用下命令的口气对自己说话。 即使是非常明显的错误&#xff0c;一旦用粗鲁的口气命令指责别人&#xff0c;也会引起对方的反感&#xff0c;改正起来也会非常困难。 即使作为领导&#xff0c;也不要用命令的语气面对下级要做到合情合…

Python提供内置正则表达式库

正则表达式是一种强大的文本处理工具&#xff0c;可以匹配文本片段的模式 最简单的正则表达式就是普通的字符串&#xff0c;可以匹配自身 要注意的是&#xff0c;正则表达式并不是一个程序&#xff0c;它使用一种特定的语法模式来描述在搜索文本时要匹配的一个或多个字符串。正…

2024最新!!!iOS高级面试题,全!(一)

TCP,HTTP,HTTPS&#xff0c;,WebSokect 区别&#xff1a; IP协议&#xff08;网络层协议&#xff09; TCP&#xff1a;传输控制协议&#xff0c;主要解决数据如何在网络中传输&#xff0c;面向连接&#xff0c;可靠。&#xff08;传输层协议&#xff09; UDP&#xff1a;用户数…

需求导向的正则表达式

目录 re.sub 需求&#xff1a;把 1. 2.这些序号转成&#xff08;1&#xff09; &#xff08;2&#xff09; 需求&#xff1a;反过来&#xff0c;把(1)->1. ,&#xff08;2&#xff09;》2. 。 需求&#xff1a;把出现的 1 2 3都转成下标 进阶1&#xff01;只想让化学符…

华为HarmonyOS地图服务 4 - 通过“地图相机“控制地图的可见区域

场景介绍 华为地图的移动是通过模拟相机移动的方式实现的&#xff0c;您可以通过改变相机位置&#xff0c;来控制地图的可见区域&#xff0c;效果如图所示。 本章节将向您介绍相机的各个属性与含义&#xff0c;并移动相机。 相机移动前 …

CentOS中使用Docker运行Nginx并挂载本地目录

CentOS 中安装 Docker 并挂载本地目录&#xff1a; 一、安装 Docker 更新系统软件包&#xff1a; sudo yum update -y安装必要的软件包以允许使用 yum 安装 Docker&#xff1a; sudo yum install -y yum-utils device-mapper-persistent-data lvm2添加 Docker 仓库&#xff1a…

哪个编程工具让你的工作效率翻倍?

在探讨哪款编程工具能让工作效率翻倍时&#xff0c;我们不得不承认&#xff0c;这并非是一个非黑即白的答案&#xff0c;因为不同的开发者、不同的项目阶段以及不同的编程需求&#xff0c;都可能使得某种工具成为提升效率的关键。然而&#xff0c;如果要从智能的代码编辑器、强…

8606 二叉树的构建及遍历操作

### 伪代码 1. **CreateBiTree**: - 读取一个字符 ch。 - 如果 ch 是 #&#xff0c;则当前节点为空。 - 否则&#xff0c;创建一个新节点 T&#xff0c;将 ch 赋值给 T 的数据域。 - 递归创建 T 的左子树。 - 递归创建 T 的右子树。 2. **PreOrderTraverse**: …