C#|.net core 基础 - 深拷贝的五大类N种实现方式
bigegpt 2024-09-22 00:40 4 浏览
在实际应用中经常会有这样的需求:获取一个与原对象数据相同但是独立于原对象的精准副本,简单来说就是克隆一份,拷贝一份,复制一份和原对象一样的对象,但是两者各种修改不能互相影响。这一行为也叫深克隆,深拷贝。
在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(#34;{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
相关推荐
- pyproject.toml到底是什么东西?(py trim)
-
最近,在Twitter上有一个Python项目的维护者,他的项目因为构建失败而出现了一些bug(这个特别的项目不提供wheel,只提供sdist)。最终,发现这个bug是由于这个项目使用了一个pypr...
- BDP服务平台SDK for Python3发布(bdp数据平台)
-
下载地址https://github.com/imysm/opends-sdk-python3.git说明最近在开发和bdp平台有关的项目,用到了bdp的python的sdk,但是官方是基于p...
- Python-for-Android (p4a):(python-for-android p4a windows)
-
一、Python-for-Android(p4a)简介Python-for-Android(p4a),一个强大的开发工具,能够将你的Python应用程序打包成可在Android设备上运行...
- Qt for Python—Qt Designer 概览
-
前言本系列第三篇文章(QtforPython学习笔记—应用程序初探)、第四篇文章(QtforPython学习笔记—应用程序再探)中均是使用纯代码方式来开发PySide6GUI应用程序...
- Python:判断质数(jmu-python-判断质数)
-
#Python:判断质数defisPrime(n):foriinrange(2,n):ifn%i==0:return0re...
- 为什么那么多人讨厌Python(为什么python这么难)
-
Python那么棒,为什么那么多人讨厌它呢?我整理了一下,主要有这些原因:用缩进替代大括号许多人抱怨Python完全依赖于缩进来创建代码块,代码多一点就很难看到函数在哪里结束,那么你就需要把一个函数拆...
- 一文了解 Python 中带有 else 的循环语句 for-else/while-else
-
在本文中,我们将向您介绍如何在python中使用带有else的for/while循环语句。可能许多人对循环和else一起使用感到困惑,因为在if-else选择结构中else正常...
- python的numpy向量化语句为什么会比for快?
-
我们先来看看,python之类语言的for循环,和其它语言相比,额外付出了什么。我们知道,python是解释执行的。举例来说,执行x=1234+5678,对编译型语言,是从内存读入两个shor...
- 开眼界!Python遍历文件可以这样做
-
来源:【公众号】Python技术Python对于文件夹或者文件的遍历一般有两种操作方法,一种是至二级利用其封装好的walk方法操作:import osfor root,d...
- 告别简单format()!Python Formatter类让你的代码更专业
-
Python中Formatter类是string模块中的一个重要类,它实现了Python字符串格式化的底层机制,允许开发者创建自定义的格式化行为。通过深入理解Formatter类的工作原理和使用方法,...
- python学习——038如何将for循环改写成列表推导式
-
在Python里,列表推导式是一种能够简洁生成列表的表达式,可用于替换普通的for循环。下面是列表推导式的基本语法和常见应用场景。基本语法result=[]foriteminite...
- 详谈for循环和while循环的区别(for循环语句与while循环语句有什么区别)
-
初九,潜龙勿用在刚开始使用python循环语句时,经常会遇到for循环和while循环的混用,不清楚该如何选择;今天就对这2个循环语句做深入的分析,让大家更好地了解这2个循环语句以方便后续学习的加深。...
- Python编程基础:循环结构for和while
-
Python中的循环结构包括两个,一是遍历循环(for循环),一是条件循环(while循环)。遍历循环遍历循环(for循环)会挨个访问序列或可迭代对象的元素,并执行里面的代码块。foriinra...
- 学习编程第154天 python编程 for循环输出菱形图
-
今天学习的是刘金玉老师零基础Python教程第38期,主要内容是python编程for循环输出菱形※。(一)利用for循环输出菱形形状的*号图形1.思路:将菱形分解为上下两个部分三角形图案,分别利用...
- python 10个堪称完美的for循环实践
-
在Python中,for循环的高效使用能显著提升代码性能和可读性。以下是10个堪称完美的for循环实践,涵盖数据处理、算法优化和Pythonic编程风格:1.遍历列表同时获取索引(enumerate...
- 一周热门
- 最近发表
-
- pyproject.toml到底是什么东西?(py trim)
- BDP服务平台SDK for Python3发布(bdp数据平台)
- Python-for-Android (p4a):(python-for-android p4a windows)
- Qt for Python—Qt Designer 概览
- Python:判断质数(jmu-python-判断质数)
- 为什么那么多人讨厌Python(为什么python这么难)
- 一文了解 Python 中带有 else 的循环语句 for-else/while-else
- python的numpy向量化语句为什么会比for快?
- 开眼界!Python遍历文件可以这样做
- 告别简单format()!Python Formatter类让你的代码更专业
- 标签列表
-
- mybatiscollection (79)
- mqtt服务器 (88)
- keyerror (78)
- c#map (65)
- xftp6 (83)
- bt搜索 (75)
- c#var (76)
- xcode-select (66)
- mysql授权 (74)
- 下载测试 (70)
- linuxlink (65)
- pythonwget (67)
- androidinclude (65)
- libcrypto.so (74)
- linux安装minio (74)
- ubuntuunzip (67)
- vscode使用技巧 (83)
- secure-file-priv (67)
- vue阻止冒泡 (67)
- jquery跨域 (68)
- php写入文件 (73)
- kafkatools (66)
- mysql导出数据库 (66)
- jquery鼠标移入移出 (71)
- 取小数点后两位的函数 (73)