• 【转】.NET IL实现对象深拷贝


    对于深拷贝,通常的方法是将对象进行序列化,然后再反序化成为另一个对象。例如在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选项卡。

    LINQPad

    使用像JetBrains的dotPeek这样的反编译/反汇编程序也是一个不错选择。您可以将编译的程序集在dotPeek中打开它来显示IL。

    最后,ReSharper是不可或缺的工具。ReSharper带有一个方便的IL查看器

    ReSharper

    这些工具可以帮助您如何解决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上提出您的宝贵意见。

    参考内容:

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

  • 相关阅读:
    UVALive 5983 MAGRID DP
    2015暑假训练(UVALive 5983
    poj 1426 Find The Multiple (BFS)
    poj 3126 Prime Path (BFS)
    poj 2251 Dungeon Master 3维bfs(水水)
    poj 3278 catch that cow BFS(基础水)
    poj3083 Children of the Candy Corn BFS&&DFS
    BZOJ1878: [SDOI2009]HH的项链 (离线查询+树状数组)
    洛谷P3178 [HAOI2015]树上操作(dfs序+线段树)
    洛谷P3065 [USACO12DEC]第一!First!(Trie树+拓扑排序)
  • 原文地址:https://www.cnblogs.com/dazhuangtage/p/7810675.html
Copyright © 2020-2023  润新知