C#でtemplateっぽいことをしてみたかった話

きっかけ

今、研究でゲームAIをC#で書いているのですが、「相手がパスした後に呼び出されるメソッド」と「相手が通常の着手を行った後に呼び出されるメソッド」が必要になる場面がありました。しかし、これらの2つのメソッドは一部分だけが異なり、残りは概ね同じ処理なので出来れば1つのメソッドに統合したいです。C++であればテンプレート引数に何でもとれるので以下のように実装できます(注: 実際のコードはもっと複雑です)。

template<bool AFTER_PASS>
void VisitNode(Node node)
{
	/*
	 *
	 * 共通の処理
	 *
	*/

	if constexpr (AFTER_PASS)
	{
		// 相手がパスした場合の処理
	}
	else
	{
		// 相手が通常の着手をした場合の処理
	}

	/*
	 *
	 * 共通の処理
	 *
	*/
}

このコードはコンパイラによって以下の2つの関数に自動的に展開されます。つまり上のコードのif文は実行時には消えるというわけです。このほうがいちいち2つの関数を定義しなくていいのでシンプルですね。

template<>
void VisitNode<true>(Node node)
{
	/*
	 *
	 * 共通の処理
	 *
	*/

	// 相手がパスした場合の処理

	/*
	 *
	 * 共通の処理
	 *
	*/
}

template<>
void VisitNode<false>(Node node)
{
	/*
	 *
	 * 共通の処理
	 *
	*/

	// 相手が通常の着手をした場合の処理

	/*
	 *
	 * 共通の処理
	 *
	*/
}

さて、C#でもこれと同じようなことをやってみようとすると1つの問題にぶつかります。C#にはC++のtemplateと似た概念としてジェネリック(もしくはジェネリクス)というものあります。しかし、ジェネリックはtemplateの下位互換であり、C++でいうtemplate引数に型引数しかとれません。bool型の値を渡すことなどはできないのです。

ジェネリックメソッドの展開

C#ジェネリックでは、型引数に値型を与えた場合に限り、C++のtemplateと同様にコンパイル時にそれぞれの型に応じて展開されます。例えば、以下のようなFoo<T>というジェネリックメソッドについて考えてみましょう(namespaceやclassは省略しています)。

static void Main()
{
	Foo<int>();
	Foo<long>();
}

static void Foo<T>() where T : struct
{
	if(typeof(T) == typeof(int))
		Console.WriteLine("Hello 32-bit signed integer!!");
	else if(typeof(T) == typeof(long))		
		Console.WriteLine("Hello 64-bit signed integer!!");
	else
		Console.WriteLine($"Hello struct!!");
}

このFoo<T>メソッドは、Mainメソッド内でFoo<int>()、Foo<long>()と呼び出されています。この場合は、以下のような2つのメソッドがコンパイラによって展開されます。したがってFoo<T>内のif文は実行時には存在しないことになります。

static void Main()
{
	Foo<int>();
	Foo<long>();
}

static void Foo<int>() 
{
	Console.WriteLine("Hello 32-bit signed integer!!");
}

static void Foo<long>() 
{
	Console.WriteLine("Hello 64-bit signed integer!!");
}

以上のことを踏まえると、C++のtemplate引数にbool型の値を与えるときと同じようなことをC#ジェネリックで実現できそうです。

実際に自分がやったこと

まず以下のinterfaceと構造体を定義します。

interface IFlag { }
struct True : IFlag { }
struct False : IFlag { }

これらを定義すれば、先ほどのC++のコードをC#で以下のように記述できます。

void VisitNode<AfterPass>(Node node) where AfterPass : struct, IFlag
{
	/*
	 *
	 * 共通の処理
	 *
	*/

	if (typeof(AfterPass) == typeof(True))
	{
		// 相手がパスした場合の処理
	}
	else
	{
		// 相手が通常の着手をした場合の処理
	}

	/*
	 *
	 * 共通の処理
	 *
	*/
}

このようにすれば、相手がパスした場合と相手が通常の着手をした場合のメソッドをわざわざ作らずとも1つのメソッドにまとめることができました。if文に関しては、typeof(AfterPass)がコンパイル時に決まるので実行時には消えています。
どうでもいい話ですけど、if (typeof(AfterPass) == typeof(True)) の部分が、if(AfterPass == true)と書いているみたいで少しバカみたいですね。

整数などを引数に取らせたい場合はどうするか(未解決)

整数なども苦行ではありますが、One構造体やTwo構造体を作ればできそうではありますが、typeof(T)同士の大小比較はできないのであまりできることはなさそうです。そういった場合は、Source Generatorなどを代替手段に使うか、条件に応じてメソッドを作るしかなさそうです。何か他に案があれば誰かコメントください。