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

性能

框架性能怎么样呢,分别比较通过手写代码、DeepCopy、二进制序列化和Json.Net序列化来实现对象的深拷贝,然后通过 Benchmark测试一下它们之间的性能。

实现方式 Method Mean Error StdDev Gen 0 Allocated
手写代码 CodeCopy 7.874 ns 0.0941 ns 0.0880 ns 0.0203 64 B
DeepCopy DeepCopy 114.510 ns 0.4071 ns 0.3608 ns 0.0203 64 B
二进制序列化 BinarySerialize 46,912.139 ns 156.4497 ns 138.6886 ns 3.4180 10827 B
Json.Net序列化 JsonSerialize 8,942.457 ns 97.0560 ns 90.7862 ns 1.6479 5208 B

虽然DeepCopy与手写代码来说性能相差很大(差异很大的根本原因是反复查找是否存在自引用),但是与二进制序列化、Json序列化来说,性能不在一个等级上。

总结

这是框架的内部逻辑,当然还有一些细节被遗漏了,例如:数组中的特殊处理DeepCopier.cs

当然还有很多需要优化的细节,大家可以在github上提出您的宝贵意见。

参考内容:

作者:Sweet Tang

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

欢迎转载,请在明显位置给出出处及链接。

.NET IL实现对象深拷贝的更多相关文章

  1. .NEL IL实现对象深拷贝

    对于深拷贝,通常的方法是将对象进行序列化,然后再反序化成为另一个对象.例如在stackoverflow上有这样的解决办法:https://stackoverflow.com/questions/785 ...

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

    对于深拷贝,通常的方法是将对象进行序列化,然后再反序化成为另一个对象.例如在stackoverflow上有这样的解决办法:https://stackoverflow.com/questions/785 ...

  3. [C#]对象深拷贝

    关键代码: /// <summary> /// 对象深拷贝 /// </summary> /// <typeparam name="T">泛型& ...

  4. javascript对象深拷贝,浅拷贝 ,支持数组

    javascript对象深拷贝,浅拷贝 ,支持数组 经常看到讨论c#深拷贝,浅拷贝的博客,最近js写的比较多, 所以也来玩玩js的对象拷贝. 下面是维基百科对深浅拷贝的解释: 浅拷贝 One meth ...

  5. JavaScript递归实现对象深拷贝

    let personOne = { name:"张三", age:18, sex:"male", children:{ first:{ name:"z ...

  6. JavaScript:利用递归实现对象深拷贝

    先来普及一下深拷贝和浅拷贝的区别浅拷贝:就是简单的复制,用等号即可完成 let a = {a: 1} let b = a 这就完成了一个浅拷贝但是当修改对象b的时候,我们发现对象a的值也被改变了 b. ...

  7. Map拷贝 关于对象深拷贝 浅拷贝的问题

    问题:map拷贝时发现数据会变化. 高能预警,你看到的下面的栗子是不正确的,后面有正确的一种办法,如果需要看的话的,请看到底,感谢各同学的提醒,已做更正,一定要看到最后      先看例子:     ...

  8. 也来玩玩 javascript对象深拷贝,浅拷贝

    经常看到讨论c#深拷贝,浅拷贝的博客,最近js写的比较多, 所以也来玩玩js的对象拷贝. 下面是维基百科对深浅拷贝的解释: 浅拷贝 One method of copying an object is ...

  9. js对象深拷贝

    数组一维深拷贝:slice.concat.Array.from 对象一维深拷贝:Object.assign 一.利用扩展运算符...对数组中嵌套对象进行深拷贝 var arr=[{a:1,b:2},{ ...

随机推荐

  1. linux磁盘管理系列三:LVM的使用

    磁盘管理系列 linux磁盘管理系列一:磁盘配额管理   http://www.cnblogs.com/zhaojiedi1992/p/zhaojiedi_linux_040_quota.html l ...

  2. JDK源码分析(1)之 String 相关

    ​在此之前有无数次下定决心要把JDK的源码大致看一遍,但是每次还没点开就已被一个超链接或者其他事情吸引直接跳开了.直到最近突然意识到,因为对源码的了解不深导致踩了许多莫名其妙的坑,所以再次下定决心要把 ...

  3. linux集群自动化搭建(生成密钥对+分发公钥+远程批量执行脚本)

    之前介绍过ansible的使用,通过ssh授权批量控制服务器集群 但是生成密钥和分发公钥的时候都是需要确认密码的,这一步也是可以自动化的,利用ssh + expect + scp就可以实现,其实只用这 ...

  4. Spring Boot(九)Swagger2自动生成接口文档和Mock模拟数据

    一.简介 在当下这个前后端分离的技术趋势下,前端工程师过度依赖后端工程师的接口和数据,给开发带来了两大问题: 问题一.后端接口查看难:要怎么调用?参数怎么传递?有几个参数?参数都代表什么含义? 问题二 ...

  5. React Fiber源码分析 (介绍)

    写了分析源码的文章后, 总觉得缺少了什么, 在这里补一个整体的总结,输出个人的理解~ 文章的系列标题为Fiber源码分析, 那么什么是Fiber,官方给出的解释是: React Fiber是对核心算法 ...

  6. JavaScript 中,定义函数时用 var foo = function () {} 和 function foo() {}有什么区别?

    对于新手来说(本人也是新手-_-!),好像var foo = function () {} 和 function foo(){}并没有什么区别,意识里可能就认为就是两种不同的写法而已.但是,通过网上查 ...

  7. C# 提取PPT文本和图片的实现方案

    在图文混排的文档中,我们可以根据需要将文档中的文字信息或者图片提取出来,通过C#代码可以提取Word和PDF文件中的文本和图片,那么同样的,我们也可以提取PPT幻灯片当中的文本和图片.本篇文档将讲述如 ...

  8. Android破解学习之路(八)—— 进化之地内购破解

    最近在TapTap闲逛,看到了进化之地这款游戏,TapTap上有两个进化之地,一个是在TapTap直接购买的,另外一个则是试玩版,玩到中间就会有个购买完整版. 试玩版连接:https://www.ta ...

  9. 20, CSS 定义选择器

    1. ID 与类 2. 层叠 3. 分组 4. 继承 5. 上下文选择器 6. 子类选择器 7. 其他选择器 8. 结构与注释 20.1 ID 与类 选择器是用于控制页面设计的样式.即 ID 选择器何 ...

  10. 前端入门18-JavaScript进阶之作用域链

    声明 本系列文章内容全部梳理自以下几个来源: <JavaScript权威指南> MDN web docs Github:smyhvae/web Github:goddyZhao/Trans ...