LoginSignup
7
5

More than 5 years have passed since last update.

はじめてテキストテンプレート使ってみた

Last updated at Posted at 2017-12-07

手を出そうとしたきっかけ

C#でEnumの拡張メソッドのコメントで

(付加情報が)増えていくと記述が冗長になるので、ある程度のところでClassに切り分けたほうが良いのかもしれませんね。

そりゃまあ情報量が増えていくと「Enumにぶら下げる付随情報」というよりenumの方が「さまざまな情報を引っ張り出すための検索キー」みたいな立場になるわけで、とはいってもそのキーをクラスのプロパティの一部にしてしまうとenumとは別物で、インテリセンスなどで拾ってくれなくなる。
定義をたくさん並べるならキー:情報1,情報2,...みたいにまとめて定義したいけど、「定数の定義(enum)」と「各種プロパティの値」をまとめて定義するようなことはできない。
C++ならマクロを使って可能だけど、C#にはプリプロセッサがないからなー。
そういえばプリプロセッサのかわりにテキストテンプレートってのがあったな。使ったことないけど。

という思い付きでテキストテンプレートを調べたらできそうだからやってみたけど、やっぱり最初は思った通りにはいきませんでした。

ASPの経験があってよかった

文法や考え方はASPとよく似てます。
ブラウザに投げるHTMLなどのかわりにテキストファイルとして出力する。
定義ファイルを別に用意して、そのレコードから必要な情報を抜き出して「enumの宣言ブロック」と「各種プロパティの値を初期値としてセットしたクラスを作成するブロック」をそれぞれ出力すればいい。
雛型となるプレーンテキストの中でデータを挿入したい箇所に<#= 変数名 #>みたいに書けばその場所に変数の内容が入る。
ループやifなどの制御ステートメントはC#のコードで書ける。ASPそっくり。

テンプレート内で型定義はできないのね 1

あらかじめクラス定義を書いたdllを作ってインポートすることはできるみたいです。
でもソースコードを作成するためのテンプレートなので、そのプロジェクトのコンパイルの前にテンプレートからファイルを出力するわけで、そうなると別プロジェクトでdllを作ってからテキストテンプレートに読み込ませてファイル出力となる。

最初は定義ファイルをJSONで作ろうと思ったんですが、.NET標準のJSONデシリアライザを使うには読み込んだデータを格納するクラスの定義が必要。
サードパーティー製のパーサーを使って読み込めばできるのかもしれないけど、思い付きでやるには大げさすぎる。

あくまで練習用なので外部依存や複数のプロジェクトみたいなことはしないで、最小限の構成にしたい。(訳:めんどくせー)
というわけでできたのがこちらになります。

定義ファイル

AnimalTypeDefinitions.csv
Code: Dog, Name: イヌ, CryText: わん
Code: Cat, Name: ネコ, CryText: にゃー, AliasName: にゃんこ, AliasName: ぬこ
Code: Rabbit, Name: ウサギ

テンプレートファイル

AnimalTypeTemplate.tt
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
using System.Collections.Generic;
<#
List<string[]> atdList = new List<string[]>();
using (var sr = new System.IO.StreamReader(this.Host.ResolvePath("AnimalTypeDefinitions.csv")))
{
    string line;
    while((line = sr.ReadLine()) != null)
    {
        //Codeで始まるレコードだけ抽出
        if (line.StartsWith("Code:"))
        { atdList.Add(line.Split(',').Select(clm => clm.Trim()).ToArray()); }
    }
}
#>

namespace AnimalTypes
{
    public enum AnimalType
    {
<#
foreach(var atd in atdList) {
    string code = atd[0].Remove(0, "Code:".Length).Trim();
    if (!string.IsNullOrEmpty(code)) {
#>
        <#= code #>,
<#
    }
}
#>
    }

    public class AnimalTypeProperties {
        public string Name = "";
        public string CryText = "";
        public string[] AliasNames = new string[] {};
    }

