首页 > 编程语言 >C#|.net core 基础 - 深拷贝的五大类N种实现方式

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

时间:2024-09-25 12:35:42浏览次数:7  
标签:core C# clone original var new net type public

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

  合集 - C#|.net core 基础(6)   1.C#|.net core 基础 -“hello”.IndexOf(“\0”,2)中的坑08-302.C#|.net core 基础 -如何判断连续子序列09-033.C#|.net core 基础 -值传递 vs 引用传递09-194.C#|.net core 基础 -扩展数组添加删除性能最好的方法09-205.C#|.net core 基础 -byte数组如何高效转换为16进制字符串08-29 6.C#|.net core 基础 -深拷贝的五大类N种实现方式09-21 收起  

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

在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 基础 分类: C#|.net core 基础

标签:core,C#,clone,original,var,new,net,type,public
From: https://www.cnblogs.com/sexintercourse/p/18431088

相关文章

  • 一个.NET开源、快速、低延迟的异步套接字服务器和客户端库
    一个.NET开源、快速、低延迟的异步套接字服务器和客户端库 思维导航前言项目介绍主要特性功能组件使用示例基准测试项目源代码TCP聊天服务器示例项目源码地址优秀项目和框架精选前言最近有不少小伙伴在问:.NET有什么值得推荐的网络通信框架?今天大姚给大家分......
  • 用C#写个PDF批量合并工具简化日常工作
    用C#写个PDF批量合并工具简化日常工作一.前言由于项目需要编写大量的材料,以及各种签字表格、文书等,最后以PDF作为材料交付的文档格式,过程文档时有变化或补充,故此处理PDF文档已经成为日常工作的一部分。网上有各种PDF处理工具,总是感觉用得不跟手。最后回顾自己的需求总结为以下......
  • C#上位机与PLC通信心跳的实现方法
    付工上位机 C#上位机与PLC通信心跳的实现方法合集-上位机开发(4) 1.零基础学习Modbus通信协议09-132.RS485与ModbusRTU09-103.C#上位机与PLC通信心跳的实现方法09-234.ModbusRTU通信协议报文剖析09-24收起 -Begin-大家好!我是付工。众所周知,在工业自......
  • JavaScript 之父联手近万名开发者集体讨伐 Oracle:给 JavaScript 一条活路吧!
    JavaScript之父联手近万名开发者集体讨伐Oracle:给JavaScript一条活路吧!投递人 itwriter 发布于 2024-09-2401:08 评论(6) 有1528人阅读 原文链接 [收藏] « »近日,据外媒消息,JavaScript杰出人士和至少9000名其他相关方签署了一封联名信,再次要求Oracle......
  • WINCCV7.5SP2使用VBA一次性修改多个IO域连接的变量
    某浪博客那边效率低下,学习笔记类型的也要审核多日,还做了访问量清零的事情。我把今天的学习笔记在这里也记录一遍。前几天QQ群里面有哥们询问在WINCC中页面中一次性设定多个IO域连接变量,这些连接变量有规律。我以前没有用过VBA,尝试着弄了一下,现在把过程记录下来,当作学习笔记吧。......
  • AIGC赋能游戏美术新高度,2024年还不会用AI技术的原画师设计师真的out了!
    大家好,我是强哥随着AIGC技术的飞速发展与大模型的不断成熟迭代,使得其应用前景正在越来越宽阔地展现出来,**“AIGC+”也将逐渐成为各类行业发展的新模式,**也极大地提升了各内容行业的想象空间。而在众多应用领域中,游戏相比其他内容形态具备更强的科技属性,这意味着,游戏行业有......
  • 【YashanDB知识库】yashandb执行包含带oracle dblink表的sql时性能差
    本文内容来自YashanDB官网,具体内容请见https://www.yashandb.com/newsinfo/7396959.html?templateId=1718516问题现象yashandb执行带oracledblink表的sql性能差:同样的语句,同样的数据,oracle通过dblink访问远端oracle执行,耗时不到1秒钟:问题的风险及影响yashandb通过dblink访问oracle......
  • 安装PyTorch环境(CPU版)
    1、下载Anaconda官网,安装时需要勾选的选项见下图DownloadAnacondaDistribution|Anacondahttps://www.anaconda.com/download 2、创建虚拟环境2.1打开AnacondaPrompt在所有应用中找到Anaconda中的AnacondaPrompt,点击打开进入cmd面板2.2创建环境在cmd面板中,输入......
  • Navicat连接Mongodb成功了,但是无法显示数据库怎么办?
    不知道你是否遇到过?Navicat连接Mongodb成功了,但是无法显示数据库怎么办?解决办法这个问题比较坑,对于第一次接触的小伙伴,可能会一脸懵逼,原因就是在Navicat中默认会不显示隐藏的项目,如果不手动勾选上,就无法显示,勾选之后,下次就不用重复勾选了。......
  • STL之手撕vector
    前言面试的时候遇到了,是从来没想过会出问题的手撕。竟然在面试环节下出了不少纰漏。要点构造函数:默认构造、拷贝构造、赋值运算符重载、移动构造函数、析构函数push_back/pop_back代码#include<iostream>usingnamespacestd;#defineDEFAULT_CAP(200)class......