LoginSignup
14
8

More than 3 years have passed since last update.

T4テキストテンプレート入門 - デザイン時T4テキストテンプレート編

Last updated at Posted at 2020-04-28

はじめに

コーディングをする中で T4 を使うシーンが出たため, 備忘録代わりに記事にしていきます.

T4 とはなんぞや??という方は, 公式の概要ページ を一読してみてください.

今回は, 2種類ある T4テキストテンプレート の中の デザイン時T4テキストテンプレート について記述します.

かなり部分的な内容しか記述しませんが, 今回の記事内容のことを把握していればやりたいことはだいたいできると思います.

もし, やりたいことに対して記事内容が不足している場合は

辺りを参考にしていただくと、やりたいことを実現するための情報が得られるかもしれないです.


今回の目標

こういう練習にはすでに存在しているものを再発明するのがわかりやすくて良いので, Action を自前実装した MyAction というものを作っていきたいと思います.

ご存知の方が多いとは思いますが, Action の定義は以下のような煩雑なコードの繰り返しとなっているため, 手で書くのは非常に面倒です.

image.png

そこで今回はこれを T4 を利用して作成してみます.

イメージとしては, 以下のようなコードが生成されるのが今回のゴールです.

namespace T4TemplatePractice
{
    delegate void MyAction<in T>(T t);
    delegate void MyAction<in T1, in T2>(T1 t1, T2 t2);
    //
    // Similar codes...
    //
}

T4テキストテンプレート

1. T4テキストテンプレート をプロジェクトに追加する

プロジェクト を右クリックして, 新しい項目の追加 から テキストテンプレート を追加します.
今回は MyActionTemplate.tt という名称で追加しました.

image.png

無事追加できればプロジェクト以下にファイルが追加されると思います.

image.png

2. T4テキストテンプレート を編集する

1. outputファイルを .cs ファイルにする

MyActionTemplate.tt を開くと以下のようになっていると思います.

MyActionTemplate.tt(修正前)
<#@ 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=".txt" #>

この最終行の output extension の値を変更することで, 出力時の拡張子を変更することができます.

今回はもちろん .cs とします.

MyActionTemplate.tt(修正後)
<#@ 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" #>  // <-- ここを修正

また, 今回は利用しませんが System.IO などをテンプレート作成時に利用したい場合, import namespace を追加してあげます.

MyActionTemplate.tt(System.IO追加サンプル)
// ================================================================
// 以下はあくまでもサンプルなので, コードを追加する必要は一切ありません
// ================================================================
<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.IO" #>  // <-- ここらへんに追加
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>

修正した状態で上書き保存をすると, 以下のように MyActionTemplate.cs.tt に紐づく形で追加されると思います.
もし, 紐づく形で表示されない場合は, 一度 MyActionTemplate.cs を削除したあとに, MyActionTemplate.tt を上書き保存をすると正しく表示されると思います.

image.png

2. テンプレート内で利用する変数・メソッドを定義する

テンプレート内では定義されている 変数メソッド を利用することが可能です.

これらは利用したいときに定義可能ですが, 情報が散乱すると後でテンプレートを修正するときに泣きをみるので, おとなしく一まとめにしておきましょう.

今回は最上部で一括定義することにします.

定義する際には コントロールブロック と呼ばれる <# ~ #> で囲まれた中に C# コードを記述していきます.

MyActionTemplate.tt(変数・メソッド定義の追加)
<#@ 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 startNumber = 1;

    // 型パラメータの最大番号
    var maxNumber = 16;

    // 型パラメータ生成用メソッド
    string CreateGenericTypes(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"in T{i}"));

    // パラメータ生成用メソッド
    string CreateParameters(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"T{i} t{i}"));

#> // <--- ここまで追加 

3. テンプレートを記述する

それでは, 実際に出力するテンプレートを記述していきます.

と言っても大部分は通常の .cs と同様に記述ができます.

試しに, 以下のような普通のコードを記述し, 上書き保存してみましょう.

MyActionTemplate.tt
<#@ 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 startNumber = 1;

    // 型パラメータの最大番号
    var maxNumber = 16;

    // 型パラメータ生成用メソッド
    string CreateGenericTypes(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"in T{i}"));

    // パラメータ生成用メソッド
    string CreateParameters(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"T{i} t{i}"));
#>
// ここから追加 --->
namespace T4TemplatePractice
{
    public class Foo
    {
        public int Bar { get; set; }
    }
}
// <--- ここまで追加

そうすると, MyActionTemplate.cs に以下のようにそのままC#のコードが出力されていると思います.

MyActionTemplate.cs
namespace T4TemplatePractice
{
    public class Foo
    {
        public int Bar { get; set; }
    }
}

この普通の C#コードT4テンプレート用の記述方法 を組み合わせることで, MyAction を作成します.

それではまず 1~16 までの値をループするためのコードを追加します.
これには先ほどの コントロールブロック を利用します.

