对于深拷贝,通常的方法是将对象进行序列化,然后再反序化成为另一个对象。例如在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
; - 如果传入的对象已经拷贝过,则返回其拷贝过的对象;
- 如果传入是“不可变的对象”,则直接返回传入对象;
- 如果传入是一个数组,则将每个元素复制到一个新数组中并将其返回;
- 创建一个新的传入类型实例,递归地将每个字段从传入对象复制到拷贝对象并返回。
对“不可变对象”的定义很简单:类型是一个基原类型、 Enum
、 String
、 Guid
、 DateTime
...,或者使用特殊 [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上提出您的宝贵意见。
参考内容:
注:本文内容来自互联网,旨在为开发者提供分享、交流的平台。如有涉及文章版权等事宜,请你联系站长进行处理。