Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9571

C# 9.0で加わったC# Source Generatorと、それで作ったValueObjectGeneratorの紹介

$
0
0

ValueObjectGenerator

この投稿では、C# 9.0で加わったC# Source Generatorとそれを使って開発したValueObjectGeneratorを紹介します。

コードはこちら!

https://github.com/RyotaMurohoshi/ValueObjectGenerator

背景

次のProductクラスは、2つのint型のプロパティProductIdProductCategoryIdを持っています。

publicclassProduct{publicProduct(stringname,intproductId,intproductCategoryId){Name=name;ProductId=productId;ProductCategoryId=productCategoryId;}publicstringName{get;}publicintProductId{get;}publicintProductCategoryId{get;}}

この型のインスタンスの利用シーンにおいて、いくつかの場所ではProductIdが必要で、他の場所ではProductCategoryIdが必要でしょう。どちらもint型で名前が似ているので、うっかりとProductIdProductCategoryIdを取り違えてしまうかもしれません.

この取り違え型を防ぐにはどうしたらいいでしょうか?一つの方法としては、次のようなProductId型とCategoryId型を作り、それらを利用することです。これらの型を利用することで、コンパイラはProductIdプロパティとProductCategoryIdプロパティの取り違えを検出し、プログラム上のミスを防ぐことができます。このようにValueObjectクラス(もしくは、Wrapperクラス)を作成し利用することで、int型やstring型のプロパティの取り違えやミスを防ぐことができます。

ProductId型とCategoryId型は次のような感じになります。

