C#9.0 SourceGeneratorでReadonly構造体を生成するGeneratorを作ってみました。


こちらの記事を拝見し、
[C# 9.0で加わったC# Source Generatorと、それで作ったValueObjectGeneratorの紹介](https://qiita.com/RyotaMurohoshi/items/83775cf4ed1ce4f6378d)

私も練習がてらに何か作ってみようかな、ということで作ってみました！

## 作ったもの
### ReadonlyStructGenerator
構造体の宣言で必要なプロパティだけ記述すると、以下のコードを自動生成するGeneratorです。
（[Record型の構造体版](https://ufcpp.net/blog/2020/10/pickuproslynrecord10/)のようなものをイメージしました）

- 構造体に readonly 修飾子を付加
- プロパティを初期化するコンストラクタの追加
- `IEquatable<T>` の実装
- `object.Equals()`, `object.GetHashCode()` and `object.ToString()`のオーバーライド
- 演算子 (`==`, `!=`)のオーバーロード

ソースコード
https://github.com/pierre3/ReadonlyStructGenerator

#### 使いかた

1. partial キーワードを付けて構造体を宣言します
2. 構造体に`ReadonlyStructGenerator.ReadonlyStructAttribute`属性を付与します。
3. initアクセサーを付けたプロパティを追加します。

```cs
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Point
{
    public int X { get; init; }
    public int Y { get; init; }
}
```

ビルドすると以下のコードが自動生成されます。

```cs
#nullable enable
using System;
namespace SampleConsoleApp
{
    readonly partial struct Point : IEquatable<Point>
    {
        public Point(int x,int y) => (X,Y) = (x,y);
        public bool Equals(Point other) => (X,Y) == (other.X,other.Y);
        public override bool Equals(object? obj) => (obj is Point other) && Equals(other);
        //注意）HashCode.Combine() のオーバーロードが引数8つまでしか対応していないので
        //プロパティの数が9個以上あるとエラーになってしまいます…
        public override int GetHashCode() => HashCode.Combine(X,Y);
        public static bool operator ==(Point left, Point right) => left.Equals(right);
        public static bool operator !=(Point left, Point right) => !(left == right);
        public override string ToString() => $"{nameof(Point)}({X},{Y})";
    }
}
```

- initアクセサーを付けたプロパティのみがコンストラクタによる初期化や、同値比較の対象となります(get only なプロパティは対象外)。
- また、自前でコンストラクタを述した場合コンストラクタは自動生成の対象外となります。




```cs
[ReadonlyStructGenerator.ReadonlyStruct]
public partial struct Vector3
{
    public float X { get; init; }
    public float Y { get; init; }
    public float Z { get; init; }

    public float Norm { get; }    //自動生成対象外

    //コンストラクタをこちらで定義した場合自動生成対象外
    public Vector3(float x, float y, float z)
    {
        (X, Y, Z) = (x, y, z);
        Norm = (float)Math.Sqrt(X * X + Y * Y + Z * Z);
    }
}
```

```cs
#nullable enable
using System;
namespace SampleConsoleApp
{
    readonly partial struct Vector3 : IEquatable<Vector3>
    {
        public bool Equals(Vector3 other) => (X,Y,Z) == (other.X,other.Y,other.Z);
        public override bool Equals(object? obj) => (obj is Vector3 other) && Equals(other);
        public override int GetHashCode() => HashCode.Combine(X,Y,Z);
        public static bool operator ==(Vector3 left, Vector3 right) => left.Equals(right);
        public static bool operator !=(Vector3 left, Vector3 right) => !(left == right);
        public override string ToString() => $"{nameof(Vector3)}({X},{Y},{Z})";
    }
}
```

## Source Generator を作る際の注意点

### Source Generator のプロジェクトの構成
Visual Studio でビルドして使う場合は、Visual Studio 自身が.NetFrameworkで動いているため、ターゲットフレームワークを`netstandard2.0`としないと動作してくれません。
また、Microsoft.CodeAnalysis系のライブラリも下記バージョンでないと動作しませんでした。（2020/12/6現在）

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  <LangVersion>preview</LangVersion>
  </PropertyGroup>
  <ItemGroup>
      <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0-5.final" PrivateAssets="all" />
      <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.1" PrivateAssets="all" />
  </ItemGroup>
</Project>
``` 
ただし、dotnet コマンドでビルドする分には.NET5.0でも問題なく動作します。
（手元の環境で試したところ、`.NET5.0`, `Microsoft.CodeAnalysis.CSharp v3.8.0`, `Microsoft.CodeAnalysis.Analyzers v3.3.2` で動作しました。）

### Generatorを利用する側の参照設定
利用側のプロジェクトでは、Generatorをアナライザーとして参照する必要があります。
（ProjectReferenceに `OutputItemType="Analyzer" ReferenceOutputAssembly="false"` を付与）

```xml
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\ReadonlyStructGenerator\ReadonlyStructGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
  </ItemGroup>
</Project>
```

### デバッグ
開発中は、C#のシンタックスツリーとかセマンティックモデルだとか使い慣れないオブジェクトを触るため、トレースデバッグは必須です。
Source GeneratorではInitializeメソッドに下記コードを記述することでトレース実行が可能となります。
（実行時ではなくビルド時に`System.Diagnostics.Debugger.Launch();`の行でブレイクされます。）

```cs

public void Initialize(GeneratorInitializationContext context)
{
    System.Diagnostics.Debugger.Launch();
}
```

### 何かおかしかったら再起動
更新したコードが反映されないだとか、コードは正しいのにエラーが解消されないだとか挙動がおかしくなった場合はVisual Studioを再起動すると直ることがあります。

## 感想
Source Generatorを作ってみた感想ですが、ドキュメントやサンプルが少ないのと動きが安定していないということもあって、現時点では複雑な機能を作るのはしんどそうな気がします。
が、この機能自体はいろいろ面白いことができそうなので、何か思いついたらまたチャレンジしてみたいと思います。