既存のクラスに実装されたメソッドをとあるインターフェイスが定義したメソッドであるかのようにふるまえると嬉しい場面があります。例えば次のようなテキストの記録が行えるインターフェイス IRecordable
があったとします。
public interface IRecordable { void AppendText(string text); }
仮に IRecordable
を用いてログを保存するとすれば次のような感じになるかと思います。
public class Logger { public void LogMessage(string message, IRecordable recordable) { recordable.AppendText(message); } }
もしログを記録したい場所が TextBox
だとしたら IRecordable
を TextBox
に継承させる, TextBox
のラッパーを作成するといった方法が考えられます。
public class RecordableTextBox : TextBox, IRecordable { }
public class TextBoxProxy : IRecordable { private TextBoxBase textBox; public TextBoxProxy(TextBoxBase textBox) this.textBox = textBox; } public void AppendText(string text) { textBox.AppendText(text); } }
前者の方法だと TextBox
を継承したほかの TextBox
クラスがたくさんあったときにそれぞれに対して IRecordable
を継承させなければなりません。そもそも sealed クラスだったら継承できません。後者の方法は簡単にかつ確実に実装できます。
しかし簡単に実装できるといっても多少の面倒があります。そこで Emit
を用いてラッパークラスを動的に生成することで手間を省きます。
public interface IProxy { }
public static Type CreateProxyType<Proxy>(Type delegation) where Proxy : IProxy { AssemblyName name = new AssemblyName("ProxyAssembly"); AssemblyBuilder assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(name, AssemblyBuilderAccess.Run); ModuleBuilder module = assemblyBuilder.DefineDynamicModule(name.Name + ".dll"); Type proxy = typeof(Proxy); TypeBuilder proxyType = module.DefineType(delegation.Name + "Proxy", TypeAttributes.Public, typeof(object), new Type[] { proxy }); FieldBuilder self =proxyType.DefineField("self", delegation, FieldAttributes.Private); ConstructorBuilder constructor = proxyType.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new Type[] { delegation }); ILGenerator constructorIL = constructor.GetILGenerator(); constructorIL.Emit(OpCodes.Ldarg_0); constructorIL.Emit(OpCodes.Call, DefaultConstructor); constructorIL.Emit(OpCodes.Ldarg_0); constructorIL.Emit(OpCodes.Ldarg_1); constructorIL.Emit(OpCodes.Stfld, self); constructorIL.Emit(OpCodes.Ret); foreach (MethodInfo declaration in proxy.GetMethods()) { ParameterInfo[] parameters = declaration.GetParameters(); Type[] parameterTypes = new Type[parameters.Length]; for (int index = 0; index < parameters.Length; index++) { parameterTypes[index] = parameters[index].ParameterType; } Type returnType = declaration.ReturnType; MethodInfo delegatedMethod = delegation.GetMethod(declaration.Name, parameterTypes); MethodBuilder proxyMethod = proxyType.DefineMethod(declaration.Name, MethodAttributes.Public | MethodAttributes.Virtual, CallingConventions.HasThis, returnType, parameterTypes); ILGenerator methodIL = proxyMethod.GetILGenerator(); methodIL.Emit(OpCodes.Ldarg_0); methodIL.Emit(OpCodes.Ldfld, self); // 引数のインデックスは1から始まる。 for (int index = 1; index <= parameterTypes.Length; index++) { methodIL.Emit(OpCodes.Ldarg_S, index); } methodIL.Emit(OpCodes.Callvirt, delegatedMethod); methodIL.Emit(OpCodes.Ret); proxyType.DefineMethodOverride(proxyMethod , declaration); } return proxyType.CreateType(); }
これで前述の TextBoxProxy
型を動的に作成できます。
Type proxy = CreateProxyType<IRecordable>(typeof(TextBoxBase)); IRecordable recordable = (IRecordable)Activator.CreateInstance(proxy, textBox);
この例ではインターフェイスに定義されるメソッドのシグネチャが一致している場合しか使用できませませんが,適当に属性を使用すれば他のメソッドに委譲することもできるでしょう。
[2012-08-27 追記] ある程度まとまったので GitHub にて公開。