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

news2024/11/17 12:37:35

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

在这里插入图片描述

在C#里拷贝对象是一个看似简单实则相当复杂的事情,因此我不建议自己去做封装方法然后项目上使用的,这里面坑太多,容易出问题。下面给大家分享五大类N种深拷贝方法。

01第一类、针对简单引用类型方式

这类方法只对简单引用类型有效,如果类型中包含引用类型的属性字段,则无效。

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));
}

02第二类、手动方式

这类方法都是需要手动处理的,简单又复杂。

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接口

首先这是内置接口,也仅仅是定义了接口,具体实现还是需要靠自己实现,所以理论上和纯手工一样的,可以唯一的好处就是有一个统一定义,具体实现看完这篇文章都可以用来实现这个接口,这里就不在赘述了。

03第三类、序列化方式

这类方法核心思想就是先序列化再反序列化,这里面也可以分为三小类:二进制类、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);
}

04第四类、第三方库方式

这类方法使用简单,方案成熟,比较适合项目上使用。

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);
}

05第五类、扩展视野方式

这类方法都是半成品方法,仅供参考,提供思路,扩展视野,不适合项目使用,当然你可以把它们完善,各种特殊情况问题都处理好也是可以在项目上使用的。

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);
    }
}

06基准测试

最后我们对后面三类所有方法进行一次基准测试对比,每个方法分别执行三组测试,三组分别测试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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2162688.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第6章 常用UI组件库

一.Element Plus组件库 1. 安装Element Plus 什么是Element Plus&#xff1f; Element Plus是基于Vue 3开发的优秀的PC端开源UI组件库&#xff0c;它是Element的升级版&#xff0c;对于习惯使用Element的人员来说&#xff0c;在学习Element Plus时&#xff0c;不用花费太多的…

CC面试准备

半导体基础 半导体是介于导体和绝缘体之间的一种介质&#xff0c;在不同条件下表现出不同的导电性或者不导电特性&#xff0c; 电子半导体器件材料大部分为硅&#xff0c;锗等元素 本征半导体&#xff1a;完全不含杂质的纯净半导体&#xff0c;因为不含杂质&#xff0c;其中…

[笔记]一组电缆、定位相关产品的技术参数

csdn不允许做广告&#xff0c;这里的那家定位供应商的技术看起来是可以的。很有希望。它的原理并不复杂&#xff0c;这家企业在处理业务领域以外的新型产品时&#xff0c;是查过资料的&#xff0c;这就超过了60%的同行。 1.电缆 仅给出现在市面供应的铠装电缆结构&#xff0c…

七层负载均衡和四层负载均衡的区别

文章目录 什么是七层负载均衡&#xff1f;一、定义与工作原理二、优点与缺点三、应用场景四、常见七层负载均衡器五、负载均衡算法 什么是四层负载均衡&#xff1f;一、定义与原理定义&#xff1a;原理&#xff1a; 二、特点与应用场景特点&#xff1a;应用场景&#xff1a; 三…

STM32基础学习笔记-ADC面试基础题6

第六章、ADC 常见问题 1、基本概念&#xff1a;什么是ADC &#xff1f;作用 &#xff1f;逐次逼近型 2、传感器本质 &#xff1f;传感器、电压、ADC数值转化 &#xff1f; 3、ADC的特征 &#xff1f; 转化时间、分辨率、精度、量化误差 &#xff1f; 4、ADC框图组成部分 &…

华为云发布全栈可观测平台AOM,以AI赋能应用运维可观测

9月19日&#xff0c;华为全联接大会2024举办期间&#xff0c;在“AI赋能应用现代化&#xff0c;加速软件生产力跃升”为主题的论坛上&#xff0c;华为云发布全栈可观测平台AOM&#xff0c;以AI赋能应用运维可观测&#xff0c;提升企业应用可用性与稳定性。 该平台发布标志着华…

针对国产化--离线安装Nginx rpm包下载 ARM64(.aarch64.rpm) 版本下载

源地址&#xff1a;https://nginx.org/packages/centos/7/aarch64/RPMS/ 可以选择系统分别进行下载对应的rmp包

公安局软件管理平台建设方案和必要性,论文-2-———未来之窗行业应用跨平台架构

一、平台方略 随着gov信息化建设的不断推进&#xff0c;各类ZW软件的应用需求日益增加。为了提高ZW软件的获取便利性、AQ性和规范性&#xff0c;建设一个专门的GOV软件管理平台具有重要意义。 集中提供各类ZW软件&#xff0c;方便工作人员快速获取和安装&#xff0c;减少因软…

基于DAMODEL——Faster-RCNN 训练与测试指南

