【Unity】プロパティを[field:SerializeField]は避ける

プロパティに [field: SerializeField] を付けると最初は便利かもしれませんが、後々面倒なので避けましょうねと言う話になります。

初めに

Unity でインスペクター上から値を変更できるようにするには以下のように記述します。

// (1)publicで宣言する
public float _value1;

// (2)SerializeFieldを付ける
[SerializeField] private float _value2;
// privateは書かなくても良い
// [SerializeField] float _value2;

// (3)プロパティにSerializeFieldを付ける
[field: SerializeField] public float Value3 { get; private set; }

このうち(3)のプロパティに field: SerializeField を付けるのはやめたほうがいい(推奨)という話です。

各実装の確認・避けたほうがいい理由

まず各々の実装を見ていきます。

(1) のフィールド変数を public にするとインスペクターから編集できるようになる代わりにクラス外から変更された上に変更されたことも検出できないので一般的なプログラミングとして、やめたほうがいいです(特に複数人で操作が起きる場合)

// こうするとほかのクラスから無制限に値を変え放題になる
public float _value1;

(2) 方法ですがクラス外から参照 or 変更したい場合プロパティやメソッドを追加実装する必要があって少々面倒です

[SerializeField] float _value2;

public float Value2 => _value2; // プロパティを使って値が読み取れるようにする
// 読み書きしたいときは以下のようにする
public float Value2 { get => _value2; set => _value2 = value; }

(3) はクラス外でインスペクターの値を読み書きしたい場合都合がいいように見えます。

が、実際は使わないほうがいいです。

// 一見よさそうに見えるが、、、
[field: SerializeField] public float Value3 { get; private set; }

その理由は以下の2つです。

  • 通常は名前を変更した時に値が引き継げない
  • インスペクター上の名前の表示規則が標準と微妙に異なる

理由(1):名前を変更した時に値が引き継げない

特に名前を変更したいときにちょっと困ります。

[field: SerializeField] public float Value3 { get; private  set; }

// ★以下のように名前を変更したい
[field: SerializeField] public float Sample { get; private  set; }

この時、フィールドは FormerlySerializedAs("Value3") と書けば値を引き継ぐことができますが、プロパティの場合エラーが出て属性を付与することができません。

[FormerlySerializedAs("Value3")]
[field: SerializeField] public float Sample { get; private  set; }

// CS0592 属性 'FormerlySerializedAs' はこの宣言型では無効です。
// 'フィールド' 宣言でのみ有効です。

これは(やろうと思えばできるけど)一般的には回避する方法が存在しません。

理由(2):インスペクター表示が標準と異なる

(実際そのようなプロパティ名にするかはさておき)以下のように設定した場合、インスペクターに表示される変数名が通常とは異なる動作となります。

[field: SerializeField] public float _Sample { get; private  set; }
// インスペクター上には「_Sample」と表示される。
// フィールドだと「Sample」とアンダースコアをとってくれる

技術的な話

C# の細かい話になってしまいますがプロパティは宣言すると実際は以下のようにコードが展開されてます。

// こうやって宣言すると
[field: SerializeField] public float Value3 { get; private  set; }

// 実際はこんな感じに展開されている
[CompilerGenerated]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
private float <Value3>k__BackingField; // バッキングフィールドの自動生成という

public float Value3
{
    [CompilerGenerated]
    get => return <Value3>k__BackingField;
    
    [CompilerGenerated]
    private set => <Value3>k__BackingField = value;
}

つまり、上記を考慮すると内部変数名を直接指定し、以下のように記述すれば変更できるます(が、これも頻繁に発生すると手間だと思います)

// 内部的に自動生成されているフィールドの名前を直接指定すればエラーは出ず変更も可能
[field: FormerlySerializedAs("<Sample>k__BackingField")]
[field: SerializeField] public float Sample { get; private  set; }

で、Unity上ではこの <Value3>k_BackingField; から <> の中身の Value3 という文字列をインスペクターに表示 + 区切りにスペースの追加してくれるようです。 なので、プロパティ名を _Value3 とすると <_Value3> → 「_Value3」 という表示となり、期待するアンダースコアが除去されず、なんかフィールドの命名ルールと違うなぁと、ほんの少しだけ気持ち悪くなれます。

あと、リスクとしてこのバッキングフィールドの変数名がランタイムのバージョンアップなどで変わると Unity 上でインスペクターから設定している値がが全て消滅するので多分すごい大変なことになります。今更そのような破壊的変更が入ることは通常あり得ないとは思いますが、、、

まとめ

プロパティを通常の C# の実装だと考えて、安易に変更するとインスペクター上で設定していた値が消えてしまたり(Fieldでも状況は完全に同じではありますが)、値を引き継ごうにもやや手間がかかるため field: SerializeField はプロパティには使用しないほうがいいよと言う話でした。

従って、推奨事項は、インスペクター上に表示したいけどクラス外からは編集されたくない場合、多少面倒ですが以下のようにフィールドとプロパティを併用するのが良いと思います。

// クラス外からはValue2プロパティ経由で値を取得する
[SerializeField] float _value2;
public float Value2 => _value2;

以上です

関連記事

takap-tech.com