IL入门之旅(二)——动态包装

1.包装与为什么要包装

    oo的世界看起来很完美,但是也有不少缺点,尤其是遇到静态语言(例如:c#,java等),经常会受制于类型不匹配这样的问题。

    例如,某个类库需要一个INamedObject对象,而另一个类库仅仅提供了一个Thread对象,怎么办哪?在不可能修改类库的情况下,通常就会写一个Wrapper,把Thread包装成INamedObject,大概的代码如下:

public interface INamedObject

{

    string Name { get; }

}



public class ThreadWrapper : INamedObject

{

    private Thread m_thread;



    public ThreadWrapper(Thread thread)

    {

        m_thread = thread;

    }



    public string Name

    {

        get { return m_thread.Name; }

    }

}

    这样就把一个Thread包装成了一个INamedObject,但是,如果有一堆这样的类需要被包装的话,这也就以为之有一堆的包装类需要去写。说道这里,相信oo的缺点已经暴露了出来了。

2.Duck Typing与动态包装

    接下来看看另一套类型系统Duck Typing是如何处理这个问题的:

"when I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck."

    说白了,Duck Typing并不关心对象的真实类型,而仅仅是关心有没有对应的方法,换句话说,Duck Typing本身并不关心INamedObject,也根本不需要这个接口的存在,它所需要的仅仅是某个对象的Name属性。

    c# 4.0提供了dynamic关键字,可以很轻松的完成这样的工作,不过,4.0还没正式发布,而且就算发布了,也不会所有的项目都用4.0来写。

    那么,在2.0的时代就没法享受Duck Typing的思想了吗?

    其实只要那个Wrapper可以自动生成,那么,INamedObject就可以简单的生成一个包装,实现这个接口,这样就完成了一次伪装。而如何在运行时生成这样一个包装就是本文接下来要讲述的。

3.分析和目标制定

    在开工前,先分析一下要实现任意类->INamedObject的动态包装类需要完成和注意些什么问题。

    看一下ThreadWrapper类:

  • 这个类需要实现INamedObject接口
  • 需要一个原始对象的字段,来保存原始对象
  • 一个构造函数,把这个原始对象放进去
  • 一系列的方法,实现这个接口
  • 在每个方法中,调用原始对象的同名方法

    因为INamedObject只有一个Name属性,所以,这一系列的方法就简化成一个Name属性的get方法。

    其次,因为这里需要动态生成一个类型(例如ThreadWrapper),所以这次不能像上一次那样偷工减料的用一个DynamicMethod,而是需要完整的DynamicAssembly。

    最后,因为是运行时动态生成的类型,显然不能在代码中依赖到这些类型,也就是无法直接用new去创建wrapper类,这时候,需要借用创建模式中的工厂方法来协助创建这些wrapper。

    (不难发现,设计模式总是在必要的时候,自然而然的被使用;而不是特意去套用那些设计模式,或者说滥用设计模式,这也是初学者最容易犯的错误之一)

4.实现目标

    首先,创建一个动态程序集和其他一些基本要素:

public static class DynamicWrapper

{

    private readonly static AssemblyBuilder s_assembly =

        AppDomain.CurrentDomain.DefineDynamicAssembly(

        new AssemblyName("DynamicWrapper"), AssemblyBuilderAccess.Run);

    private readonly static ModuleBuilder s_module =

        (ModuleBuilder)s_assembly.GetModules()[0];

    private static int s_typeId;



    public static TInterface Wrap<TClass, TInterface>(TClass obj)

    {

        throw new NotImplementedException();

    }



    internal static TypeBuilder DefineType()

    {

        return s_module.DefineType("DynamicWrapper" + (Interlocked.Increment(ref s_typeId)).ToString());

    }

}

    做个简单的说明:

  • s_assembly用于保持对动态程序集实例的引用,可以看到创建参数用了Run,也就是这个动态程序集支持直接运行里面的类型,但是不支持把这个动态程序集保存到硬盘
  • s_module则简单的直接引用了动态程序集的默认Module,当然也可以另外创建,不过这里没有这个必要
  • s_typeId则记录了类型的个数,用于创建类型名称时避免重复。
  • Wrap方法就是预留的工厂方法,当然暂时未实现
  • DefineType这个内部方法用于创建一个类型

    现在问题变成如何实现Wrap方法,这里先不考虑创建类型的问题,先考虑一下性能问题,创建类型本身是一个比较消耗的CPU的,如果为相同的类型重复创建Wrapper类型,肯定得不偿失,因此必须要准备一个必要的缓存机制,如果有缓存机制的存在,那么同时也要考虑多线程并发的问题。

    当然,这不是本文的重点,因此直接使用一个最简单的缓存机制——泛型类型的静态字段:

internal static class WrapperImpl<TClass, TInterface>

{

    public readonly static Func<TClass, TInterface> WrapperCreator = CreateWrapperCreator();



    private static Type CreateWrapperType()

    {

        var type = DynamicWrapper.DefineType();

        // todo

        return type.CreateType();

    }



    private static Func<TClass, TInterface> CreateWrapperCreator()

    {

        Type type = CreateWrapperType();

        return o => (TInterface)Activator.CreateInstance(type, o);

    }

}

    这样,去掉参数检查的话,Wrap方法可以非常简单的写成:

public static TInterface Wrap<TClass, TInterface>(TClass obj)

{

    return WrapperImpl<TClass, TInterface>.WrapperCreator(obj);

}

    看到CreateWrapperCreator方法了吧,是不是想起了上一集讨论的如何创建实例,对了,这也就是上一集为什么要讨论创建实例的问题,还记得几个实现的速度差异吧(当然CreateInstance<T>方法用不上,这个方式没法带参数),如果想改用DynamicMethod,当然也可以,只不过,这里就用CreateInstance方式简化非重点内容了。

    好,回到重点的CreateWrapperType方法上,这里真正需要创建一个Wrapper类型了,使用DynamicWrapper类预先提供的DefineType方法可以获得一个继承自Object的空类型,那么首先要实现TInterface:

type.AddInterfaceImplementation(typeof(TInterface));

    是不是很容易,别急,这里只是相当于在ThreadWrapper类型后面加了个”: INamedObject”,方法还没哪,这样的一个类型在type.CreateType()时会报错的(除非这个接口本来就是一个空接口。。。),因此,接下来是实现接口,完整地代码如下:

private static Type CreateWrapperType()

{

    var type = DynamicWrapper.DefineType();

    type.AddInterfaceImplementation(typeof(TInterface));

    var impl = type.DefineField("impl", typeof(TClass), FieldAttributes.Private | FieldAttributes.InitOnly);

    CreateCtor(type, impl);

    foreach (MethodInfo mi in typeof(TInterface).GetMethods(BindingFlags.Public | BindingFlags.Instance))

    {

        ImplInterface(type, impl, mi);

    }

    return type.CreateType();

}



private static void CreateCtor(TypeBuilder type, FieldBuilder impl)

{

    var ctor = type.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { typeof(TClass) });

    var il = ctor.GetILGenerator();

    il.Emit(OpCodes.Ldarg_0);

    il.Emit(OpCodes.Ldarg_1);

    il.Emit(OpCodes.Stfld, impl);

    il.Emit(OpCodes.Ret);

}



