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

news/2024/12/26 0:56:45/文章来源:https://www.cnblogs.com/hugogoos/p/18424476

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

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

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

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

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));
}

第二类、手动方式

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

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接口

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

第三类、序列化方式

这类方法核心思想就是先序列化再反序列化,这里面也可以分为三小类:二进制类、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);
}

第四类、第三方库方式

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

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);
}

第五类、扩展视野方式

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

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);}
}

基准测试

最后我们对后面三类所有方法进行一次基准测试对比,每个方法分别执行三组测试,三组分别测试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.hqwc.cn/news/801196.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智能写作新体验:AI写作小助手助力内容创作

在信息时代的浪潮中,内容创作已成为连接世界、传递价值的重要桥梁。然而,传统的写作方式在效率和质量上往往难以满足现代社会的需求。此时,AI写作小助手的诞生,为内容创作带来了全新的体验。本文将深入探讨AI写作小助手如何助力内容创作,开启智能写作的新篇章。AI写作小助…

基于Vue实现动态组织结构图

最近一个项目里有个前端绘制家谱图的需求,大概是下面这个样子:组件源码如下<template><table v-if="treeData.name"><tr><td :colspan="Array.isArray(treeData.children) ? treeData.children.length * 2 : 1":class="{pare…

中国能源发展报告2022

中国能源发展与未来中国能源发展报告2022林伯强高耗能产业的出路CCUS(Carbon Capture,Utilization and Storage,即碳捕获、利用与封存技术)高耗能产业布局:08 年,东高西低 >> 08 年之后,西高东低,自南向北移动,东减西增; 转移趋势北部沿海城市-河北,山东,201…

Qt表格入门

这篇博客详细介绍了Qt表格的基础知识,包括如何使用QTableWidget和QTableView来显示数据,以及如何使用QStyledItemDelegate和QSortFilterProxyModel进行数据代理、过滤和排序。此外,博客还提供了完整的代码示例,用于演示如何在Qt中创建和定制表格视图。这些内容对于Qt初学者…

王悦帆的第一次作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/zjlg/rjjc这个作业的目标 熟悉如何运用博客,展示自己姓名-学号 王悦帆 2022329301024一、自我介绍 (一)基本情况 大家好,我叫王悦帆,来自河南长垣,是自动化一班的成员,兴趣爱好是踢足球,看足球比赛。曾经去过现场…

day5[LangGPT结构化提示词编写实践]

任务要求:利用LangGPT优化提示词,使LLM输出正确结果。

Rebound-hackthebox

端口扫描smb探测 crackmapexec smb 10.10.11.231 -u anonymous -p "" --sharesRID 枚举 使用 CME 工具对指定主机的 SMB 服务进行扫描,并尝试使用 RID 枚举技术获取主机上的用户和组信息。RID 枚举(Relative Identifier enumeration)是一种用于获取 Windows 主机上…

CSP-J 2024 入门组初赛第一轮初赛试题及答案解析

CSP-J 2024 入门组初赛第一轮初赛试题及答案解析 一、 单项选择题(共15题,每题2分,共计30分:每题有且仅有一个正确选项) 1 32 位 int 类型的存储范围是( ) A -2147483647 ~ +2147483647 B -2147483647 ~ +2147483648 C -2147483648 ~ +2147483647 D -2147483648 ~ +…

数字产品护照 (DPP) 解决方案:利用 Blazor 和区块链实现产品全生命周期追踪

数字产品护照 (DPP) 解决方案:利用 Blazor 和区块链实现产品全生命周期追踪 随着全球对可持续发展和产品透明度的关注日益增加,企业需要一种可靠的方法来跟踪和管理产品生命周期中的关键数据。我们的数字产品护照(Digital Product Passport,DPP)系统正是为此而生,提供了一…

四种常用的IO模型

不管是做C端还是做B端,都要接触网络。文件操作,rpc,网上冲浪等,都与网络相关。网络又离不开IO。用的最多的IO操作就是读取和写入了。在Linux系统中,用read系统调用来发起读取操作,用write系统调用来发起写入操作。虽然在开发中,很少接触到底层的原理。但是学习后可以让我…

java学习9.21

今天回炉mybatis的用法,由于之前只是跟着教程走能成功配置数据库,但是一旦出现细小的区别就会产生自己改不了的bug,因此熟悉mybaits和其他技术的内容。知道问题出在哪里,以及怎么改。 mybatis配置 1.导入操作 (1)官网下载jar包并导入 (2)maven直接导入依赖 2.导入完之后创建…

龙祖维的第一次作业

这个作业属于哪个课程 https://edu.cnblogs.com/campus/zjlg/rjjc这个作业的目标 熟悉博客、向老师介绍自己姓名-学号 龙祖维 2022329301084一、自我介绍 基本信息 我叫龙祖维,来自湖南郴州,是22电气工程及其自动化2班的一名学生 兴趣爱好 我喜欢运动,篮球、足球、乒乓及羽毛…