インターフェイスを実装したふりをさせる


既存のクラスに実装されたメソッドをとあるインターフェイスが定義したメソッドであるかのようにふるまえると嬉しい場面があります。例えば次のようなテキストの記録が行えるインターフェイス IRecordable があったとします。

public interface IRecordable
{
	void AppendText(string text);
}

仮に IRecordable を用いてログを保存するとすれば次のような感じになるかと思います。

public class Logger
{
	public void LogMessage(string message, IRecordable recordable)
	{
		recordable.AppendText(message);
	}
}

もしログを記録したい場所が TextBox だとしたら IRecordableTextBox に継承させる, 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 にて公開