はなちるのマイノート

Unityをメインとした技術ブログ。自分らしくまったりやっていきたいと思いますー!

【C#】GenericのFullNameを実際のコードに埋め込みできるよう変換する方法(T4の実行時テンプレート, Raw String Literal)

はじめに

例えばRaw String Literal(もしくはT4の実行時テンプレート)を利用して、.csの生成をしようとします。

using System.Text;

namespace SampleConsole
{
    internal class Program
    {
        public static void Main()
        {
            // C#11より登場した生文字列リテラル (T4の実行時テンプレートの移行先としてしばしば利用される)
            // 開始の「""" (3個以上の ")」の後ろには改行必須, 1行目のインデントを基準にしてそれよりも前の空白文字は無視
            // 先頭の「$」の数によって補完する際「{, }」の数が変わります
            var sampleClass = $$"""
using SampleConsole;

public class SampleClass
{
    public static void Piyo()
    {
        var x = new {{typeof(Hoge<int>).FullName}}();
        x.Fuga();
    }    
}
""";
            
            // ファイル書き込み
            const string path = "/Users/.../SampleConsole.cs";
            using var writer = new StreamWriter(path, false, Encoding.UTF8);
            writer.Write(sampleClass);
        }
    }

    public class Hoge<T>
    {
        public void Fuga()
        {
            Console.WriteLine("Fuga");
        }
    }
}


ぱっと見いけそうな気がしますが、実はtypeof(Hoge<int>).FullNameは色々と付加情報が入ってしまいます。

// 生成されたファイル
using SampleConsole;

public class SampleClass
{
    public static void Piyo()
    {
        var x = new SampleConsole.Hoge`1[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]();
        x.Fuga();
    }    
}

ジェネリックがなければこのままでも動作するのですが、ジェネリックに対応するためには自身で変換する処理を書かなければなりません。

// 本来はこうなってほしい
using SampleConsole;

public class SampleClass
{
    public static void Fuga()
    {
        var x = new SampleConsole.Hoge<System.Int32>();
        x.Fuga();
    }    
}

その方法を紹介したいと思います。

解決方法

List<List<int>>みたいな場合もあるので、再帰呼び出しによって実装する必要があります。

private static string ToSourceType(Type t)
{
    if (t == null) return null;
    if (t.IsGenericType)
        return
            $"{t.FullName?.Substring(0, t.FullName.IndexOf('`'))}<{string.Join(", ", t.GenericTypeArguments.Select(ToSourceType))}>";
    return t.FullName;
}

実際のコード

冒頭のコードに適応してみます。

using System.Text;

namespace SampleConsole
{
    internal class Program
    {
        public static void Main()
        {
            var hoge = new Hoge<int>();

            // これだとNG
            // SampleConsole.Hoge`1[[System.Int32, System.Private.CoreLib, Version=7.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]]
            Console.WriteLine(typeof(Hoge<int>).FullName);

            // こうしたい
            // SampleConsole.Hoge<System.Int32>
            Console.WriteLine(ToSourceType(typeof(Hoge<int>)));

            // C#11より登場した生文字列リテラル (T4の実行時テンプレートの移行先としてしばしば利用される)
            // 開始の「""" (3個以上の ")」の後ろには改行必須, 1行目のインデントを基準にしてそれよりも前の空白文字は無視
            // 先頭の「$」の数によって補完する際「{, }」の数が変わります
            var sampleClass = $$"""
using SampleConsole;

public class SampleClass
{
    public static void Fuga()
    {
        var x = new {{ToSourceType(typeof(Hoge<int>))}}();
        x.Fuga();
    }    
}
""";
            
            // ファイル書き込み
            const string path = "/Users/hayato.sato/RiderProjects/SampleConsole/SampleConsole/SampleConsole.cs";
            using var writer = new StreamWriter(path, false, Encoding.UTF8);
            writer.Write(sampleClass);
        }

        private static string ToSourceType(Type t)
        {
            if (t == null) return null;
            if (t.IsGenericType)
                return
                    $"{t.FullName?.Substring(0, t.FullName.IndexOf('`'))}<{string.Join(", ", t.GenericTypeArguments.Select(ToSourceType))}>";
            return t.FullName;
        }
    }

    public class Hoge<T>
    {
        public void Fuga()
        {
            Console.WriteLine("Fuga");
        }
    }
}