首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

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

编程知识
2024年09月21日 19:39

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

在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

From:https://www.cnblogs.com/hugogoos/p/18424476
本文地址: http://www.shuzixingkong.net/article/2189
0评论
提交 加载更多评论
其他文章 Qt表格入门
这篇博客文章深入探讨了Qt表格处理的基础知识与实践技巧。主要内容包括:使用QTableWidget和QTableView展示数据,通过QStyledItemDelegate和QSortFilterProxyModel实现数据代理、过滤与排序功能。文章还附有详细的代码示例,指导读者如何在Qt中创建并个
Qt表格入门 Qt表格入门 Qt表格入门
数字产品护照 (DPP) 解决方案:利用 Blazor 和区块链实现产品全生命周期追踪
数字产品护照 (DPP) 解决方案:利用 Blazor 和区块链实现产品全生命周期追踪 随着全球对可持续发展和产品透明度的关注日益增加,企业需要一种可靠的方法来跟踪和管理产品生命周期中的关键数据。我们的数字产品护照(Digital Product Passport,DPP)系统正是为此而生,提供了一
数字产品护照 (DPP) 解决方案:利用 Blazor 和区块链实现产品全生命周期追踪 数字产品护照 (DPP) 解决方案:利用 Blazor 和区块链实现产品全生命周期追踪 数字产品护照 (DPP) 解决方案:利用 Blazor 和区块链实现产品全生命周期追踪
Maven 使用方法
Maven Maven是一个项目管理工具,它包含了一个项目对象模型(POM:Project Object Model),其表现于一个XML文件(pom.xml),其中包含了项目的基本学习,依赖关系,插件配置,构建路径等等 为什么使用Maven 导入第三方jar包更便捷:之前我们在使用第三方框架时我们
Maven 使用方法 Maven 使用方法 Maven 使用方法
适用于 VitePress 的公告插件开发实记
开发了一个适用于 VitePress 站点的公告插件 vitepress-plugin-announcement
适用于 VitePress 的公告插件开发实记 适用于 VitePress 的公告插件开发实记 适用于 VitePress 的公告插件开发实记
IT男如何走上的自由职业之路。
前言 在博客园的一篇文章《40岁大龄失业程序猿,未来该何去何从》,在下面留言,目前自己在做自由职业,很多人加好友咨询自由职业的事情,对IT行业自由职业比较感兴趣,问怎么能走上这条路,所以才有了今天这篇文章。把自己走上自由职业这条路说下情况。 正文 现状:我84年的,自由18年开始自由职业,已有6年有
IT男如何走上的自由职业之路。 IT男如何走上的自由职业之路。 IT男如何走上的自由职业之路。
web架构-nginx负载均衡
nginx的负载均衡 Nginx 是一个广泛使用的反向代理服务器,能够高效地实现负载均衡。负载均衡的核心作用是将来自客户端的请求分发到多个后端服务器上,从而平衡每台服务器的压力。通过Nginx,我们可以实现多种负载均衡算法,如轮询、IP哈希等。 vi /etc/nginx/nginx.conf 插入
web架构-nginx负载均衡 web架构-nginx负载均衡 web架构-nginx负载均衡
java基础 -网络编程笔记
666,InetAddress package com.hspedu.api; import java.net.InetAddress; import java.net.UnknownHostException; public class API { public static void main(
java基础 -网络编程笔记 java基础 -网络编程笔记 java基础 -网络编程笔记
提升软件测试效率与灵活性:探索Mock测试的重要性
Mock测试是测试过程中的一种方法,用于替代那些难以构造或获取的对象,通过创建虚拟对象来进行测试。所谓难以构造的对象如何理解呢? 举例来说,像HttpServletRequest这样的对象需要在具有servlet容器环境的情况下才能创建和获取。而难以获取的对象则是指需要准备相关环境才能使用的对象,比
提升软件测试效率与灵活性:探索Mock测试的重要性