Faster-RCNN 训练与测试指南 前言 今天我们要来实现一个经典的目标检测模型&#xff1a;Faster-Rcnn。我们使用DAMODEL云平台来实现&#xff0c;这是个很强大的云端平台&#xff0c;功能众多&#xff0c;你可以投你所好去进行你想做的事情。 1. 环境与工具准备 1.1 远程连接…

经颅磁刺激技术,脑科学研究——精神患者治疗方案

经颅磁刺激&#xff08;Transcranial Magnetic Stimulation &#xff0c;TMS&#xff09;技术是一种利用脉冲磁场作用于中枢神经系统&#xff08;主要是大脑&#xff09;&#xff0c;改变皮层神经细胞的膜电位&#xff0c;使之产生感应电流&#xff0c;影响脑内代谢和神经电活动…

开放原子开源基金会OPENATOM

AtomGit_开放原子开源基金会代码托管平台-AtomGit 开放原子开源基金会是致力于推动全球开源事业发展的非营利机构&#xff0c;于 2020 年 6 月在北京成立&#xff0c;由阿里巴巴、百度、华为、浪潮、360、腾讯、招商银行等多家龙头科技企业联合发起。 精选项目&#xff1a; 比…

HDFS_API文件和文件夹

代码&#xff1a; Beforepublic void init() throws URISyntaxException, IOException {URI uri new URI("hdfs://master:9000");// 创建一个配置文件Configuration entries new Configuration();// 获取到了客户端对象 // entries.set("dfs.replicat…

【C++笔试强训】

​ 学习编程就得循环渐进&#xff0c;扎实基础&#xff0c;勿在浮沙筑高台 循环渐进Forward-CSDN博客 目录 循环渐进Forward-CSDN博客 第一题&#xff1a;除2&#xff01; 第二题&#xff1a;dd爱框框 第三题&#xff1a;简写单词 第一题&#xff1a;除2&#xff01; 牛客网…

Uniapp 打包后的横屏控制

文章目录 问题处理参考 问题 Uniapp 打包后的横竖屏跟开发模式不太一样&#xff0c;需要在代码里设置一下 处理 需要改两个地方 pages.json&#xff1a;在 globalStyle 中添加 "pageOrientation": "auto", manifest.json&#xff1a;在第一层后面添加…

每日学习一个数据结构-哈希表(散列表)

文章目录 示意图一、基本概念二、工作原理三、常用哈希函数四、冲突解决方法五、优缺点六、应用场景 哈希表&#xff08;Hash table&#xff09;&#xff0c;也被称为散列表&#xff0c;是一种基于哈希函数的数据结构&#xff0c;它通过把关键码值&#xff08;Key value&#x…

毛竹泛基因组-文献精读52

Haplotype-based pangenomes reveal genetic variations and climate adaptations in moso bamboo populations 基于单倍型的泛基因组揭示了毛竹种群中的遗传变异和气候适应性 摘要 毛竹&#xff08;Phyllostachys edulis&#xff09;是东亚地区一种在生态和经济上都具有重要…

二刷LeetCode:“51.N皇后 37.解数独”题解心得(简单易懂)

引言&#xff08;初遇噩梦&#xff0c;再遇坦然&#xff09; 在阅读本文之前&#xff0c;建议大家已经接触过回溯算法&#xff0c;并完成回溯相关题目&#xff0c;例如&#xff1a;子集问题、组合问题、排列问题。 子集&#xff1a;子集II、子集 组合&#xff1a;组合、组合总和…

[spring]用MyBatis XML操作数据库 其他查询操作 数据库连接池 mysql企业开发规范

文章目录 一. MyBatis XML配置文件1. 配置链接字符串和MyBatis2. 写持久层代码方法定义Interface方法实现xml测试 3. 增删改查增:删改查 二. 开发规范(mysql)三. 其他查询操作1. 多表查询2. #{} 和 ${}(面试题)使用区别 排序功能like查询 三. 数据库连接池 一. MyBatis XML配置…

【华为】用策略路由解决双出口运营商问题

需求描述 不同网段访问互联网资源时&#xff0c;走不同的出口&#xff0c;即PC1走电信出口&#xff0c;PC2走移动出口。 客户在内网接口下应用策略路由后往往出现无法访问内网管理地址的现象&#xff0c;该举例给出解决办法。 拓扑图 基础配置 #sysname R1 # # interface G…

【hot100-java】【下一个排列】

R8-技巧篇 最近速成java中&#xff0c;算法基础需要兼顾。 class Solution {public void nextPermutation(int[] nums) {int lennums.length;List<Integer>list new ArrayList<>();boolean flagtrue;for (int ilen-1;i>0;i--){list.add(nums[i]);Collections.…