はじめに
この記事では、デザイン時T4テキストテンプレートを使って面倒なコード作成を自動化する方法についてまとめます。
デザイン時T4テキストテンプレートとは
Visual Studio上で.ttファイル(テキストテンプレートファイル)を作成することでそのテンプレートの保存時にテンプレートを元にコードを自動生成する仕組みのことです。デザイン時T4テキストテンプレートだけでなく、コード上でテキスト形式でコードを生成する実行時T4テキストテンプレートも仕組みとして用意されています。詳細はこちらを参照ください。
デザイン時T4テキストテンプレートを使わない場合の問題点
例えば以下のような型と文字列を紐づけて記録する独自コレクション(TypeAndStringMappingCollection)を作ったとします。
コレクションでは、型と紐づけて文字列を記録するためのAddメソッドと、引数に文字列と型一覧を与えて、型一覧全てに文字列が紐づいているかを返すメソッドの2つがあるとします。
/// <summary>
/// 型と文字列を紐づけて記録するコレクション
/// </summary>
internal class TypeAndStringMappingCollection : ITypeAndStringMappingCollection
{
/// <summary>
/// 記録用のディクショナリ
/// 型と文字列の一覧を紐づけて記録する
/// </summary>
private Dictionary<Type, List<string>> _typeAndStringsDictionary;
/// <summary>
/// 型に紐づけて文字列を記録する
/// </summary>
/// <typeparam name="T">型</typeparam>
/// <param name="str">文字列</param>
public void Add<T>(string str)
{
if (!_typeAndStringsDictionary.ContainsKey(typeof(T)))
{
_typeAndStringsDictionary.Add(typeof(T), new List<string>());
}
_typeAndStringsDictionary[typeof(T)].Add(str);
}
/// <summary>
/// 引数の文字列と型一覧が全て紐づいているか
/// </summary>
/// <param name="types">型一覧</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列と型一覧が全て紐づいているか</returns>
public bool CommonStringExists(IEnumerable<Type> types, string str)
{
foreach (var type in types)
{
if (!_typeAndStringsDictionary.ContainsKey(type)) return false;
if (!_typeAndStringsDictionary[type].Contains(str)) return false;
}
return true;
}
}
そして、このコレクションは以下のインターフェース(ITypeAndStringMappingCollection)を実装しているとします。
/// <summary>
/// 型と文字列を紐づけて記録するコレクション
/// </summary>
public interface ITypeAndStringMappingCollection
{
/// <summary>
/// 型に紐づけて文字列を記録する
/// </summary>
/// <typeparam name="T">型</typeparam>
/// <param name="str">文字列</param>
void Add<T>(string str);
/// <summary>
/// 引数の文字列と型一覧が全て紐づいているか
/// </summary>
/// <param name="types">型一覧</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列と型一覧が全て紐づいているか</returns>
bool CommonStringExists(IEnumerable<Type> types, string str);
}
このインターフェースのCommonStringExsitsメソッドに対して、ジェネリックを使って型を受け取るような拡張メソッドを定義したいと考えます。仮にジェネリックを使って1~16個の型を受け取れるようにすると以下のように似たようなコードを大量に書かないといけません。
/// <summary>
/// 型と文字列を紐づけて記録するコレクションの拡張メソッドを定義するクラス
/// </summary>
public static class ITypeAndStringMappingCollectionExtensions
{
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<T1>(this ITypeAndStringMappingCollection self, string str)
{
var types = new List<Type>(){ typeof(T1), };
return self.CommonStringExists(types, str);
}
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<T1, T2>(this ITypeAndStringMappingCollection self, string str)
{
var types = new List<Type>(){ typeof(T1), typeof(T2), };
return self.CommonStringExists(types, str);
}
...(ここにはジェネリックが3個から15個のメソッドが定義されているが省略)
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this ITypeAndStringMappingCollection self, string str)
{
var types = new List<Type>(){ typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8), typeof(T9), typeof(T10), typeof(T11), typeof(T12), typeof(T13), typeof(T14), typeof(T15), typeof(T16), };
return self.CommonStringExists(types, str);
}
}
デザイン時T4テキストテンプレートを使うとどうなるのか
前章の拡張メソッドを直で書くのはなかなか大変なので、デザイン時T4テキストテンプレートを使って実装してみます。
VisualStudio上でテンプレートを追加するフォルダを右クリックして[追加]-[新しい項目]メニューから以下のようにテキストテンプレートを追加します。
テンプレートを以下のように記載します。なお、テンプレートファイルの編集にはこちらのtangible T4 EditorのようなT4用エディタを使うと編集しやすいです。(インテリセンスがある程度効くようになるため)
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#
// ジェネリックとして指定できる型の最大数
var maxGenericTypesCount = 16;
// 引数に与えた数分のジェネリックを定義するための文字列を生成する
// (メソッドの後につける<>の中に記載するジェネリック定義の文字列が生成される)
Func<int, string> CreateGenericTypesString = genericTypesCount =>
{
var genericTypesString = "T1";
for(var count = 2; count <= genericTypesCount; count++)
{
genericTypesString += $", T{count}";
}
return genericTypesString;
};
// 引数に与えた数分のジェネリックの型のリストを定義するための文字列を生成する
Func<int, string> CreateGenericTypsListDefinitionString = genericTypesCount =>
{
var genericTypesListDefinition = "new List<Type>(){ ";
for(var count = 1; count <= genericTypesCount; count++)
{
genericTypesListDefinition += $"typeof(T{count}), ";
}
genericTypesListDefinition += "}";
return genericTypesListDefinition;
};
# >
using System;
using System.Collections.Generic;
namespace T4TemplateSample.Interfaces
{
/// <summary>
/// 型と文字列を紐づけて記録するコレクションの拡張メソッドを定義するクラス
/// </summary>
public static class ITypeAndStringMappingCollectionExtensions
{
<# for (var count = 1; count <= maxGenericTypesCount; count++) { #>
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<<#= CreateGenericTypesString(count) #>>(this ITypeAndStringMappingCollection self, string str)
{
var types = <#= CreateGenericTypsListDefinitionString(count) #>;
return self.CommonStringExists(types, str);
}
<# }#>
}
}
テンプレートの以下の部分では生成するファイルの拡張子を指定しています。今回は.csファイルを作成したいので.cs
としています
<#@ template debug="false" hostspecific="false" language="C#" #>
...
<#@ output extension=".cs" #> // ここでcsファイルを生成するように設定
<#
...
以下の部分では、コード生成時に利用する変数と文字列を生成するメソッドを定義しています。
必要な変数やメソッドはこのようにテンプレートファイルの上部にまとめておくとわかりやすいです。
ジェネリックで指定できる型の最大数を変数で保持し、ジェネリックを指定するための文字列(メソッドの名の右につける<T1,...,T16>
のこと)を返すメソッド、ジェネリックで指定された型をまとめたリストを作るための文字列(拡張メソッド内で利用するnew List<Type>{typeof(T1),...typeof(T16)}
のこと)を返すメソッドを定義しています。
...
<#@ output extension=".cs" #>
<#
// ジェネリックとして指定できる型の最大数
var maxGenericTypesCount = 16;
// 引数に与えた数分のジェネリックを定義するための文字列を生成する
// (メソッドの後につける<>の中に記載するジェネリック定義の文字列が生成される)
Func<int, string> CreateGenericTypesString = genericTypesCount =>
{
var genericTypesString = "T1";
for(var count = 2; count <= genericTypesCount; count++)
{
genericTypesString += $", T{count}";
}
return genericTypesString;
};
// 引数に与えた数分のジェネリックの型のリストを定義するための文字列を生成する
Func<int, string> CreateGenericTypsListDefinitionString = genericTypesCount =>
{
var genericTypesListDefinition = "new List<Type>(){ ";
for(var count = 1; count <= genericTypesCount; count++)
{
genericTypesListDefinition += $"typeof(T{count}), ";
}
genericTypesListDefinition += "}";
return genericTypesListDefinition;
};
# >
...
以下の部分が実際に生成されるコードの内容となります。
<# #>
で囲んだ部分ではC#のコードを記載できます。以下では定義した変数を使ってfor文を記載しています。その結果<# for...{ #>
から<# } #>
の中に記載されたコードが1~16(テンプレートの上部で定義したジェネリックで指定できる型の最大数)まで繰り返し生成されます。
for文内では<#=
を使って、テンプレートの上部定義したメソッドを呼び出しています。<#= CreateGenericTypesString(count) #>
と記載した場合は、変数countでCreateGenericTypesStringメソッドを呼び出し際の戻り値の文字列が入ります。つまり、for文の1ループ目(変数countの値は1)では、<#= CreateGenericTypesString(count) #>
で生成される文字列はT1
、<#= CreateGenericTypsListDefinitionString(count) #>で生成される文字列は`new List {typeof(T1), }`となります。for文の2ループ目(変数countの値は2)では、`<#= CreateGenericTypesString(count) #>`で生成される文字列は`T1, T2`、<#= CreateGenericTypsListDefinitionString(count) #>で生成される文字列は`new List {typeof(T1), typeof(T2), }`となります。そのため、ジェネリックを1~16個持つ拡張メソッドを以下の記載でいっぺんにコード生成できます。
using System;
using System.Collections.Generic;
namespace T4TemplateSample.Interfaces
{
/// <summary>
/// 型と文字列を紐づけて記録するコレクションの拡張メソッドを定義するクラス
/// </summary>
public static class ITypeAndStringMappingCollectionExtensions
{
<# for (var count = 1; count <= maxGenericTypesCount; count++) { #>
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<<#= CreateGenericTypesString(count) #>>(this ITypeAndStringMappingCollection self, string str)
{
var types = <#= CreateGenericTypsListDefinitionString(count) #>;
return self.CommonStringExists(types, str);
}
<# }#>
}
}
上記のテンプレートファイルを保存するとそのテンプレートファイルに紐づくようにcsファイルが生成されます。
実際に生成されたコードは以下です。
using System;
using System.Collections.Generic;
namespace T4TemplateSample.Interfaces
{
/// <summary>
/// 型と文字列を紐づけて記録するコレクションの拡張メソッドを定義するクラス
/// </summary>
public static class ITypeAndStringMappingCollectionExtensions
{
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<T1>(this ITypeAndStringMappingCollection self, string str)
{
var types = new List<Type>(){ typeof(T1), };
return self.CommonStringExists(types, str);
}
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<T1, T2>(this ITypeAndStringMappingCollection self, string str)
{
var types = new List<Type>(){ typeof(T1), typeof(T2), };
return self.CommonStringExists(types, str);
}
...(ここにはジェネリックが3個から15個のメソッドが定義されているが省略)
/// <summary>
/// 引数の文字列とジェネリックで指定した型一覧が全て紐づいているか
/// </summary>
/// <param name="self">コレクション</param>
/// <param name="str">文字列</param>
/// <returns>引数の文字列とジェネリックで指定した型一覧が全て紐づいているか</returns>
public static bool CommonStringExists<T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16>(this ITypeAndStringMappingCollection self, string str)
{
var types = new List<Type>(){ typeof(T1), typeof(T2), typeof(T3), typeof(T4), typeof(T5), typeof(T6), typeof(T7), typeof(T8), typeof(T9), typeof(T10), typeof(T11), typeof(T12), typeof(T13), typeof(T14), typeof(T15), typeof(T16), };
return self.CommonStringExists(types, str);
}
}
}
このようにデザイン時T4テキストテンプレートを使えば、面倒なコード実装を簡単に行うことができます。
T4テンプレートを使うことでコメントを変えたりした場合に全てのメソッドに逐一変更を反映させずに済みますし、例えばジェネリックの数を16→10に変更したいとなった時にもテンプレート内で定義した変数の値を変えるだけで済みます。
まとめ
本記事では、デザイン時T4テキストテンプレートを使って面倒なコード作成を自動化する方法についてまとめました。似たようなコードやメソッドをいくつか実装しないといけない状況になった際にはデザイン時T4テキストテンプレートが使えないかを一度考えてみると幸せになるかもしれません。