koturnの日記

普通の人です.ブログ上のコードはコピペ自由です.

Unityでの変数・プロパティのアクセス性について考える

前置き

最近,Unityに触れており,C#を書くことが多い. 古いUnityの文化はわからないが,publicメンバ変数が気持ち悪くて仕方がない. (おそらく,古代のUnityではプロパティを使用できない,あるいは実行処理系がプロパティのインライン展開を行うことができなかったのではないかと思うが.... また,インスペクタ表示の兼ね合いもあったのかもしれない.)

普通のC#を書いてきた身としては,publicメンバ変数は用いず,代わりに(自動実装)プロパティを用いるべきであるという思いがある. なので,超初歩的な内容ではあるが,メンバ変数宣言とプロパティについてまとめることにした.

変数とプロパティのアクセス表

基本的に使用することがあるのは下記の表のものだと思う. T は適当な型を指す.

プロパティについては自動実装プロパティのみを記載している (public 変数 との対比であるので).

もし,private変数とプロパティを別々に記述する場合は,インスペクタ表示は変数の欄,アクセス性はプロパティの欄に従うことになる.

protectedinternal のような他のアクセス修飾子は些細な違いしかないので省略している. また,setだけのプロパティは普通作らないので,これも省略した.

No. 宣言 自クラスでの値取得 自クラスでの値設定 他クラスから値取得 他クラスから値設定 インスペクタ表示
1 public T foo;
2 public readonly T foo; △(※1) △(※2)
3 private T _foo;
4 private readonly T _foo; △(※1)
5 [SerializeField]
private _foo;
6 public T Foo { get; set; }
7 [field: SerializeField]
public T Foo { get; set; }
8 public T Foo { get; private set; }
9 [field: SerializeField]
public T Foo { get; private set; }
10 public T Foo { get; } △(※1)
11 [field: SerializeField]
public T Foo { get; }
△(※1)

No.1, No.2はpulbicメンバ変数なので使用すべきでない,No.11のgetオンリーなプロパティはたとえ SerializeField 属性を付与しようとも,バッキングフィールドが readonly であるため,インスペクタからは見えず,SerializeField の意味がないという点で使用すべきでない. なので,No.3~No.10の8つのうち,いずれかを使用すべきである.

※1:初期化時のみ値設定が可能. 例えば,コンストラクタ内や,

public class Foo
{
    private readonly int _hoge;

    public int Fuga { get; }

    Foo(int hoge, int fuga)
    {
        // これはOK
        _hoge = hoge;
        Fuga = fuga
    }

    // SetAndShow(int hoge, int fuga)
    // {
    //     // これはNG
    //     _hoge = hoge;
    //     Fuga = fuga;
    // }
}

宣言と同時に初期化では可能.

public class Foo
{
    private readonly int _hoge = 42;

    public int Fuga { get; } = 84;
}

MonoBehaviour 継承クラスにコンストラクタを実装することはないので,Unityではあまりreadonlyなメンバ変数とgetオンリーな自動実装プロパティを利用する機会は少ないかもしれない.

※2: 値型はunsafeな手段を用いれば書き換え可能.

class Program
{
    static void Main(string[] args)
    {
        var f = new Foo();
        Console.WriteLine(f.hoge);  // => 42
        unsafe
        {
            fixed (int* p = &f.hoge)
            {
                *p = 20;
            }
        }
        Console.WriteLine(f.hoge);  // => 20
    }
}

public class Foo
{
    public readonly int hoge = 42;
}

自動実装プロパティのインスペクタでの表示

自動実装プロパティの場合,バッキングフィールド名がそのままインスペクタに表示されてしまう. それが嫌なら,

ここにある RenameFieldAttribute のようなものを用いて,インスペクタの表示を操作してやるか,

[field: SerializeField]
[field: RenameField("Foo")]
public int Foo { get; set; }

自動実装プロパティをやめ,実態となる [SerializeField] private変数を別に用意し,それに対するプロパティを用意する必要がある.

/// <summary>
/// インスペクタからは見えるが,他クラスからは見えない
/// </summary>
[SerializeField]
private int _foo;

/// <summary>
/// 他クラスから<see cref="_foo"/>を見るためのプロパティ
/// </summary>
public int Foo
{
    get { return _foo; }
    set { _foo = value; }
}

UdonSharpについて

なお,現在のUdonSharpでは自作クラスにプロパティを使用することはできないし,そもそもpublicメンバ変数であっても普通にアクセスすることもできない. なので,おそらく裏でリフレクションを使用しているであろう手段でアクセスすることになるが,これはアクセス修飾子を無視してアクセスできるので,おとなしくpublicメンバ変数を用いるのがよいと思われる(そのうち普通にアクセスできるようになると思うし,変数publicだと他クラスでも nameof() で変数名を取得できるため,typoを実行時ではなくコンパイル時に検知できる).

まとめ

基本的にはprivate変数かpublicプロパティのどちらかの選択となり,あとはインスペクタ表示が必要かどうかで判断することになる.

  1. 外部クラスから見える必要があるか?
    • Yes: プロパティ
    • No: private変数
  2. インスペクタから操作できる必要があるか?(Unityに限る)
    • Yes: [SerializeField] を付ける
    • No: そのまま
  3. 値設定は初期化時のみに限定したい?
    • Yes: readonlyな変数 / getオンリーなプロパティ
    • No: そのまま

参考