JsonUtilityを使ってDictionaryを含むクラスをシリアライズする (unity)

# 前提
- unity 2018.4.2f1

# やりたいこと
- クラスを丸ごとセーブロードできるように、任意のクラスのシリアライズ/デシリアライズ(`string`との相互変換)を低コストで実現したい。

# これまでやっていたこと
<details><summary>オレオレJsonシリアライズの自前実装 (蛇足なので隠蔽)</summary><div>

### オレオレJsonシリアライズ
- オレオレJsonは、対になる括弧とセパレータをユニークにすることで、解析を簡単にしました。
    - `{1>"a":"あ"<1>"b":{2>"A":"ああ"<2>"B":"いい"<2}<1>"c":"う"<1}`みたいな感じです。
- 必要なクラス全てで以下を実装しました。
    - `ToString ()`をオーバーライドして、シリアライズしたいメンバーを`ToString ()`して連結する感じで、オレオレJsonを吐かせます。(下記コードを参照)
    - `string json`を受け取るコンストラクタで、メンバーの名前で要素を取り出しては`new`する感じで、オレオレJsonからインスタンスを再現します。
- また、ジェネリックなユーティリティ関数群を使って記述を簡素化し、`Dictionary`を含む様々な型にも対応させます。
- 使い勝手は悪くないのですが、`string`を取り回すせいで、大規模に使うとGC Allocが大量発生してよろしくないです。

### 使用時のイメージ
```cs:
public Preference () {
	this.version = CurrentVersion;
	this.CurrentSlot = 0;
	this.Continue = false;
	this.seVolume = 0.5f;
	this.smVolume = 0f;
}
public Preference (string json) : this () {
	if (!string.IsNullOrEmpty (json)) {
		if (json == "load") {
			json = PlayerPrefs.GetString (Prefkey);
		}
		this.version = json.JsToValue ("version", string.Empty);
		this.CurrentSlot = json.JsToValue (int.Parse, "CurrentSlot", 0);
		this.Continue = json.JsToValue (bool.Parse, "Continue", false);
		this.seVolume = json.JsToValue (float.Parse, "seVolume", 0.5f);
		this.smVolume = json.JsToValue (float.Parse, "smVolume", 0f);
	}
}

public override string ToString () {
	return (new [] {
		this.version.ValueToJs ("version"),
		this.CurrentSlot.ValueToJs ("CurrentSlot"),
		this.Continue.ValueToJs ("Continue"),
		this.seVolume.ValueToJs ("seVolume"),
		this.smVolume.ValueToJs ("smVolume"),
	}).Brackets ();
}
```

</div></details>

# やってみたこと

- `ISerializationCallbackReceiver `を使った簡易な方法を採用しました。
- シリアライズできない型とシリアライズ可能な型を相互に変換するだけで、シリアライズ自体は`JsonUtility`にさせています。


### コード
```cs:
using System;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

public class JsonUtilityDictionaryTest : MonoBehaviour {

	void Start () {
		var x = new ItemDict (new string [] { "a", "b", "c", "d", "e", "f" }); // 生成
		var y = JsonUtility.ToJson (x, true); // シリアライズ
		var z = JsonUtility.FromJson<ItemDict> (y); // デシリアライズ

		Debug.Log (x);
		Debug.Log (y);
		Debug.Log (z);
	}

}


[Serializable]
public class ItemDict : ISerializationCallbackReceiver {
	public int this [Item i] { get { return Items [i]; } }
	[NonSerialized] public Dictionary<Item, int> Items;
	[SerializeField] private List<Item> _keys;
	[SerializeField] private List<int> _values;

	public ItemDict (ICollection<string> names) {
		Items = new Dictionary<Item, int> { };
		foreach (var name in names) {
			Items.Add (new Item (name), Random.Range (0, 100));
		}
	}

	public void OnBeforeSerialize () {
		_keys = new List<Item> { };
		_values = new List<int> { };
		foreach (KeyValuePair<Item, int> keyvalue in Items) {
			_keys.Add (keyvalue.Key);
			_values.Add (keyvalue.Value);
		}
	}

	public void OnAfterDeserialize () {
		Items = new Dictionary<Item, int> { };
		for (var i = 0; i < _keys.Count; i++) {
			Items.Add (_keys [i], _values [i]);
		}
	}

	public override string ToString () {
		var items = new List<KeyValuePair<Item, int>> { };
		foreach (var item in Items) {
			items.Add (item);
		}
		return $"\"Items\":[ {string.Join (",", items.ConvertAll (item => $"{item.Key}:{item.Value}"))}]";
	}
}

[Serializable]
public class Item : ISerializationCallbackReceiver {

	[SerializeField] public string Name;
	[NonSerialized] public Guid Id;
	[SerializeField] private string _id;

	public Item (string name) {
		Id = Guid.NewGuid ();
		this.Name = name ?? Id.ToString ();
	}

	public void OnBeforeSerialize () {
		_id = Id.ToString ();
	}

	public void OnAfterDeserialize () {
		Id = Guid.Empty;
		Guid.TryParse (_id, out Id);
	}

	public override string ToString () {
		return $"{{\"name\":\"{Name}\", \"id\":\"{Id}\"}}";
	}

}
```