MyActionTemplate.tt
// (中略)
<#
    // 型パラメータの開始番号
    var startNumber = 1;

    // 型パラメータの最大番号
    var maxNumber = 16;

    // 型パラメータ生成用メソッド
    string CreateGenericTypes(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"in T{i}"));

    // パラメータ生成用メソッド
    string CreateParameters(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"T{i} t{i}"));
#>
// ここから追加 --->
namespace T4Practice
{
// 以下のコントロールブロック内でも, 上の方のコントロールブロックで定義した変数・メソッドを使える
<# for (int i = startNumber; i <= maxNumber ; i++) { #>
    // do something
<# } #>
}
// <--- ここまで追加

コントロールブロックを利用することで, ブロック内に C# の forループ を記述することができます.
もちろん foreachループ や if文 などの制御構文を利用することもできます.

} もコントロールブロックで囲む必要があることに注意してください.

では, do something の位置に, 実際に MyAction を定義してみます.

image.png

MyActionTemplate.tt(コピペ用)
// (中略)
<#
    // 型パラメータの開始番号
    var startNumber = 1;

    // 型パラメータの最大番号
    var maxNumber = 16;

    // 型パラメータ生成用メソッド
    string CreateGenericTypes(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"in T{i}"));

    // パラメータ生成用メソッド
    string CreateParameters(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"T{i} t{i}"));
#>
namespace T4Practice
{
<# for (int i = startNumber; i <= maxNumber ; i++) { #>
// ここから追加 --->
    delegate void MyAction<<#= CreateGenericTypes(startNumber, i) #>>(<#= CreateParameters(startNumber, i) #>);
// <--- ここまで追加
<# } #>
}

型パラメータ と パラメータ を作成するためにそれぞれメソッドを利用しています.
コントロールブロック では メソッドの結果を利用することができない ため, ここでは 式コントロールブロック と呼ばれる <#= ~ #> で囲まれた式の結果を文字列として出力する機能を利用しています.

上記のような変更を加えてから上書き保存をすることで, MyActionTemplate.cs は以下のようになっていると思います.

MyActionTemplate.cs
namespace T4TemplatePractice
{
    delegate void MyAction<in T1>(T1 t1);
    delegate void MyAction<in T1,in T2>(T1 t1,T2 t2);
    delegate void MyAction<in T1,in T2,in T3>(T1 t1,T2 t2,T3 t3);
    delegate void MyAction<in T1,in T2,in T3,in T4>(T1 t1,T2 t2,T3 t3,T4 t4);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13,T14 t14);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13,T14 t14,T15 t15);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13,T14 t14,T15 t15,T16 t16);
}

これで引数を必要とする MyAction は定義できましたが, 引数がいらない MyAction は定義できていないので, 最後に一行差し込んでおきます.

MyActionTemplate.tt
// (中略)
<#
    // 型パラメータの開始番号
    var startNumber = 1;

    // 型パラメータの最大番号
    var maxNumber = 16;

    // 型パラメータ生成用メソッド
    string CreateGenericTypes(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"in T{i}"));

    // パラメータ生成用メソッド
    string CreateParameters(int start, int count)
        => string.Join(",", Enumerable.Range(start, count).Select(i => $"T{i} t{i}"));
#>
namespace T4Practice
{
// ここから追加 --->
    delegate void MyAction();
// <--- ここまで追加
<# for (int i = startNumber; i <= maxNumber ; i++) { #>
    delegate void MyAction<<#= CreateGenericTypes(startNumber, i) #>>(<#= CreateParameters(startNumber, i) #>);
<# } #>
}

for文の外に追記していることに注意してください.
上記の変更を加えてから上書き保存をすることで, MyActionTemplate.cs は以下のようになっていると思います.

MyActionTemplate.cs
namespace T4TemplatePractice
{
    delegate void MyAction();
    delegate void MyAction<in T1>(T1 t1);
    delegate void MyAction<in T1,in T2>(T1 t1,T2 t2);
    delegate void MyAction<in T1,in T2,in T3>(T1 t1,T2 t2,T3 t3);
    delegate void MyAction<in T1,in T2,in T3,in T4>(T1 t1,T2 t2,T3 t3,T4 t4);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13,T14 t14);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13,T14 t14,T15 t15);
    delegate void MyAction<in T1,in T2,in T3,in T4,in T5,in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>(T1 t1,T2 t2,T3 t3,T4 t4,T5 t5,T6 t6,T7 t7,T8 t8,T9 t9,T10 t10,T11 t11,T12 t12,T13 t13,T14 t14,T15 t15,T16 t16);
}

これで MyAction の完成です!


おわりに

今回紹介した機能は T4テキストテンプレート機能 のごくごく一部のものだけです.
もっと詳しく知りたい方は, 公式リファレンス を熟読することをオススメします.

14
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
8