private static void ImplInterface(TypeBuilder type, FieldBuilder impl, MethodInfo mi)

{

    Type[] methodParams = (from p in mi.GetParameters()

                           select p.ParameterType).ToArray();

    var method = type.DefineMethod(mi.Name,

        MethodAttributes.Public | MethodAttributes.NewSlot |

        MethodAttributes.Virtual | MethodAttributes.Final);

    method.SetReturnType(mi.ReturnType);

    method.SetParameters(methodParams);

    var il = method.GetILGenerator();

    var implMethod = typeof(TClass).GetMethod(mi.Name,

        BindingFlags.Public | BindingFlags.Instance, null, methodParams, null);

    if (implMethod != null && implMethod.ReturnType == mi.ReturnType)

    {

        il.Emit(OpCodes.Ldarg_0);

        il.Emit(OpCodes.Ldfld, impl);

        for (int i = 0; i < methodParams.Length; i++)

        {

            il.Emit(OpCodes.Ldarg, i + 1);

        }

        il.Emit(OpCodes.Callvirt, implMethod);

        il.Emit(OpCodes.Ret);

    }

    else

    {

        il.Emit(OpCodes.Ldstr, typeof(TClass).FullName);

        il.Emit(OpCodes.Ldstr, mi.Name);

        il.Emit(OpCodes.Newobj, typeof(MissingMethodException).GetConstructor(

            new Type[] { typeof(string), typeof(string) }));

        il.Emit(OpCodes.Throw);

    }

}

    这里需要注意几点:

    首先,声明了一个叫impl的字段,类型为TClass,并且是Private和InitOnly(没有声明为Static,所以为实例字段)。InitOnly就相当于c#的readonly,也就是仅仅在构造函数中才能够设置其值。

    其次,调用了一个CreateCtor的方法,用于创建构造函数,参数为一个TClass。

    最后,为每一个接口方法Delegate到一个实现类的方法。当然前提是方法名称、参数和返回值都一样。

    不过这里有个问题,如果方法对应不到实现哪?

    当然,这种情况有两种解决方案:

  • 认为这个对象无法转换成接口,直接throw new InvalidCastException();
  • 不过也可以运用Duck Typing的一个原则:

In other words, don't check whether it IS-a duck: check whether it QUACKS-like-a duck, WALKS-like-a duck, etc, etc, depending on exactly what subset of duck-like behaviour you need to play your language-games with.

    也就是这里用的认为实现了这个接口,而是在真正调用这个方法时抛出MissingMethodException来代表这个方法其实没有实现。

5.简单测试

    一个初步的实现已经完成了,来看看运行起来的效果如何:

static void Main(string[] args)

{

    Thread.CurrentThread.Name = "Hello world!";

    INamedObject namedObj = DynamicWrapper.Wrap<Thread, INamedObject>(Thread.CurrentThread);

    Console.WriteLine(namedObj.Name);

    INamedObject duckObj = DynamicWrapper.Wrap<object, INamedObject>(new object());

    try

    {

        Console.WriteLine(duckObj.Name);

    }

    catch (MissingMethodException ex)

    {

        Console.WriteLine(ex.Message);

    }

}

    看看执行的结果:

Hello world!
未找到方法“System.Object.get_Name”。

    看起来还不错吧。

6.缺陷

    写到这里,有没有发现问题?

    什么,没发现。。。好吧,再仔细想一想:

  • 值类型和引用类型,对了,这里把所有的TClass当成了引用类型,在遇到值类型的时候就会出错,这是第一个问题
  • 接口如果有泛型方法的时候,并没有对应的处理,这是第二个问题
  • 接口如果有要求实现其他接口的话,创建包装的时候需要吧要求实现的接口一起实现,这是第三个问题

    当然这些问题是可以解决的,至于怎么解决,就是留给大家的思考题。

你可能感兴趣的:(入门)