Unity+Firestoreでenum変換とNullableな値の変換をする

こんにちは。[もぐめっと](https://mogmet.com)です。
![IMG_8947.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/42234/f7fa48ce-ef04-724a-a591-f6979cad4338.jpeg)
仕事をするときはちゃんとスーツを着るタイプです。

今回は豪華二本立てのunity tipsです。

**!!Attention!!**
前提条件として、nullableを使うためにC#8が使えるunity2020.2（2020/10/08現在、まだベータ版）を使って説明してます。
それ以前は適宜[CanBeNull]とかに置き換えてもらえればきっとできるんじゃないかと思います。（未検証）


## Firestoreのenum変換

unityでいい感じにenum変換するにはConverterTypeをかます必要があります。

例えば人狼ゲームで、役職によってチームが人狼と村人チームと別れている。。。みたいなのを表現するとします。
その役職チームはRoleTeamとして表現して、string型なenumでWerewolf, Villageと2チームに表現します。
下記のように表現します

```c#:Role.cs
[FirestoreData]
public struct Role
{
    [FirestoreDocumentId] public DocumentReference document { get; set; }
    [FirestoreProperty] public Timestamp createdAt { get; set; }
    [FirestoreProperty] public string roleName { get; set; }
    [FirestoreProperty] public RoleTeam team { get; set; } // Enumで定義
}
    
[FirestoreData(ConverterType = typeof(FirestoreEnumNameConverter<RoleTeam>))]
public enum RoleTeam { Werewolf, Village }

```

はい、ここで出てきました。Mrコンバーターこと[FirestoreEnumNameConverter](https://firebase.google.com/docs/reference/unity/class/firebase/firestore/firestore-enum-name-converter-t-?hl=ja_JP)君です。
これを使ってあげることによってDocumentSnapshotにある**ConvertTo<T>**メソッドでenumが入っていたとしても簡単にRole型のデータを取得することができるようになります。
では実際にどうやってDecodeしているかの例を見てみましょう。

```c#
public async UniTask<Role> GetVillagerRole()
{
    var snapshot = await firestore.Collection("roles").Document("villager").GetSnapshotAsync();
    return snapshot.ConvertTo<Role>(ServerTimestampBehavior.Estimate);
}
```

とても簡単にDecodeできちゃいますね。
基本はConvertToで変換できるようにするのが一番楽です。

でもnullableがはいってるとまだ対応してねーっすって怒られちゃうんです・・・
おじさん悲しい・・・

## Nullableなデータの変換をする

たとえばこのRoleにnullableな値として、占い師などは占う能力(ability)をもっていますが、村人は何も能力を持ちません。このように役職ごとに値があったりなかったりするとしましょう。
こんな感じで新しく定義してみました。

```c#:Role.cs(v2)
[FirestoreData]
public struct Role
{
    [FirestoreDocumentId] public DocumentReference document { get; set; }
    [FirestoreProperty] public Timestamp createdAt { get; set; }
    [FirestoreProperty] public string roleName { get; set; }
    [FirestoreProperty] public RoleTeam team { get; set; } // Enumで定義
    [FirestoreProperty] public int? ability { get; set; } // nullable
}
```

どうしたもんかと悩んでいたらこんな記事を見つけました。

[【Unity×Cloud Firestore(Firebase)】UnityにおけるFirestoreへの書き込み＆読み込み](https://qiita.com/NWaka_1415/items/ac3d06ef8c350ba53160)

上記からヒントを得て、こんなExtensionを作ってみました。

```c#ConvertExtension.cs
public static class ConvertExtension
{
    public static T? ConvertNullable<T>(object data) where T : struct
    {
        try
        {
            return (T) Convert.ChangeType(data, typeof(T));
        }
        catch
        {
            return null;
        }
    }
}
```

EnumもDecodeできるようにExtensionを作りました

```c#:EnumExtension.cs
public static class EnumExtension
{
    public static bool TryParse<TEnum>(string s, out TEnum enumValue) where TEnum : struct
    {
        return Enum.TryParse(s, out enumValue) && Enum.IsDefined(typeof(TEnum), enumValue);
    }
}

```

そしてDocumentSnapshotからDecodeするメソッドを作っていました。

```c#:Role.cs(v3)
[FirestoreData]
public struct Role
{
    [FirestoreDocumentId] public DocumentReference document { get; set; }
    [FirestoreProperty] public Timestamp createdAt { get; set; }
    [FirestoreProperty] public string roleName { get; set; }
    [FirestoreProperty] public RoleTeam team { get; set; } // Enumで定義
    [FirestoreProperty] public int? ability { get; set; } // nullable
    [FirestoreProperty] public int[]? voices { get; set; } // Arrayなnullable

    public static Role fromSnapshot(DocumentSnapshot snapshot)
    {
        var data = snapshot.ToDictionary(ServerTimestampBehavior.Estimate);
        EnumExtension.TryParse(data[nameof(team)].ToString(), out RoleTeam outTeam); // EnumのDecode。outTeamという変数にdecodeする。TryParseの返り値がfalseならthrowしたほうがいいかも。
        return new Role()
        {
            document = snapshot.Reference,
            createdAt = (Timestamp) Convert.ChangeType(data[nameof(createdAt)], typeof(Timestamp)),
            roleName = data[nameof(roleName)].ToString(),
            team = outTeam,
            ability = ConvertExtension.ConvertNullable<int>(data[nameof(ability)]) // Extension使ってNullableな値にコンバート
        }
    }
}

```

これで無事にnullableでもdecodeできるようになりました！！

ただ、Mapで入れ子状にデータを入れることもあると思います。
例えば占い師が占ったプレイヤー情報をRoleに格納するとしましょう。
この場合はまた色々と変換処理をかまさないとできません。
まず、このようなinterfaceを準備します。

```c#:IDictionaryConvertible.cs
public interface IDictionaryConvertible<T>
{
    T fromDictionary(Dictionary<string, object> dictionary);
}
```

そしてMapなデータをコンバートできるようにExtensionを拡張します

```c#:ConvertExtension.cs(v2)
public static class ConvertExtension
{
    public static T? ConvertNullable<T>(object data) where T : struct
    {
        try
        {
            return (T) Convert.ChangeType(data, typeof(T));
        }
        catch
        {
            return null;
        }
    }
    
    /// MapからDecodeする
    public static T ChangeType<T>(object data) where T : IDictionaryConvertible<T>, new()
    {
        var obj = (Dictionary<string, object>) Convert.ChangeType(data, typeof(Dictionary<string, object>));
        return new T().fromDictionary(obj);
    }
}
```

Player情報はこんな感じで定義しておきます。

```c#:Player.cs
public struct Player: IDictionaryConvertible<PlayerInfo>
{
    [FirestoreProperty] public string username { get; set; }

    public static Player fromDictionary(Dictionary<string, object> dictionary)
    {
        return new PlayerInfo()
        {
            username = dictionary[nameof(username)].ToString()
        };
    }
}
```

このPlayer情報をRoleでDecodeできるような感じにすると下記のようになります！

```c#:Role.cs(v4)
[FirestoreData]
public struct Role
{
    [FirestoreDocumentId] public DocumentReference document { get; set; }
    [FirestoreProperty] public Timestamp createdAt { get; set; }
    [FirestoreProperty] public string roleName { get; set; }
    [FirestoreProperty] public RoleTeam team { get; set; } // Enumで定義
    [FirestoreProperty] public int? ability { get; set; } // nullable
    [FirestoreProperty] public int[]? voices { get; set; } // Arrayなnullable
    [FirestoreProperty] public Player? player { get; set; } // nullableなPlayer

    public static Role fromSnapshot(DocumentSnapshot snapshot)
    {
        var data = snapshot.ToDictionary(ServerTimestampBehavior.Estimate);
        EnumExtension.TryParse(data[nameof(team)].ToString(), out RoleTeam outTeam);
        return new Role()
        {
            document = snapshot.Reference,
            createdAt = (Timestamp) Convert.ChangeType(data[nameof(createdAt)], typeof(Timestamp)),
            roleName = data[nameof(roleName)].ToString(),
            team = outTeam,
            ability = ConvertExtension.ConvertNullable<int>(data[nameof(ability)]),
            player = ConvertExtension.ChangeType<Player>(data[nameof(player)]) // nullableなMapデータをPlayerに変換する
        }
    }
}
```

これでいい感じにnullableを扱えるようになりました！！


## 所感

unityで、nullableは最近ようやく対応してきたばかりでこんな苦労をしていますが、firestoreでのnullableもいずれ対応してくれるようになると非常に嬉しいですね。

また、nullableなstringのコンバートでいい感じの方法がわからないのでもし誰かわかる方いらっしゃったらコメント欄とかでひっそりと教えて下さい。

## おまけ

同じ感じの要領でMapデータのArray版もつくってみたので良かったら使ってみてください

```c#
    public static List<T>? ConvertNullableArray<T>(object data) where T: IDictionaryConvertible<T>, new()
    {
        try
        {
            var list = (List<object>) Convert.ChangeType(data, typeof(List<object>));
            return list.Select(obj =>
            {
                var dict = (Dictionary<string, object>) Convert.ChangeType(obj, typeof(Dictionary<string, object>));
                return new T().fromDictionary(dict);
            }).ToList();
        }
        catch(Exception exception)
        {
            Debug.LogError(exception);
            return null;
        }
    }
```

## 宣伝

[ワンナイト人狼のアプリ](http://werewolf.mogmet.com/)作ってます！よかったら遊んでみてね！