ValueObjectGenerator
この投稿では、C# 9.0で加わったC# Source Generator
とそれを使って開発したValueObjectGenerator
を紹介します。
コードはこちら!
背景
次のProduct
クラスは、2つのint型のプロパティProductId
とProductCategoryId
を持っています。
public class Product
{
public Product(string name, int productId, int productCategoryId)
{
Name = name;
ProductId = productId;
ProductCategoryId = productCategoryId;
}
public string Name { get; }
public int ProductId { get; }
public int ProductCategoryId { get; }
}
この型のインスタンスの利用シーンにおいて、いくつかの場所ではProductId
が必要で、他の場所ではProductCategoryId
が必要でしょう。どちらもint型で名前が似ているので、うっかりとProductId
とProductCategoryId
を取り違えてしまうかもしれません.
この取り違え型を防ぐにはどうしたらいいでしょうか?一つの方法としては、次のようなProductId
型とCategoryId
型を作り、それらを利用することです。これらの型を利用することで、コンパイラはProductId
プロパティとProductCategoryId
プロパティの取り違えを検出し、プログラム上のミスを防ぐことができます。このようにValueObjectクラス(もしくは、Wrapperクラス)を作成し利用することで、int型やstring型のプロパティの取り違えやミスを防ぐことができます。
ProductId
型とCategoryId
型は次のような感じになります。
public sealed class ProductId: IEquatable<ProductId>
{
public int Value { get; }
public ProductId(int value)
{
Value = value;
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is ProductId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(ProductId left, ProductId right) => Equals(left, right);
public static bool operator !=(ProductId left, ProductId right) => !Equals(left, right);
public bool Equals(ProductId other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Value == other.Value;
}
public static explicit operator ProductId(int value) => new ProductId(value);
public static explicit operator int(ProductId value) => value.Value;
}
public class CategroyId: IEquatable<CategroyId>
{
public int Value { get; }
public CategroyId(int value)
{
Value = value;
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is CategroyId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(CategroyId left, CategroyId right) => Equals(left, right);
public static bool operator !=(CategroyId left, CategroyId right) => !Equals(left, right);
public bool Equals(CategroyId other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Value == other.Value;
}
public static explicit operator CategroyId(int value) => new CategroyId(value);
public static explicit operator int(CategroyId value) => value.Value;
}
このProductId
型とCategoryId
型を使った、Product
型はこんな感じ。
public class Product
{
public Product(string name, ProductId productId, CategroyId productCategoryId)
{
Name = name;
ProductId = productId;
ProductCategoryId = productCategoryId;
}
public string Name { get; }
public ProductId ProductId { get; }
public CategroyId ProductCategoryId { 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
をつかうことで、ボイラープレートのコードは非常に簡単になります。
他、参考。
- Introducing C# Source Generators
- New C# Source Generator Samples
- Source Generators Cookbook
- C# 9.0の新機能 コード ジェネレーターのサポート
使い方
さてそんなC# Source Generator
を使って、私が開発した、ValueObjectGenerator
を紹介します。
次のように IntValueObject
属性をクラスに付与します。
using ValueObjectGenerator;
[IntValueObject]
public partial class ProductId
{
}
そうすると次のようなコードが生成されます。
public partial class ProductId: IEquatable<ProductId>
{
public int Value { get; }
public ProductId(int value)
{
Value = value;
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is ProductId other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(ProductId left, ProductId right) => Equals(left, right);
public static bool operator !=(ProductId left, ProductId right) => !Equals(left, right);
public bool Equals(ProductId other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Value == other.Value;
}
public static explicit operator ProductId(int value) => new ProductId(value);
public static explicit operator int(ProductId value) => value.Value;
}
ProductId
は、ValueObjectクラス(もしくは、Wrapperクラス)です。
次のように、ValueObjectクラス(もしくは、Wrapperクラス)を定義するためにあった、大量のボイラープレートコードはなくなりました。
[IntValueObject]
public class ProductId { }
[IntValueObject]
public class CategoryId { }
public class Product
{
public Product(string name, ProductId productId, CategoryId productCategoryId)
{
Name = name;
ProductId = productId;
ProductCategoryId = productCategoryId;
}
public string Name { get; }
public ProductId ProductId { get; }
public CategoryId ProductCategoryId { get; }
}
こんなふうに、ValueObjectGenerator
を使えば、 ValueObjectクラス(もしくは、Wrapperクラス)の生成に必要な大量のボイラープレートコードを排除することが可能です。
ValueObjectGeneratorの機能
ValueObjectGeneratorの機能の紹介をします。
- サポートするValue型
- クラスと構造体のサポート
- プロパティの名前指定
サポートするValue型
ValueObjectGenerator
は、int型以外のValueObjectクラス(もしくは、Wrapperクラス)をサポートしています。
[StringValueObject]
public partial class ProductName
{
}
public sealed partial class ProductName : System.IEquatable<ProductName>
{
public string Value { get; }
public ProductName(string value)
{
Value = value;
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is ProductName other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(ProductName left, ProductName right) => Equals(left, right);
public static bool operator !=(ProductName left, ProductName right) => !Equals(left, right);
public bool Equals(ProductName other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Value == other.Value;
}
public static explicit operator ProductName(string value) => new ProductName(value);
public static explicit operator string(ProductName value) => value.Value;
}
次のテープルは、提供している属性とそれに対応する型を示しています。
属性 | 型 |
---|---|
StringValueObject | string |
IntValueObject | int |
LongValueObject | long |
FloatValueObject | float |
DoubleValueObject | double |
クラスと構造体のサポート
ValueObjectGenerator
は、クラスと構造体、両方の生成のサポートをします。
まずは、クラス利用例。
[IntValueObject]
public partial class IntClassSample
{
}
クラスの場合、次のようなコードが生成されます。
public sealed partial class IntClassSample : System.IEquatable<IntClassSample>
{
public int Value { get; }
public IntClassSample(int value)
{
Value = value;
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is IntClassSample other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(IntClassSample left, IntClassSample right) => Equals(left, right);
public static bool operator !=(IntClassSample left, IntClassSample right) => !Equals(left, right);
public bool Equals(IntClassSample other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return Value == other.Value;
}
public static explicit operator IntClassSample(int value) => new IntClassSample(value);
public static explicit operator int(IntClassSample value) => value.Value;
}
次に、構造体の利用例。
[IntValueObject]
public partial struct IntStructSample
{
}
構造体の場合、次のようなコードが生成されます。
public partial struct IntStructSample : System.IEquatable<IntStructSample>
{
public int Value { get; }
public IntStructSample(int value)
{
Value = value;
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is IntStructSample other && Equals(other);
public override int GetHashCode() => Value.GetHashCode();
public override string ToString() => Value.ToString();
public static bool operator ==(IntStructSample left, IntStructSample right) => Equals(left, right);
public static bool operator !=(IntStructSample left, IntStructSample right) => !Equals(left, right);
public bool Equals(IntStructSample other)
{
return Value == other.Value;
}
public static explicit operator IntStructSample(int value) => new IntStructSample(value);
public static explicit operator int(IntStructSample value) => value.Value;
}
プロパティの名前指定
生成するValueObjectクラス(もしくは、Wrapperクラス)が持つ、プロパティの名前も指定することができます。
次のようにすることで、
[StringValueObject(PropertyName = "StringValue")]
public partial class CustomizedPropertyName
{
}
次のような型が生成されます。
public sealed partial class CustomizedPropertyName : System.IEquatable<CustomizedPropertyName>
{
public string StringValue { get; }
public CustomizedPropertyName(string value)
{
StringValue = value;
}
public override bool Equals(object obj) => ReferenceEquals(this, obj) || obj is CustomizedPropertyName other && Equals(other);
public override int GetHashCode() => StringValue.GetHashCode();
public override string ToString() => StringValue.ToString();
public static bool operator ==(CustomizedPropertyName left, CustomizedPropertyName right) => Equals(left, right);
public static bool operator !=(CustomizedPropertyName left, CustomizedPropertyName right) => !Equals(left, right);
public bool Equals(CustomizedPropertyName other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return StringValue == other.StringValue;
}
public static explicit operator CustomizedPropertyName(string value) => new CustomizedPropertyName(value);
public static explicit operator string(CustomizedPropertyName value) => value.StringValue;
}
今後の改善案
やりたいなと思っているのは、
- IComparableのサポート
- JSON serializer/deserializer
- 他のValueタイプ
- 算術演算のサポート
などです。
あと以前、ufcppさんのYoutube配信に、お邪魔した時に、ufcppとゲストのxin9leさんに、このValueObjectGenerator
を紹介した時に、いろいろご指摘をいただきました。その番組の指摘のおかげで
- string interpolationを使うとパフォーマンスが悪いので、パフォーマンスもよくする
- 余分なメンバを持っていたらコンパイルエラーにするアナライザー
も入れたほうがいいことがわかりました。ufcppさん、xin9leさん、ありがとうございます。
もしかしたら、recordでいいかも?
先に紹介した、ufcppさんのYoutube配信番組で、「1要素Recordとそんなに変わらない」という指摘もいただきました。
public record ProductId(int Value) { }
いや、もうその通りなんですよね。
ただ、Recordではできないものがあります。それは、「オーバーライドを認めないという」ものです。Recordではできないのでそれがメリットになります。と、ufcppさんとxin9leさんにご指摘いただきました!
ありがとうございます!
まとめ
この投稿では、C# 9.0で加わったC# Source Generator
とそれを使って開発したValueObjectGenerator
を紹介しました。
現在、ValueObjectGeneratorは開発・改善途中です。「便利じゃん!」「使いたい!」という方は、ぜひGitHubでStarをください!励ましになります!
ご意見、ご要望があれば、GitHubのissueか、Twitterの@RyotaMurohoshiまで!