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当成了引用类型,在遇到值类型的时候就会出错,这是第一个问题
- 接口如果有泛型方法的时候,并没有对应的处理,这是第二个问题
- 接口如果有要求实现其他接口的话,创建包装的时候需要吧要求实现的接口一起实现,这是第三个问题
当然这些问题是可以解决的,至于怎么解决,就是留给大家的思考题。