【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) のSerializeFieldを方法ですがクラス外から使用したい場合は、プロパティやメソッドを追加実装する必要があって少々面倒です。

[SerializeField] float _value2;

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

(3) はクラス外でインスペクターの値を読み書きしたい場合、手軽で使いやすいように見えます。が、なるべく使わないほうがいいです。

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

その理由は以下の3つ

  • 名前を変更した時に簡単に値が引き継げない(できるがやや壁がある)
  • インスペクター上の名前の表示規則が標準と微妙に異なる
  • そのほか細かい事

理由(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' はこの宣言型では無効です。
// 'フィールド' 宣言でのみ有効です。

で回避方法ですが、FormerlySerializedAs の先頭に field: キーワードを付けて以下のように指定します。

// ★先頭にfield: を追加 + 見た目と異なる文字を指定する必要があり
[field: FormerlySerializedAs("<Sample>k__BackingField")]
[field: SerializeField] public float Sample { get; private  set; }

で、何で見た目と異なる文字列を指定する必要があるのかと言うと、プロパティって実際は以下のように展開されているからなんですよね。

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

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

// set/getが自動で追加(自分ではこれは書けない
public float Sample
{
    [CompilerGenerated]
    get => return <Sample>k__BackingField;
    
    [CompilerGenerated]
    private set => <Sample>k__BackingField = value;
}

つまり、上記を考慮すると内部変数名を直接指定するために、「"k__BackingField"」という名前を指定する必要があります。

で、何が言いたいかと言うと内部の private な SerializeField は変数はまぁいいですが、外部に公開してるプロパティで名前を修正したい時にこのような小難しいルールが存在していると、変更したくなくなりますよね?

不正確な名前つけちゃったとか、後から意味が変わって変更したい、みたいな局面で。

その時、上記のルールを考慮して引継ぎ属性を付けて名前を調べて、、、ってやって、やっとプロパティ変えた後に、Editor上でインスペクター値が引き継げたかどうかのチェックまでしないといけません。

リファクタボタンで一発変更OKと比べると、どうしても触りたくないみたいな判断になりがち、というか実際危険性があるので避けたいです。すると、プロジェクトで時間が経つと、どんどんそういうのが積み重なって技術的負債として積みありちょっと厳しいことになります。

外部公開のプロパティくらい自由に名前を変更させてくれよと、、、あと、Odinみたいなツールの検証とも食い合わせが悪いのでまぁいいことがほとんどないというのが体感です。

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

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

// インスペクター上には「_sample」と表示される
[field: SerializeField] public float _sample { get; private  set; }


// インスペクター上には「Sample」と表示される
[SerializeField] public float _sample;

となりフィールドの時と若干挙動が異なります(先頭のアンダースコアを取ってくれない、先頭文字を大文字に自動でしてくれない)ため、なんかフィールドの命名ルールと違うなぁと、ほんの少しだけ気持ち悪くなれます。

理由(3):そのほか細かい事

細かい事がもいくつかあって

(1) setterがあるのにインスペクター上から設定されるとsetterの処理が無視される

(2) SerializeField の変数設定の独特の挙動がプロパティに現れる

(3) このバッキングフィールドの変数名がランタイムのバージョンアップなどで変わると Unity 上でインスペクターから設定している値が全て消滅するので多分すごい事になると思います(ただし、さすがに今更そのような破壊的変更が言語自体に入ることは通常あり得ないとは思いますが心のどこかでひっかかりが残ります)

まとめ

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

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

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

// 設定が必要な場合以下の通り
[SerializeField] float _value3;
public float Value3 { get => _value3; set => _value3 = value; }

以上です

関連記事

takap-tech.com