.net Framework 3.5 + C# 3 发布了包括LinQ等一系列功能,其中包括了匿名类型,而我们在升级到.net4后,发现原来写好的用于POCO的深拷贝方法 static object Clone(object obj) 在匿名对象上不管用了。
原因与切入点
目前使用的深拷贝实现方式包括:
- 在类型内部编码实现,比如实现ICloneable接口。
- 通过序列化、反序列化方式复制对象。
- 使用反射遍历被拷贝对象的属性,取值并赋值给新的实例。
上述方式均不可用,考察原因,我们使用.net Reflector反编译匿名类型 new { Foo = 123, Bar = 456 },可见其代码结构如下:
注:编译与运行在.net Framework 4。目前发现使用ILSpy似乎只能看到IL,不能反出C#代码来。
[CompilerGenerated] internal sealed class <>f__AnonymousType0<j__TPar, j__TPar> { // Fields [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly j__TPar i__Field; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private readonly j__TPar i__Field; // Constructor [DebuggerHidden] public <>f__AnonymousType0( j__TPar Foo, j__TPar Bar); // Properties public j__TPar Bar { get; } public j__TPar Foo { get; } // Methods [DebuggerHidden] public override bool Equals(object value); [DebuggerHidden] public override int GetHashCode(); [DebuggerHidden] public override string ToString(); }
得到:
- 匿名类型的代码是编译器生成的,所以无法在其内部进行手工编码。
- .net内置的几个序列化器要求类型被标记SerializableAttribute,或者实现序列化接口,或者属性可读写且有无参数的构造函数,匿名类型并不符合这些条件。
- 匿名类型的属性没有set方法,不能通过反射赋值。
从反编译的代码,我们可以看到,匿名类型仅有一个构造函数,而该构造函数的参数和其属性是一一对应的,查看其代码,发现其正式通过此构造函数为各个域赋值的,我们便从从这个点入手考虑深拷贝的实现。
解决方案
现在将匿名类型和非匿名类型的深拷分开处理,这里我们将原来的深拷贝方法重命名为CloneOnymousObject,而匿名类型的深拷贝方法为CloneAnonymousObject,那么现在的Clone方法如下:
static object Clone(object obj) { if (obj == null) return null; if (IsAnonymousType(obj.GetType())) return CloneAnonymousObject(obj); return CloneOnymousObject(obj); }
如何判断类型是匿名类型
并没有发现.net Framework提供了直接的方式来判定类型是否是匿名类型,目前只能通过类型的特征来判断,从匿名类型的结构上抽取这些特征:
- 是一个泛型的非公共class;
- 标记有CompilerGeneratedAttribute;
- 类名称带有“AnonymousType”。微软编译器编译的类型名称还带有“<>”,但测试Mono编译器的编译结果是没有的,这里取其公共部分。
根据这些特征,编写IsAnonymousType的实现如下:
private static bool IsAnonymousType(Type type) { if (!type.IsGenericType) return false; if ((type.Attributes & TypeAttributes.NotPublic) != TypeAttributes.NotPublic) return false; if (!Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)) return false; return type.Name.Contains("AnonymousType"); }
深拷贝的实现
我们要做的便是从被拷贝对象的属性获取对应的值,将其作为新对象的构造函数的参数。而观察匿名类型的结构,可知其构造函数的参数的类型与参数名称其属性的定义是一致的,于是有了下面的方法:
private static object CloneAnonymousObject(object obj) { var type = obj.GetType(); var parameters = type.GetConstructors()[0].GetParameters(); var args = new object[parameters.Length]; // 对应构造函数的每个参数,取同名属性的值 for (int i = 0; i < parameters.Length; i++) { var propertyInfo = type.GetProperty(parameters[i].Name); var value = propertyInfo.GetValue(obj, null); args[i] = Clone(value); } var instance = Activator.CreateInstance(type, args); return instance; }
下面是完整的代码:
public static object Clone(object obj) { if (obj == null) return null; if (IsAnonymousType(obj.GetType())) return CloneAnonymousObject(obj); return CloneOnymousObject(obj); } private static bool IsAnonymousType(Type type) { if (!type.IsGenericType) return false; if ((type.Attributes & TypeAttributes.NotPublic) != TypeAttributes.NotPublic) return false; if (!Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)) return false; return type.Name.Contains("AnonymousType"); } private static object CloneAnonymousObject(object obj) { var type = obj.GetType(); var parameters = type.GetConstructors()[0].GetParameters(); var args = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { var propertyInfo = type.GetProperty(parameters[i].Name); var value = propertyInfo.GetValue(obj, null); args[i] = Clone(value); } var instance = Activator.CreateInstance(type, args); return instance; } private static object CloneOnymousObject(object obj) { //原来的Clone方法 }
简单的测试:
var o = new { Foo = 3, Bar = "x" }; dynamic cloned = Clone(o); Console.WriteLine("{0} {1}", cloned.Foo, cloned.Bar); //=> 3 x var o2 = new { Foo = "x", Bar = 1 }; dynamic cloned2 = Clone(o2); Console.WriteLine("{0} {1}", cloned2.Foo, cloned2.Bar); //=> x 3
小结
该方案的缺点显而易见:它是根据匿名类型的编译结果分析得到的,依赖于编译器的实现,一旦编译结果改变,方案可能就不管用了。
写在后面
两个疑问:
- 此问题来自于一次对于匿名类型的不太正确的使用,该场景随后被改进,于是不再需要拷贝匿名对象了,但留下了如何进行拷贝的问题。那么到底在什么场景下才需要用到匿名类型的拷贝呢?
- 如果用Mono.Cecil给匿名类型加上SerialiableAttribute,其实例是否可用BinaryFormatter进行序列化和反序列化,进而通过这种方式实现Clone呢?