1. 程式人生 > >【轉】.NET IL實現對象深拷貝

【轉】.NET IL實現對象深拷貝

tro 原理 htm 過程 image bcf compiler javascrip tex

對於深拷貝,通常的方法是將對象進行序列化,然後再反序化成為另一個對象。例如在stackoverflow上有這樣的解決辦法:https://stackoverflow.com/questions/78536/deep-cloning-objects/78612#78612。這種序列化的方式,對深拷貝來講,無疑是一個性能殺手。

今天大家介紹一個深拷貝的框架 DeepCopy,github地址:https://github.com/ReubenBond/DeepCopy,它是從orleans框架改編過來的,實現邏輯非常簡單。

框架的實現原理是通過IL代碼生成字段拷貝的方法。IL的優點是可以繞過C#的語法規則,例如:訪問私有對象以及給readonly

字段賦值等。

在介紹框架前,先介紹一下IL相關的工具。

IL工具

即使您不是第一次使用IL,這也不是一件容易的事情,無法確認什麽樣IL代碼才能達到預期的結果。這是工具來幫助您的地方。可以先用C#編寫代碼,然後將它復制到LINQPad中,運行並打開輸出中的IL選項卡。

技術分享

使用像JetBrains的dotPeek這樣的反編譯/反匯編程序也是一個不錯選擇。您可以將編譯的程序集在dotPeek中打開它來顯示IL。

最後,ReSharper是不可或缺的工具。ReSharper帶有一個方便的IL查看器。

技術分享

這些工具可以幫助您如何解決IL產生的問題,您也可以訪問官方文檔。

DeepCopy

DeepCopy本質上它只提供了一個方法:

    public static T Copy<T>(T original);

DeepCopy調用示例代碼:

    List<string> original = new List<string>(2);

    original.Add("A");
    original.Add("B");

    var result = DeepCopier.Copy(original);

實現原理

Copy方法將遞歸傳遞對象中的每個字段復制到相同類型的新實例中。首先要處理的是對同一個對象的多次引用,如果用戶提供了一個包含自身引用的對象,那麽結果也會包含對自身的引用。這意味著我們需要執行引用跟蹤。這點很容易做到:我們維護一個Dictionary<object, object>

從原始對象到拷貝對象的映射。我們的主要方法Copy<T>(T orig)將調用上下文的方法來檢查字典中拷貝的對象是否存在:

    public static T Copy<T>(T original, CopyContext context)
    {
      /* TODO: implementation */
    }

拷貝流程大致如下:

  • 如果傳入是null,則返回null
  • 如果傳入的對象已經拷貝過,則返回其拷貝過的對象;
  • 如果傳入是“不可變的對象”,則直接返回傳入對象;
  • 如果傳入是一個數組,則將每個元素復制到一個新數組中並將其返回;
  • 創建一個新的傳入類型實例,遞歸地將每個字段從傳入對象復制到拷貝對象並返回。

對“不可變對象”的定義很簡單:類型是一個基原類型、EnumStringGuidDateTime...,或者使用特殊[Immutable]標記的類型。更詳細的不可變類型可以參考源代碼,CopyPolicy.cs。

除了上面的最後一步,其它的事情都很簡單。最後一步,遞歸復制每個字段,可以使用反射來獲取和設置字段值。反射是一個性能殺手,所以使用IL來實現這一步。

IL代碼實現

DeepCopy中的主要IL代碼在CopierGenerator.cs類的CreateCopier<T>(Type type)方法中。讓我們一步步揭秘:

首先創建一個DynamicMethod對象,它將保存創建的IL代碼。在創建DynamicMethod對象時,必須告訴它簽名是什麽,在這裏,它是一個通用的委托類型delegate T DeepCopyDelegate<T>(T original, CopyContext context)

    var dynamicMethod = new DynamicMethod(
        type.Name + "DeepCopier",
        typeof(T), // 委托返回的類型
        new[] {typeof(T), typeof(CopyContext)}, // 委托的參數類型。
        typeof(CopierGenerator).Module,
        true);
    
    var il = dynamicMethod.GetILGenerator(); 

IL將會變得相當復雜,因為它需要處理不可變的類型和值類型,接下來讓我一點一點地說明。

    // 定義一個變量來保存返回的結果。
    il.DeclareLocal(type);

