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

在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. [oeasy]python0007-调试程序_debug

    ​ 调试程序 回忆上次内容 ​py​​ 的程序是按照顺序执行的 是一行行挨排解释执行的 程序并不是数量越多越好 kpi也在不断演化 ​ 编辑 写的代码越多 出现的bug就越多 什么是bug 如何找bu ...

  2. Linux 基于flock命令实现多进程并发读写文件控制

    基于flock命令实现多进程并发读写文件控制 需求描述 实际项目中,需要在Linux下通过shell脚本并发读写同一个文件,但是希望同一时刻,只有一个进程可以在读.写目标文件. 解决方案 使用floc ...

  3. c#写一个WINFORM的多线程操作

    以下是一个简单的示例,展示了如何在C# WinForms中创建一个按钮的异步事件,并使用Label控件来显示事件执行的时长. 首先,确保你已经在你的项目中添加了一个Button和一个Label控件.假 ...

  4. M1安装Anaconda遇到的问题

    1. 安装时报错:"Anaconda3 is already installed in /opt/anaconda3. Use 'conda update anaconda3' to upd ...

  5. ssh 转发 和 切换图形化

    适用环境 宿主机连接到一台服务器是,服务器系统里面的浏览器点击http网页卡顿,那么这时可以通过ssh将端口转发到宿主机 使用宿主机的浏览器点击,则不会很卡顿. [root@foundation1 ~ ...

  6. Ubuntu16.04设置静态IP或动态ip(DHCP)

    Ubuntu16.04设置静态IP或动态ip(DHCP) 设置静态IP 1,vim编辑/etc/network/interfaces 网络配置文件 sudo vim /etc/network/inte ...

  7. RPA京麦咚咚批量发送消息

    京麦咚咚发送消息,首先我们需要确定给谁发,发送什么内容 给谁发:可以传入京东用户名.京东订单号.咚咚将通过此条件进行搜索进入会话框 发送什么内容:批量发送信息给不同的用户,比如给不同的订单用户发送其相 ...

  8. 一键导入抓包数据生成HTTP请求

    Jmeter一键导入抓包数据生成HTTP请求.路径:工具->Import from cURL 在弹框里粘贴cURL,点击"Create Test Plan"会自动生成HTTP ...

  9. 如何通过PowerShell批量修改O365用户的office phone属性值

    我的博客园:https://www.cnblogs.com/CQman/ 如何通过PowerShell批量修改O365用户的office phone属性值? 需求信息: 组织中的O365用户在创建时, ...

  10. 【Vue】二维码生成

    按组长提供的样例,功能比较相符合的是这个博客: https://www.jianshu.com/p/8d59107e1992 这个博客引用的是这篇文章: https://blog.csdn.net/w ...