Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
33
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

ValueObjectGenerator

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

コードはこちら!

背景

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

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型で名前が似ているので、うっかりとProductIdProductCategoryIdを取り違えてしまうかもしれません.

この取り違え型を防ぐにはどうしたらいいでしょうか?一つの方法としては、次のような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をつかうことで、ボイラープレートのコードは非常に簡単になります。

他、参考。

使い方

さてそんな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まで!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
33
Help us understand the problem. What are the problem?