• 作者:老汪软件技巧
  • 发表时间:2024-09-23 07:00
  • 浏览量:

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

在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 Models { get; set; }
}
public static void ManualPure()
{
    var original = new CloneModel
    {
        Models = new List
        {
            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(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(json);
}
public static T SerializeByJsonNet<T>(T original)
{
    var json = Newtonsoft.Json.JsonConvert.SerializeObject(original);
    return Newtonsoft.Json.JsonConvert.DeserializeObject(json);
}

第四类、第三方库方式

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

1、AutoMapper

安装AutoMapper库

public static T ThirdPartyByAutomapper<T>(T original)
{
    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap();
    });
    var mapper = config.CreateMapper();
    T clone = mapper.Map(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> _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)dymMethod.CreateDelegate(typeof(Func));
            _cacheILEmit.Add(type, func);
        }
        return func(original);
    }
}

3、表达式树

表达式树是一种数据结构,在运行时会被编译成IL代码,同样的这些代码也是比较晦涩难懂,后面找机会单独讲解。另外这里也加入了缓存机制,以提高效率。

public class DeepCopyExpressionTree<T>
{
    private static readonly Dictionary> _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();
            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>(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 Models { get; set; }
}

其中Models包含两个元素。最后测试结果如下:

通过结果可以发现:[表达式树]和[Emit] > [AutoMapper]和[DeepCloner] > [MessagePack] > 其他

第一梯队:性能最好的是[表达式树]和[Emit],两者相差无几,根本原因因为最终都是IL代码,减少了各种反射导致的性能损失。因此如果你有极致的性能需求,可以基于这两种方案进行改进以满足自己的需求。

第二梯队:第三方库[AutoMapper]和[DeepCloner] 性能紧随其后,相对来说也不错,而且是成熟的库,因此如果项目上使用可以优先考虑。

第三梯队:[MessagePack]性能比第二梯队差了一倍,当然这个也需要安装第三方库。

第四梯队:[System.Text.Json]如果不想额外安装库,有没有很高的性能要求可以考虑使用微软自身的Json序列化工具。

其他方法就可以忽略不看了。

注:测试方法代码以及示例源码都已经上传至代码库,有兴趣的可以看看。/hugogoos/Pl…