Unityのデシリアライズの不思議な挙動に悩まされた話

# はじめに
Unityで32種類までしか使えないレイヤーを実質無限にする拡張を作ってたときにデシリアライズの仕様でハマったことをメモ。

# 環境
* Unity2021.3.0f1
* Unity2021.3.11f1
* Unity2022.1.0f1

３つのUnityバージョンで試しましたが全部で起きたのでどうやら仕様っぽいです。

# Inspectorのバグみたいな挙動
まずは以下のクラス群を実装します。

```Layer.cs
using System;
using UnityEngine;

[Serializable]
public class Layer
{
    [SerializeField] private string _name;

    public Layer(string name)
    {
        _name = name ?? "";
    }
}
```

```BuiltinLayer.cs
public static class BuiltinLayer
{
    public static readonly Layer Default = new(nameof(Default));
    public static readonly Layer TransparentFX = new(nameof(TransparentFX));
    public static readonly Layer IgnoreRaycast = new("Ignore Raycast");
    public static readonly Layer Water = new(nameof(Water));
    public static readonly Layer UI = new(nameof(UI));
}
```

```LayerSettingsAsset.cs
using UnityEngine;

[CreateAssetMenu(menuName = "My ScriptableObjects/Layer Settings")]
public class LayerSettingsAsset : ScriptableObject
{
    [SerializeField] private Layer[] _layers =
    {
        BuiltinLayer.Default,
        BuiltinLayer.TransparentFX,
        BuiltinLayer.IgnoreRaycast,
        new(""),
        BuiltinLayer.Water,
        BuiltinLayer.UI,
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
        new(""),
    };
}
```

次に `Assets` → `Create` → `My ScriptableObjects` → `Layer Settings` でアセットを２つ作ります。

それらの中身はこんな感じにします。

```Layer Settings 1.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 00dcba1fefff445680661ee99e49dafa, type: 3}
  m_Name: Layer Settings 1
  m_EditorClassIdentifier: 
  _layers:
  - _name: 1
  - _name: TransparentFX
  - _name: Ignore Raycast
  - _name: 1
  - _name: Water
  - _name: UI
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
```

```Layer Settings 2.asset
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
  m_ObjectHideFlags: 0
  m_CorrespondingSourceObject: {fileID: 0}
  m_PrefabInstance: {fileID: 0}
  m_PrefabAsset: {fileID: 0}
  m_GameObject: {fileID: 0}
  m_Enabled: 1
  m_EditorHideFlags: 0
  m_Script: {fileID: 11500000, guid: 00dcba1fefff445680661ee99e49dafa, type: 3}
  m_Name: Layer Settings 2
  m_EditorClassIdentifier: 
  _layers:
  - _name: 2
  - _name: TransparentFX
  - _name: Ignore Raycast
  - _name: 2
  - _name: Water
  - _name: UI
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
  - _name: 
```

`_layers[0]` と `_layers[3]` だけファイルごとに異なるデータにします。

↑この編集をしようとInspectorから値をいじってる最中にすでにおかしなことが起きるので、直接ファイルを開いて書き換えたほうがわかりやすいです。

これら２つのアセットを交互に選択すると、Inspector上では `_layers[0]` だけ変化しなくなります。[^1]

[^1]: このときは `_layers[0]` が `2` で固定されてしまいましたが、 `1` で固定される場合もあります（どうやら `InstanceID` の小さい方で固定されるっぽい）。


![Oct-14-2022 12-24-44.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/27863/1e385d09-3eb3-e94a-4357-5ee6bebc9303.gif)


この不思議な挙動はファイルをReimportしても直らず、Unityを再起動すると一時的にリセットされます。

最初は「 `_layers` のインスタンスが共有されてる？」と思ったのですが、 `_layers[3]` は正しく別の値になるので `_layers` のインスタンス自体が共有されてるわけではなさそうです。

# デシリアライズの挙動（推測）
この現象は以下の修正を加えると起きなくなります。

　(a) `Layer` クラスを構造体に変更する
　(b) `BuiltinLayer` を使わないようにする
　(c) 要素数を足したり減らしたりして32じゃない個数にする

ここまで書いたら勘のいい人ならわかると思いますが、 `_layers` の一部の要素が実際に共有されていると推測されます。

もう一度ソースコードを確認してみます。

```Layer.cs
using System;
using UnityEngine;

[Serializable]
public class Layer
{
    [SerializeField] private string _name;

    public Layer(string name)
    {
        _name = name ?? "";
    }
}
```

```BuiltinLayer.cs
public static class BuiltinLayer
{
    public static readonly Layer Default = new(nameof(Default));
    public static readonly Layer TransparentFX = new(nameof(TransparentFX));
    public static readonly Layer IgnoreRaycast = new("Ignore Raycast");
    public static readonly Layer Water = new(nameof(Water));
    public static readonly Layer UI = new(nameof(UI));
}
```

```LayerSettingsAsset.cs
using UnityEngine;

[CreateAssetMenu(menuName = "My ScriptableObjects/Layer Settings")]
public class LayerSettingsAsset : ScriptableObject
{
    [SerializeField] private Layer[] _layers =
    {
        BuiltinLayer.Default,
        BuiltinLayer.TransparentFX,
        BuiltinLayer.IgnoreRaycast,
        new(""),
        BuiltinLayer.Water,
        BuiltinLayer.UI,
...
```

`_layers[0]` が `_layers` のインスタンスに依らず常に `BuiltinLayer.Default` を参照している場合、Inspectorからの書き換えは他のインスタンスに影響します。

`_layers[3]` は `new` で個別に生成されてるので共有されておらず、他に影響がなかったわけです。

今回は `BuiltinLayer` のフィールドが `private` だったり　`readonly` をつけてたりして深く考えてなかったのですが、調査中はシリアライズ経由でインスタンスの中身が書き換えられることを完全に見落としてました。

　
＊　＊　＊　＊　＊
　

(a)と(b)の挙動でわかることは、Unityは(少なくとも配列は)以下の手順でデシリアライズを行うということです。

0. 宣言と同時の初期化が実装されていればまずそれを読み込む
0. シリアライズされた値があれば読み込み済みのインスタンスに対して値を適用する

クラスを構造体に変更したら起きなくなるのは構造体は都度コピーされて別モノになるからです。

ただ、(c)の挙動も合わせて考えると、以下のような場合分けになると推測されます。

　
* シリアライズされた配列の**要素数が宣言と同時の初期化と同じ**場合
    1. 宣言と同時の初期化が実装されていればまずそれを読み込む
    2. シリアライズされた値があれば読み込み済みのインスタンスに対して値を適用する
* シリアライズされた配列の**要素数が宣言と同時の初期化と異なる**場合
    1. 内部的にインスタンスを `new` する
    2. 読み込み済みのインスタンスに対してシリアライズされた値を適用する

　
もろもろを勘案すると、当初の `Layer` と `BuiltinLayer` の実装は

* `BuiltinLayer` を `[Serializable]` にしないよう変更する
    * `Layer` とは別のクラスを使う
* `BuiltinLayer.Default` などを `BuiltinLayer.Default.AsSerializable()` とかに変更する
    * シリアライズ可能なインスタンスが使い回されないようにする

とするのが良いかなと思いました。

# 雑感
謎の場合分けはパフォーマンスチューニングの一環なんだろうと思いつつそのせいで調査が難航したので、どっちかに統一してほしいなという気持ち。

気になるからフォーラムで中の人に聞いてみようかな。