您的位置:首页 > 编程语言 > C#

[C#]匿名类型的深拷贝

2013-11-20 20:44 197 查看
.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<<Foo>j__TPar, <Bar>j__TPar>
{
// Fields
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly <Bar>j__TPar <Bar>i__Field;
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private readonly <Foo>j__TPar <Foo>i__Field;

// Constructor
[DebuggerHidden]
public <>f__AnonymousType0(<Foo>j__TPar Foo, <Bar>j__TPar Bar);

// Properties
public <Bar>j__TPar Bar { get; }
public <Foo>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方法
}


View Code

简单的测试:

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呢?
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: