zoukankan      html  css  js  c++  java
  • 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当成了引用类型,在遇到值类型的时候就会出错,这是第一个问题
    • 接口如果有泛型方法的时候,并没有对应的处理,这是第二个问题
    • 接口如果有要求实现其他接口的话,创建包装的时候需要吧要求实现的接口一起实现,这是第三个问题

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

  • 相关阅读:
    python中字典dict pop方法
    Markdown 学习资源
    Windows bat 设置代理
    Warning: Permanently added '...' (RSA) to the list of known hosts --Windows下git bash 警告处理
    subilme增加对markdown的高亮支持
    ubuntu笔记1
    Sublime Python 插件配置合集
    Excel VBA 快捷键 代码
    贩卖守望先锋账号
    如何用VS2017用C++语言写Hello world 程序?
  • 原文地址:https://www.cnblogs.com/vwxyzh/p/1685103.html
Copyright © 2011-2022 走看看