publicsealedclassProductId:IEquatable<ProductId>{publicintValue{get;}publicProductId(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisProductIdother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(ProductIdleft,ProductIdright)=>Equals(left,right);publicstaticbooloperator!=(ProductIdleft,ProductIdright)=>!Equals(left,right);publicboolEquals(ProductIdother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorProductId(intvalue)=>newProductId(value);publicstaticexplicitoperatorint(ProductIdvalue)=>value.Value;}publicclassCategroyId:IEquatable<CategroyId>{publicintValue{get;}publicCategroyId(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisCategroyIdother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(CategroyIdleft,CategroyIdright)=>Equals(left,right);publicstaticbooloperator!=(CategroyIdleft,CategroyIdright)=>!Equals(left,right);publicboolEquals(CategroyIdother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorCategroyId(intvalue)=>newCategroyId(value);publicstaticexplicitoperatorint(CategroyIdvalue)=>value.Value;}

このProductId型とCategoryId型を使った、Product型はこんな感じ。

publicclassProduct{publicProduct(stringname,ProductIdproductId,CategroyIdproductCategoryId){Name=name;ProductId=productId;ProductCategoryId=productCategoryId;}publicstringName{get;}publicProductIdProductId{get;}publicCategroyIdProductCategoryId{get;}}

このようにValueObjectクラス(もしくは、Wrapperクラス)を作成し利用することでコンパイルエラーを防ぐことができます。よかった、よかった!けれど、これらのコードは非常に大量のボイラープレートで溢れていますね。長いですね!ProductId型とCategoryId型。長すぎます!これらのボイラープレートコードは、他の大切な意味のあるコードを読む際にとても邪魔です。

さて、こんなふうなボイラープレートコードには、C# 9.0から加わったC# Source Generatorが大活躍します。

C# Source Generatorはこんなの

C# Source Generatorは、ビルド時にC#のソースコードを生成する仕組みです。

  • メインのプロジェクトがビルドされる前にコード生成
  • コード生成するために必要な入力値はコンパイル時に必要
  • 出力結果は、プロジェクトの一部となる
  • IDEにおいて、生成したコードの宣言にジャンプもできる
  • ILではなくC#を生成するので、デバックがすごい楽
  • 既存のソースコードを上書きしたりけしたりすることはできない

このC# Source Generatorを使うことで、プログラマティカルにコード生成をすることができます。C# Source Generatorをつかうことで、ボイラープレートのコードは非常に簡単になります。

他、参考。

使い方

さてそんなC# Source Generatorを使って、私が開発した、ValueObjectGeneratorを紹介します。

次のように IntValueObject属性をクラスに付与します。

usingValueObjectGenerator;[IntValueObject]publicpartialclassProductId{}

そうすると次のようなコードが生成されます。

publicpartialclassProductId:IEquatable<ProductId>{publicintValue{get;}publicProductId(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisProductIdother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(ProductIdleft,ProductIdright)=>Equals(left,right);publicstaticbooloperator!=(ProductIdleft,ProductIdright)=>!Equals(left,right);publicboolEquals(ProductIdother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorProductId(intvalue)=>newProductId(value);publicstaticexplicitoperatorint(ProductIdvalue)=>value.Value;}

ProductIdは、ValueObjectクラス(もしくは、Wrapperクラス)です。

次のように、ValueObjectクラス(もしくは、Wrapperクラス)を定義するためにあった、大量のボイラープレートコードはなくなりました。

[IntValueObject]publicclassProductId{}[IntValueObject]publicclassCategoryId{}publicclassProduct{publicProduct(stringname,ProductIdproductId,CategoryIdproductCategoryId){Name=name;ProductId=productId;ProductCategoryId=productCategoryId;}publicstringName{get;}publicProductIdProductId{get;}publicCategoryIdProductCategoryId{get;}}

こんなふうに、ValueObjectGeneratorを使えば、 ValueObjectクラス(もしくは、Wrapperクラス)の生成に必要な大量のボイラープレートコードを排除することが可能です。

ValueObjectGeneratorの機能

ValueObjectGeneratorの機能の紹介をします。

  • サポートするValue型
  • クラスと構造体のサポート
  • プロパティの名前指定

サポートするValue型

ValueObjectGeneratorは、int型以外のValueObjectクラス(もしくは、Wrapperクラス)をサポートしています。

[StringValueObject]publicpartialclassProductName{}publicsealedpartialclassProductName:System.IEquatable<ProductName>{publicstringValue{get;}publicProductName(stringvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisProductNameother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(ProductNameleft,ProductNameright)=>Equals(left,right);publicstaticbooloperator!=(ProductNameleft,ProductNameright)=>!Equals(left,right);publicboolEquals(ProductNameother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorProductName(stringvalue)=>newProductName(value);publicstaticexplicitoperatorstring(ProductNamevalue)=>value.Value;}

次のテープルは、提供している属性とそれに対応する型を示しています。

属性
StringValueObjectstring
IntValueObjectint
LongValueObjectlong
FloatValueObjectfloat
DoubleValueObjectdouble

クラスと構造体のサポート

ValueObjectGeneratorは、クラスと構造体、両方の生成のサポートをします。

まずは、クラス利用例。

[IntValueObject]publicpartialclassIntClassSample{}

クラスの場合、次のようなコードが生成されます。

publicsealedpartialclassIntClassSample:System.IEquatable<IntClassSample>{publicintValue{get;}publicIntClassSample(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisIntClassSampleother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(IntClassSampleleft,IntClassSampleright)=>Equals(left,right);publicstaticbooloperator!=(IntClassSampleleft,IntClassSampleright)=>!Equals(left,right);publicboolEquals(IntClassSampleother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnValue==other.Value;}publicstaticexplicitoperatorIntClassSample(intvalue)=>newIntClassSample(value);publicstaticexplicitoperatorint(IntClassSamplevalue)=>value.Value;}

次に、構造体の利用例。

[IntValueObject]publicpartialstructIntStructSample{}

構造体の場合、次のようなコードが生成されます。

publicpartialstructIntStructSample:System.IEquatable<IntStructSample>{publicintValue{get;}publicIntStructSample(intvalue){Value=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisIntStructSampleother&&Equals(other);publicoverrideintGetHashCode()=>Value.GetHashCode();publicoverridestringToString()=>Value.ToString();publicstaticbooloperator==(IntStructSampleleft,IntStructSampleright)=>Equals(left,right);publicstaticbooloperator!=(IntStructSampleleft,IntStructSampleright)=>!Equals(left,right);publicboolEquals(IntStructSampleother){returnValue==other.Value;}publicstaticexplicitoperatorIntStructSample(intvalue)=>newIntStructSample(value);publicstaticexplicitoperatorint(IntStructSamplevalue)=>value.Value;}

プロパティの名前指定

生成するValueObjectクラス(もしくは、Wrapperクラス)が持つ、プロパティの名前も指定することができます。

次のようにすることで、

[StringValueObject(PropertyName="StringValue")]publicpartialclassCustomizedPropertyName{}

次のような型が生成されます。

publicsealedpartialclassCustomizedPropertyName:System.IEquatable<CustomizedPropertyName>{publicstringStringValue{get;}publicCustomizedPropertyName(stringvalue){StringValue=value;}publicoverrideboolEquals(objectobj)=>ReferenceEquals(this,obj)||objisCustomizedPropertyNameother&&Equals(other);publicoverrideintGetHashCode()=>StringValue.GetHashCode();publicoverridestringToString()=>StringValue.ToString();publicstaticbooloperator==(CustomizedPropertyNameleft,CustomizedPropertyNameright)=>Equals(left,right);publicstaticbooloperator!=(CustomizedPropertyNameleft,CustomizedPropertyNameright)=>!Equals(left,right);publicboolEquals(CustomizedPropertyNameother){if(ReferenceEquals(null,other))returnfalse;if(ReferenceEquals(this,other))returntrue;returnStringValue==other.StringValue;}publicstaticexplicitoperatorCustomizedPropertyName(stringvalue)=>newCustomizedPropertyName(value);publicstaticexplicitoperatorstring(CustomizedPropertyNamevalue)=>value.StringValue;}

今後の改善案

やりたいなと思っているのは、

  • IComparableのサポート
  • JSON serializer/deserializer
  • 他のValueタイプ
  • 算術演算のサポート

などです。

あと以前、ufcppさんのYoutube配信に、お邪魔した時に、ufcppとゲストのxin9leさんに、このValueObjectGeneratorを紹介した時に、いろいろご指摘をいただきました。その番組の指摘のおかげで

  • string interpolationを使うとパフォーマンスが悪いので、パフォーマンスもよくする
  • 余分なメンバを持っていたらコンパイルエラーにするアナライザー

も入れたほうがいいことがわかりました。ufcppさん、xin9leさん、ありがとうございます。

もしかしたら、recordでいいかも?

先に紹介した、ufcppさんのYoutube配信番組で、「1要素Recordとそんなに変わらない」という指摘もいただきました。

publicrecordProductId(intValue){}

いや、もうその通りなんですよね。

ただ、Recordではできないものがあります。それは、「オーバーライドを認めないという」ものです。Recordではできないのでそれがメリットになります。と、ufcppさんとxin9leさんにご指摘いただきました!

ありがとうございます!

まとめ

この投稿では、C# 9.0で加わったC# Source Generatorとそれを使って開発したValueObjectGeneratorを紹介しました。

https://github.com/RyotaMurohoshi/ValueObjectGenerator

現在、ValueObjectGeneratorは開発・改善途中です。「便利じゃん!」「使いたい!」という方は、ぜひGitHubでStarをください!励ましになります!

ご意見、ご要望があれば、GitHubのissueか、Twitterの@RyotaMurohoshiまで!


Viewing all articles
Browse latest Browse all 9571

Trending Articles