### 元のインスタンス
```json:
"Items":[ {"name":"a", "id":"83cc1660-fb42-4816-8852-61c2918b445f"}:40,{"name":"b", "id":"f667c4be-191b-4fcd-ba66-ccb4a5c0abde"}:72,{"name":"c", "id":"d48e1201-6c22-45e3-9394-7a7e205e67dc"}:39,{"name":"d", "id":"e31e2516-58e9-43e3-afed-b0292810dd40"}:31,{"name":"e", "id":"84f4ca56-9ccc-4a51-bf10-8ba7921d2bcc"}:82,{"name":"f", "id":"5019a795-6f7f-49d9-b3e2-3e89f936a2e3"}:13]
```

### シリアライズ結果
```json:
{
    "_keys": [
        {
            "Name": "a",
            "_id": "83cc1660-fb42-4816-8852-61c2918b445f"
        },
        {
            "Name": "b",
            "_id": "f667c4be-191b-4fcd-ba66-ccb4a5c0abde"
        },
        {
            "Name": "c",
            "_id": "d48e1201-6c22-45e3-9394-7a7e205e67dc"
        },
        {
            "Name": "d",
            "_id": "e31e2516-58e9-43e3-afed-b0292810dd40"
        },
        {
            "Name": "e",
            "_id": "84f4ca56-9ccc-4a51-bf10-8ba7921d2bcc"
        },
        {
            "Name": "f",
            "_id": "5019a795-6f7f-49d9-b3e2-3e89f936a2e3"
        }
    ],
    "_values": [
        40,
        72,
        39,
        31,
        82,
        13
    ]
}
```

### 復元されたインスタンス
```json:
"Items":[ {"name":"a", "id":"83cc1660-fb42-4816-8852-61c2918b445f"}:40,{"name":"b", "id":"f667c4be-191b-4fcd-ba66-ccb4a5c0abde"}:72,{"name":"c", "id":"d48e1201-6c22-45e3-9394-7a7e205e67dc"}:39,{"name":"d", "id":"e31e2516-58e9-43e3-afed-b0292810dd40"}:31,{"name":"e", "id":"84f4ca56-9ccc-4a51-bf10-8ba7921d2bcc"}:82,{"name":"f", "id":"5019a795-6f7f-49d9-b3e2-3e89f936a2e3"}:13]
```

# 分かったこと
- 実行の効率は良いのだと思いますが、記述が一般化できず、クラス毎に個別の処理を書かなければならないので、かなり面倒です。
- `List<T>`は型次第で、扱えたり扱えなかったりするようです。
    - `[SerializeField] private List<KeyValuePair<Item, int>> _keyvalues;`とかはダメでした。
- `Dictionary`はダメっぽいです。
- 執筆時点で、QiitaのCodeブロックのシンタックスハイライトは、文字列挿入(string interpolation)のエスケープに対応できていないようです。(`{{`、`}}`が認識されない)

# 参考
- [UnityのJsonUtilityの細かい10の疑問をいまさら検証した](https://qiita.com/keidroid/items/24e03f82d74560dc557a)

ありがとうございました。