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

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

posted on 2010-03-13 15:50  Zhenway  阅读(519)  评论(0编辑  收藏  举报