15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Unity #3Advent Calendar 2020

Day 4

Unityで「シリアライズするフィールド」と「プロパティー」を簡潔に書きたくて、SourceGeneratorを作ってみた

Last updated at Posted at 2020-12-03

※ この投稿で紹介するSourceGeneratorは、まだUnityじゃ使えません。将来的にこうできればいいなーって話

さてまずは、C#のプロパティーの話をします。
C#では、フィールドをpublicにするのは良くなくて、プロパティーを使ってフィールドにアクセスするのが良いとされています。

↓のコードは良くなくて

public class Launcher {
    // publicなフィールドだからこれは良くない
    public float speed;
}

↓な感じでプロパティーにするのがGoodです。

public class Launcher {
    float speed;

    // ちょっと古い書き方のプロパティー
    public float Speed {
        get { return speed; } 
        set { speed = value; }
    }
}

さて、↑みたいなコードはわざわざフィールドを作らなくても、↓のような自動実装プロパティーで実現ができます。

public class Launcher {
    public float Speed { get; set; }
}

自動実装プロパティーを使うことで、C#のコードからは見えない次のようなフィールドとアクセッサが、"自動"で生成されます。

  • <Speed>k__BackingFieldというフィールド
  • get_Speedというメソッド
  • set_Speedというメソッド

自動実装プロパティー、便利ですね!


さて次はUnityのシリアライズとフィールドとプロパティーの話。

UnityにおいてMonoBehaviourのサブクラスやScriptableObjectのサブクラスなどで、次のようなpublicなフィールドに設定した値や参照は、シーンやプレファブにシリアライズされることがあります。

using UnityEngine;

public class Launcher : MonoBehaviour {
    // publicなフィールドだからこれは良くない
    public float speed;
}

ですが先にも説明したとおり、C#として↑のようなpublicなフィールドは良くなかったですね。
こういう場合は、SerializeField属性の出番です。SerializeField属性をつけたフィールドはpublicでなくても、シリアライズされます。

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField] float speed;
}

さて、このフィールドに外部からアクセスするために、プロパティーを付けてあげましょう。(とりあえずゲッタープロパティーだけ)

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField] float speed;
    public float Speed => speed;
}

うん、よくあるパターンです。 このUnityで「シリアライズするフィールド」と「プロパティー」というよくあるパターンをもっと簡潔にしたいな!というのがこの投稿の趣旨です。


先ほど説明した自動実装プロパティ、あれは使えないのでしょうか?

自動で実装される内部的なフィールド(バッキングフィールド)に、SerializeField属性をつけることができたら、実現できそうです。なんと、これ実現できます。

C# 7.3から自動実装プロパティの自動で実装される内部的なフィールド(バッキングフィールド)に属性を付与できるようになりました。

using UnityEngine;

public class Launcher : MonoBehaviour {
    [field:SerializeField]
    public float Speed => speed;
}

うん、簡潔!めでたしめでたし!
とは、ならないいんです・・・

自動実装プロパティのフィールド名は、「<Speed>k__BackingField」という名前です。困ったことに、この名前でシリアライズされてしまいます。次の画像みたいな感じで。

スクリーンショット 2020-12-04 1.06.25.png
スクリーンショット 2020-12-04 1.06.21.png

うーん、困った。


さて、ここでC# 9.0から加わったSourceGeneratorの出番です。(まだUnityじゃ使えません。将来的にこうできればいいなーって話)

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

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

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

さて、自分はC# Source Generatorをつかって、「フィールドに付与すると、そのフィールドのゲッタープロパティーを生成してくれるSource Generator」を作ってみました!

ソースコードはこちら! RyotaMurohoshi/PropertyGenerator

using System;
using PropertyGenerator;

public partial class Product
{
    [GetterProperty(PropertyName = "Identifier")]
    private readonly int id;

    [GetterProperty] private readonly string name;

    public Product(string name, int id)
    {
        this.name = name;
        this.id = id;
    }
}

↑みたいな感じで、idやnameというフィールドにGetterPropertyという属性をつけると、↓のようなプロパティが生成されます。

public partial class Product
{
    public int Identifier => this.id;
    public string Name => this.name;
}

このGetterPropertyを使って、次のようなボイラープレートなコードもC# Source Generator使ってすっきりさせてみましょう。

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField] float speed;
    public float Speed => speed;
}

↑が、↓こうなります!プロパティーがいらなくなりました!すっきり!

using UnityEngine;

public class Launcher : MonoBehaviour {
    [SerializeField, GetterProperty] float speed;
}

あんまり嬉しくない?確かに1行だとそうですね。

これが↓みたいにたくさんあったらどうでしょう?

using UnityEngine;

public class Monster : ScriptableObject {
    [SerializeField] int maxHp;
    public int MaxHp => maxHp;

    [SerializeField] int maxMp;
    public int MaxMp => maxHp;

    [SerializeField] int attack;
    public int Attack => attack;

    [SerializeField] int defense
    public int Defense => defense;

    [SerializeField] int speed
    public int Speed => speed;
}

これが↑、↓こうなる!すっきりしましたね!
ちなみに↑は、一部実装が間違っている箇所にきがつきましたか!↑はうっかりミスをしてます。↓ならこういううっかりミスも防げますね!

using UnityEngine;

public class Monster : ScriptableObject {
    [SerializeField, GetterProperty] int maxHp;
    [SerializeField, GetterProperty] int maxMp;
    [SerializeField, GetterProperty] int attack;
    [SerializeField, GetterProperty] int defense
    [SerializeField, GetterProperty] int speed
}

Unityで「シリアライズするフィールド」と「プロパティー」ですが、残念ながら自動実装プロパティはつかえません。そこで、Unityで「シリアライズするフィールド」と「プロパティー」を簡潔に書きたくて、SourceGeneratorを作ってみました。フィールドにつけるとゲッタープロパティーを生成するという非常に簡潔なものです!

よかったらコードを見てみてください! : RyotaMurohoshi/PropertyGenerator

これがUnityで使えるようになるのが楽しみです!

15
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?