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

在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

C#|.net core 基础 - 深拷贝的五大类N种实现方式的更多相关文章

  1. 前端基础----CSS语法、CSS四种引入方式、CSS选择器、CSS属性操作

    一.CSS语法 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明. 例如: h1 {color:red; font-size:14px;} 二.CSS四种引入方式 1,行内式 行内式是在标 ...

  2. mfc 类三种继承方式下的访问

    知识点 public private protected 三种继承方式 三种继承方式的区别 public 关键字意味着在其后声明的所有成员及对象都可以访问. private 关键字意味着除了该类型的创 ...

  3. 【Jmeter基础知识】Jmeter的三种参数化方式

    JMeter的三种参数化方式包括: 1.用户参数 2.函数助手 3.CSV Data Set Config 一.用户参数 位置:添加-前置处理器-用户参数 操作:可添加多个变量或者参数 二.函数助手 ...

  4. Python基础:Python运行的两种基本方式

    完成Python的安装之后,我们可以开始编写Python代码以及运行Python程序了.我们来看一下运行Python具体有哪几种方式 1.REPL 所谓REPL即read.eva.print.loop ...

  5. php 基础知识 post 和get 两种传输方式的区别

    1.post更安全(不会作为url的一部分,不会被缓存.保存在服务器日志.以及浏览器浏览记录中) 2.post发送的数据量更大(get有url长度限制) 3.post能发送更多的数据类型(get只能发 ...

  6. 技术流:6大类37种方式教你在国内推广App

    转自:http://www.gamelook.com.cn/2015/01/201906 如何有效的推广自己App,是每个发行商都要考虑的问题,当然每个产品都有适合自己的推广方式.本文就集结了包括应用 ...

  7. ASP.NET Core 基础知识(四) Startup.cs类

    ASP.NET Core应用程序需要一个启动类,按照约定命名为Startup.在 Program 类的主机生成器上调用 Build 时,将生成应用的主机, 通常通过在主机生成器上调用 WebHostB ...

  8. ASP.NET Core 基础知识(三) Program.cs类

    ASP.NET Framework应用程序是严重依赖于IIS的,System.Web 中有很多方法都是直接调用的 IIS API,并且它还是驻留在IIS进程中的.而 ASP.NET Core 的运行则 ...

  9. 【SF】开源的.NET CORE 基础管理系统 - 安装篇

    [SF]开源的.NET CORE 基础管理系统 -系列导航 1.开发必备工具 IDE:VS2017 运行环境:netcoreapp1.1 数据库:SQL Server 2012+ 2.获取最新源代码 ...

  10. 【SF】开源的.NET CORE 基础管理系统 -介绍篇

    [SF]开源的.NET CORE 基础管理系统 -系列导航 1.环境: .NET Core SDK (https://www.microsoft.com/net/core) SQL Server or ...

随机推荐

  1. Python按条件筛选、剔除表格数据并绘制剔除前后的直方图

      本文介绍基于Python语言,读取Excel表格文件数据,以其中某一列数据的值为标准,对于这一列数据处于指定范围的所有行,再用其他几列数据的数值,加以数据筛选与剔除:同时,对筛选前.后的数据分别绘 ...

  2. 前端说你的API接口太慢了,怎么办?

    当有千万条海量数据时,前端调取接口发现接口响应的太慢,前端这时让你优化一下接口,你说有几千万条数据,觉得自己尽力了,前端觉得你好菜,别急,读完这篇文章,让前端喊你一声:大佬,厉害!!! 常用的方法总结 ...

  3. 网络基础 登录对接CAS-跨域导致的一个意想不到的Bug

    登录对接CAS-跨域导致的一个意想不到的Bug 背景描述 业务需求是平台登录,接入Cas验证 问题描述 平台登录页,点击登录方式,跳转Cas登录页,提交登录请求,结果发现,又返回平台登录页: 再次点击 ...

  4. 跟着ChatGPT学习设计模式 - 工厂模式

    1. 前言 在工作过程中,越发觉得设计模式的重要性.经常会有人说工作5年的人,大学生随便培训1-2月也能做同样的工作,没错,大学生的确可以做. 但其写的代码,可维护性.可扩展性.添加新功能时方便还是简 ...

  5. XXL-JOB分片执行分布式任务

    XXL-JOB相对于springtask来说优点之一就是分布式执行任务,可以在调度中心为执行器分发任务,实现分布式. 分片广播任务即当一个微服务形成集群的时候,任务会完整的下发给每一个执行器.而不像其 ...

  6. 【爬虫】Java爬取KFC全国门店信息

    官网地址: http://www.kfc.com.cn/kfccda/storelist/index.aspx 基础库 <dependencies> <dependency> ...

  7. VScode调试C++工程

    相关: Linux环境下配置vscode的C/C++编译环境 本文主要参考: https://zhuanlan.zhihu.com/p/385276301 ====================== ...

  8. vue前端自适应布局,一步到位所有自适应

    vue前端自适应布局,一步到位所有自适应 页面展示 实现内容 1,左右布局 左侧固定宽带,右侧自适应剩余的宽度. 中间一条分割线,可以拖拉,自适应调整左右侧的宽度. 左侧的高度超长自动出现横向滚动条, ...

  9. [考试记录] 2024.8.10 csp-s 模拟赛18

    80 + 20 + 0 + 70 = 170 第三题应该有 10 分暴力的,但我没打. T1 星际旅行 题面翻译 总共有n个节点,m条路径,要求其中m-2条路径走两遍,剩下2条路径仅走一遍,问不同的路 ...

  10. 关于为什么使用 ASCII GBK Unicode编码

    关于为什么使用 ASCII GBK Unicode编码 由来:大家都知道计算机最早是美国人为了更加便捷的存储和计算数据发明的,但是呢计算机底层都是硬件,只能存储像0101这样的二进制数据,那美国人为了 ...