    public static class Definitions
    {
        public static Dictionary<AnimalType, AnimalTypeProperties> AnimalTypeTable = new Dictionary<AnimalType, AnimalTypeProperties>
        {
<#
foreach(var atd in atdList) {
    string code = atd[0].Remove(0, "Code:".Length).Trim();
    string name = atd.Where(clm => clm.StartsWith("Name:")).Select(clm => clm.Remove(0, "Name:".Length).Trim()).FirstOrDefault() ?? "";
    string cry = atd.Where(clm => clm.StartsWith("CryText:")).Select(clm => clm.Remove(0, "CryText:".Length).Trim()).FirstOrDefault();
    var alias = atd.Where(clm => clm.StartsWith("AliasName:")).Select(clm => clm.Remove(0, "AliasName:".Length).Trim());
#>
            { AnimalType.<#= code #>, new AnimalTypeProperties() { Name = "<#= name #>"
<# if (!string.IsNullOrEmpty(cry)) {#>, CryText = "<#= cry #>"<# } #>

<# if (alias.Any()) { #>, AliasNames = new string[] { "<#= string.Join("\",\"", alias) #>" }<# } #>

            }},
<#
}
#>
        };

        //拡張メソッド
        public static AnimalTypeProperties GetProperties(this AnimalType code)
        {
            AnimalTypeTable.TryGetValue(code, out AnimalTypeProperties prop);
            return prop;
        }
        public static string GetTypeName(this AnimalType code) => code.GetProperties()?.Name ?? "";
        public static string GetCrytext(this AnimalType code) => code.GetProperties()?.CryText ?? "";
        public static string[] GetAliasNames(this AnimalType code) => code.GetProperties()?.AliasNames ?? new string[] {};
    }
}

テンプレートで作成されたソースコード

AnimalTypeTemplate.cs
using System.Collections.Generic;

namespace AnimalTypes
{
    public enum AnimalType
    {
        Dog,
        Cat,
        Rabbit,
    }

    public class AnimalTypeProperties {
        public string Name = "";
        public string CryText = "";
        public string[] AliasNames = new string[] {};
    }

    public static class Definitions
    {
        public static Dictionary<AnimalType, AnimalTypeProperties> AnimalTypeTable = new Dictionary<AnimalType, AnimalTypeProperties>
        {
            { AnimalType.Dog, new AnimalTypeProperties() { Name = "イヌ"
, CryText = "わん"

            }},
            { AnimalType.Cat, new AnimalTypeProperties() { Name = "ネコ"
, CryText = "にゃー"
, AliasNames = new string[] { "にゃんこ","ぬこ" }
            }},
            { AnimalType.Rabbit, new AnimalTypeProperties() { Name = "ウサギ"


            }},
        };

        //拡張メソッド
        public static AnimalTypeProperties GetProperties(this AnimalType code)
        {
            AnimalTypeTable.TryGetValue(code, out AnimalTypeProperties prop);
            return prop;
        }
        public static string GetTypeName(this AnimalType code) => code.GetProperties()?.Name ?? "";
        public static string GetCrytext(this AnimalType code) => code.GetProperties()?.CryText ?? "";
        public static string[] GetAliasNames(this AnimalType code) => code.GetProperties()?.AliasNames ?? new string[] {};
    }
}

ディクショナリなので使い方はわかると思うけど、この場合は静的変数Definitions.AnimalTypeTableとしているのでDefinitions.AnimalTypeTable[AnimalType.Dog].Nameのようにして取り出せます。

(追記)拡張メソッドも用意しよう

元ネタが「enumの拡張メソッド」だったから、拡張メソッドも用意してみました。
ついでに未定義だったらnullではなく空文字を返すなど若干の改良。


  1. <#+ ... #>という定義専用ブロック内で型宣言すればできるようです。@kitamin さんありがとうございます。 

7
5
2

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
7
5