C#|.net core 基礎 - 深複製的五大類N種實現方式

IT规划师發表於2024-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

相關文章