接下來,需要初始化傳入類型的新實例到局部變量。有三種情況需要考慮,每種情況對應下面代碼中的一個塊:

  • 該類型是一個值類型(結構)。使用default(T)表達式來初始化它。
  • 該類型有一個無參數的構造函數。通過調用new T()初始化它。
  • 該類型沒有無參數的構造函數。在這種情況下,我們借助 .Net 框架來解決,調用FormatterServices.GetUninitializedObject(type)
    // 構造結果對象實例。
    var constructorInfo = type.GetConstructor(Type.EmptyTypes);
    if (type.IsValueType)
    {
        // 值類型可以直接初始化。
        // C#: result = default(T);
        il.Emit(OpCodes.Ldloca_S, (byte)0);
        il.Emit(OpCodes.Initobj, type);
    }
    else if (constructorInfo != null)
    {
        // 如果存在默認構造函數,則直接使用默認的參數。
        // C#: result = new T();
        il.Emit(OpCodes.Newobj, constructorInfo);
        il.Emit(OpCodes.Stloc_0);
    }
    else
    {
        // 如果沒有默認構造函數的存在,使用GetUninitializedObject創建實例。
        // C#: result = (T)FormatterServices.GetUninitializedObject(type);
        il.Emit(OpCodes.Ldtoken, type);
        il.Emit(OpCodes.Call, DeepCopier.MethodInfos.GetTypeFromHandle);
        il.Emit(OpCodes.Call, this.methodInfos.GetUninitializedObject);
        il.Emit(OpCodes.Castclass, type);
        il.Emit(OpCodes.Stloc_0);
    }

在本地創建一個用於保存結果的變量,它是傳入類型的新實例。在我們做任何事情之前,我們必須記錄新創建對象的引用。將每個參數按順序推入堆棧,並使用OpCodes.Call來調用context.RecordObject(original, result)。使用OpCodes.Call來調用CopyContext.RecordObject方法,因為CopyContext是一個sealed類,否則會使用OpCodes.Callvirt

    // 值類型的實例不會存在多次引用的問題,
    // 所以只在上下文中記錄引用類型。
    if (!type.IsValueType)
    {
        // 記錄對象引用。
        // C#: context.RecordObject(original, result);
        il.Emit(OpCodes.Ldarg_1); // 參數:context
        il.Emit(OpCodes.Ldarg_0); // 參數數:original
        il.Emit(OpCodes.Ldloc_0); // 本地用來保存結果的變量
        il.Emit(OpCodes.Call, this.methodInfos.RecordObject);
    }

枚舉對象上的每一個字段並生成代碼,將字段的值復制到結果變量中。過程如下:

    // 復制每一個字段的值。
    foreach (var field in this.copyPolicy.GetCopyableFields(type))
    {
        // 加載結果對象的引用。
        if (type.IsValueType)
        {
            // 值類型需要通過地址來加載,而不是復制到堆棧上。
            il.Emit(OpCodes.Ldloca_S, (byte)0);
        }
        else
        {
            il.Emit(OpCodes.Ldloc_0);
        }
    
        // 加載原始對象字段的值。
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldfld, field);
    
        // 如果是不可變類型則直接賦值,否則需要深拷貝字段。
        if (!this.copyPolicy.IsShallowCopyable(field.FieldType))
        {
            // 復制字段使用泛型方法 DeepCopy.Copy<T>(T original, CopyContext context)
            // C#: Copy<T>(field)
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Call, this.methodInfos.CopyInner.MakeGenericMethod(field.FieldType));
        }
    
        // 將復制的值賦給結果對象的字段。
        il.Emit(OpCodes.Stfld, field);
    }

返回結果並通過CreateDelegate構建委托,下一步可以直接使用。

    // C#: return result;
    il.Emit(OpCodes.Ldloc_0);
    il.Emit(OpCodes.Ret);
    
    return dynamicMethod.CreateDelegate(typeof(DeepCopyDelegate<T>)) as DeepCopyDelegate<T>;

總結

這是框架的內部邏輯,當然還有一些細節被遺漏了,例如:數組中的特殊處理DeepCopier.cs;

當然還有很多需要優化的細節,大家可以在github上提出您的寶貴意見。

參考內容:

  • https://reubenbond.github.io/posts/codegen-2-il-boogaloo

本文轉自:http://www.cnblogs.com/tdfblog/p/DeepCopy-By-IL.html

【轉】.NET IL實現對象深拷貝