T4でImmutableクラスコンストラクタ作成(C#)
C#(7.0)でImmutableクラスを作成する際、下記のような記述が必要になります。
public class Person { public string Name { get; } public int Age { get; } public Person(string name, int age) { Name = name; Age = age; } }
F#のレコードと異なり、コンストラクタを定義しないといけません。
今回はこのコンストラクタをT4で自動生成してみたいと思います。
Tangible T4
tangible T4 Editor for VS2010 / VS2012 / VS2013 / VS2015 / VS2017
素のVisual Studioでは、T4はハイライトや補間などが一切サポートされていません。
そこでこれらを有効にするためにTangible T4をインストールします。
有償版ではさらに、DTE関連のインテリセンスの制限が解除されたり、デバッグができるようになるようです。
C#側準備
ターゲットのクラス判別のため、Record属性を定義しています。
またターゲットのクラスはpartialにします。
public class RecordAttribute : Attribute { } [Record] public partial class Person { public string Name { get; } public int Age { get; } }
T4ファイル
DTEを用いて、csファイル内のクラスを解析していきます。
this.Hostを利用するため、hostspecificはtrueにします。
<#@ template debug="false" hostspecific="true" language="C#" #> <#@ assembly name="System.Core" #> <#@ assembly name="EnvDTE" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.IO" #> <#@ import namespace="EnvDTE" #> <#@ output extension=".cs" #> <# DTE dte = (this.Host as IServiceProvider).GetService(typeof(DTE)) as DTE; Project project = dte.Solution.FindProjectItem(this.Host.TemplateFile).ContainingProject; // T4ファイルが所属するプロジェクト #> <# foreach (CodeClass cls in GetClasses(project, "Record")) { string parameters = string.Join(", ", GetProperties(cls).Select(x => string.Format("{0} {1}", x.Type.AsString, x.Name.ToLower()))); #> namespace <#= cls.Namespace.FullName #> { public partial class <#= cls.Name #> { public <#= cls.Name #> (<#= parameters #>) { <# foreach (CodeProperty p in GetProperties(cls)) { #> <#= p.Name #> = <#= p.Name.ToLower() #>; <# } #> } } } <# } #> <#+ IEnumerable<T> Flatten<T>(IEnumerable<T> root, Func<T, IEnumerable<T>> getChildren) { // 木構造を配列に展開 return root.Concat(root.SelectMany(x => Flatten(getChildren(x), getChildren))); } IEnumerable<ProjectItem> GetCSFiles(Project project) { return Flatten(project.ProjectItems.Cast<ProjectItem>(), x => x.ProjectItems.Cast<ProjectItem>()) .Where(x => Path.GetExtension(x.Name) == ".cs"); } IEnumerable<CodeClass> GetClasses(ProjectItem item) { // 今回はnamespace直下のクラスのみ取得 return Flatten(item.FileCodeModel.CodeElements.Cast<CodeElement>(), x => x is CodeNamespace ? (x as CodeNamespace).Members.Cast<CodeElement>() : Enumerable.Empty<CodeElement>()) .OfType<CodeClass>(); } IEnumerable<CodeClass> GetClasses(Project project, string attribute) { return GetCSFiles(project) .SelectMany(GetClasses) .Where(c => c.Attributes.Cast<CodeAttribute>().Any(x => x.Name == attribute)); } IEnumerable<CodeProperty> GetProperties(CodeClass cls) { // { get; }なのか、{ get { return ユーザー定義 } }なのかの違いがCodePropertyから判別できないため、今回は文字列解析で判別 return cls.Members .OfType<CodeProperty>() .Where(x => x.StartPoint.CreateEditPoint().GetText(x.EndPoint).TrimEnd().EndsWith("{ get; }")); } #>
自動生成結果
上記T4により、下記のクラスが自動生成されます。
public partial class Person { public Person(string name, int age) { Name = name; Age = age; } }