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

.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方法
}
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呢?

转载于:https://www.cnblogs.com/cmstar/p/3431589.html

你可能感兴趣的:([C#]匿名类型的深拷贝)