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

在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. 回顾 JavaScript

    回顾 JavaScript 阅读前建议了解 ECMAScript 是什么? 不然你可能会疑惑下面内容 JavaScript 中掺杂的 ECMAScript 需要大体了解过 JavaScript 主要是 ...

  2. mysql进阶笔记

    说明:此文章并非原创,参考极客时间文章<MySQL实战45讲>做的一些笔记,方便自己查阅,有兴趣可以自行去极客时间阅读,内容非常给力. mysql引擎  Innodb: Page是Inno ...

  3. 【Mybatis】04 官方文档指北阅读 vol2 配置 其一

    https://mybatis.org/mybatis-3/zh/configuration.html 配置 MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息. 配置文 ...

  4. 【Spring-Security】Re03 认证参数修改与跨域跳转处理

    一.请求参数名设置 之前的表单信息有一些要求: 1.action属性发送的地址是Security设置的URL 2.发送的请求方式是POST 3.请求的账户信息,也就是表单发送的参数,必须对应的是use ...

  5. 【Redis】05 持久化

    持久化概述 Redis提供了不同的持久性选项: 1.RDB持久性按指定的时间间隔执行数据集的时间点快照. 2.AOF持久性会记录服务器接收的每个写入操作,这些操作将在服务器启动时再次播放,以重建原始数 ...

  6. AI未来应用的新领域:具有领域知识的专属智能拼音输入法 —— 医生专属的智能输入法

    本人上个月去辽宁中医看了些小毛病,在和医生交流的时候随便小聊一下,其中一个主要的话题就是"医生是否需要练习五笔".众所周知,医生的主要工作是看病,而需要使用输入法打字写病历只是看病 ...

  7. vscode 单步调试时设置——是否进入非本项目的代码

    如题,在vscode中默认设置在调试时不能进入非本项目的代码(类.函数.模块),这个设置基本是OK的,因为确实没有必要进入到非本项目的代码中,但是如果有特殊需要想改变设置使其能够进入到本项目外的代码时 ...

  8. Apache DolphinScheduler 在奇富科技的首个调度异地部署实践

    奇富科技(原360数科)是人工智能驱动的信贷科技服务平台,致力于凭借智能服务.AI研究及应用.安全科技,赋能金融机构提质增效,助推普惠金融高质量发展,让更多人享受到安全便捷的金融科技服务.作为国内领先 ...

  9. DMS:直接可微的网络搜索方法,最快仅需单卡10分钟 | ICML 2024

    Differentiable Model Scaling(DMS)以直接.完全可微的方式对宽度和深度进行建模,是一种高效且多功能的模型缩放方法.与先前的NAS方法相比具有三个优点:1)DMS在搜索方面 ...

  10. B2B进销存ERP后台管理系统的逻辑架构与设计,AxureRP原型产品经理实战案例

    模块分析: 进销存系统是一种用于企业管理库存.销售和采购活动的信息系统.它的主要作用包括但不限于以下几个方面: 1.库存管理 实时库存跟踪:准确记录每种商品的库存数量,确保数据的实时性和准确性. 库存 ...