MemoryPackでUniRxのReactive〇〇をシリアライズする

# はじめに
**MemoryPack** で UniRx の Reactive〇〇 のような **ジェネリック型** をシリアライズする方法を備忘録として残しておきます。
UniRxの `ReactiveProperty`, `ReactiveCollection`, `ReactiveDictionary` を例に出しますが、基本的にはどのジェネリック型にも対応できると思います。他のライブラリのクラスなど、自分では変更できないようなクラス向けです

環境は以下の通りです。
- Unity2021.3.17
- MemoryPack1.9.12

本記事の内容の実装とテストはこちら https://github.com/euglenach/MemoryPackSerializeRxTest

Zennに他にも色々なクラスをまとめてます(https://zenn.dev/euglenach/scraps/324467a4a4cfdd)

# MemoryPackとは
[MemoryPackのGithub](https://github.com/Cysharp/MemoryPack)や[neuecc先生のブログ](https://neue.cc/2022/11/04_memorypack.html)を見ることをオススメします。

# ジェネリック型の外部クラスをシリアライズする

## ReactiveProperty

MemoryPackのReadmeの [serialize-external-types](https://github.com/Cysharp/MemoryPack#serialize-external-types) にしたがってシリアライズ対応していきます。

<details><summary>ラッパーとフォーマッターのコード全文</summary><div>

```csharp:ReactivePropertyFormatter.cs
#nullable enable
using MemoryPack;
using UniRx;

[MemoryPackable]
public readonly partial struct SerializableReactiveProperty<T>
{
    [MemoryPackIgnore] public readonly ReactiveProperty<T> ReactiveProperty;
    [MemoryPackInclude] private T Value => ReactiveProperty.Value;

    [MemoryPackConstructor]
    private SerializableReactiveProperty(T value)
    {
        ReactiveProperty = new(value);
    }

    public SerializableReactiveProperty(ReactiveProperty<T> reactiveProperty)
    {
        this.ReactiveProperty = reactiveProperty;
    }
}

public class ReactivePropertyFormatter<T> : MemoryPackFormatter<ReactiveProperty<T>>
{
    public override void Serialize(ref MemoryPackWriter writer, ref ReactiveProperty<T>? value)
    {
        if (value == null)
        {
            writer.WriteNullObjectHeader();
            return;
        }

        writer.WritePackable(new SerializableReactiveProperty<T>(value));
    }

    public override void Deserialize(ref MemoryPackReader reader, ref ReactiveProperty<T>? value)
    {
        if (reader.PeekIsNull())
        {
            value = null;
            reader.Advance(1);
            return;
        }
    
        var wrapped = reader.ReadPackable<SerializableReactiveProperty<T>>();
        value = wrapped.ReactiveProperty;
    }
}
```
</div></details>

内部にReactivePropertyを持ち、`ReactiveProperty.Value` だけシリアライズ対象にする MemoryPackableな構造体 を定義しました。カスタムフォーマッターではそれをもとにシリアライズさせます。

## ReactiveCollection

<details><summary>フォーマッターのコード全文</summary><div>

```csharp:ReactiveCollectionFormatter.cs
#nullable enable
using MemoryPack;
using UniRx;

public class ReactiveCollectionFormatter<TElement> : MemoryPackFormatter<ReactiveCollection<TElement>>
{
    public override void Serialize(ref MemoryPackWriter writer, ref ReactiveCollection<TElement>? value)
    {
        if (value == null)
        {
            writer.WriteNullCollectionHeader();
            return;
        }

        var formatter = writer.GetFormatter<TElement?>();

        writer.WriteCollectionHeader(value.Count);
        foreach (var item in value)
        {
            var v = item;
            formatter.Serialize(ref writer, ref v);
        }
    }

    public override void Deserialize(ref MemoryPackReader reader, ref ReactiveCollection<TElement>? value)
    {
        if (!reader.TryReadCollectionHeader(out var length))
        {
            value = default;
            return;
        }

        var formatter = reader.GetFormatter<TElement?>();

        var collection = new ReactiveCollection<TElement>();
        for (var i = 0; i < length; i++)
        {
            TElement? v = default;
            formatter.Deserialize(ref reader, ref v);
            collection.Add(v);
        }

        value = collection;
    }
}
```
</div></details>

ReactiveCollection は [GenericCollectionFormatter](https://github.com/Cysharp/MemoryPack/blob/main/src/MemoryPack.Unity/Assets/Plugins/MemoryPack/Runtime/MemoryPack.Core/Formatters/GenericCollectionFormatters.cs#L16) の実装を丸パクリしています。

## ReactiveDictionary

<details><summary>フォーマッターのコード全文</summary><div>

```csharp:ReactiveDictionaryFormatter.cs
#nullable enable
using MemoryPack.Formatters;
using UniRx;

public class ReactiveDictionaryFormatter<TKey, TValue> : GenericDictionaryFormatterBase<ReactiveDictionary<TKey, TValue>,TKey, TValue>
    where TKey : notnull
{
    protected override ReactiveDictionary<TKey, TValue> CreateDictionary()
    {
        return new();
    }
}
```

</div></details>

`ReadtiveDictionary` のような `IDictionary<TKey, TValue>` は `GenericDictionaryFormatterBase`クラス を継承したカスタムフォーマッターを作ると実装がとても楽です。

他にも `ISet<T>`向けの `GenericSetFormatterBase` があります。 (なぜ GenericCollectionFormatterBase は無いのだろうか...)

# MemoryPackFormatterProviderに登録する

これらジェネリック型は `MemoryPackFormatterProvider.RegisterGenericType()` で登録します。 

```csharp
MemoryPackFormatterProvider.RegisterGenericType(typeof(ReactiveProperty<>), typeof(ReactivePropertyFormatter<>));
MemoryPackFormatterProvider.RegisterGenericType(typeof(ReactiveCollection<>), typeof(ReactiveCollectionFormatter<>));
MemoryPackFormatterProvider.RegisterGenericType(typeof(ReactiveDictionary<,>), typeof(ReactiveDictionaryFormatter<,>));
```
**第1引数にシリアライズ対象の型** 、**第2引数にフォーマッターの型** を渡します。

## IL2CPPで死ぬかも
`RegisterGenericType` は**IL2CPP**環境だと、コンパイル時に必要な型を認識できない可能性があるようです。
なので愚直に使う型を全て列挙する必要があります。

```csharp:IL2CPPならこっち
MemoryPackFormatterProvider.Register(new ReactivePropertyFormatter<string>());
MemoryPackFormatterProvider.Register(new ReactivePropertyFormatter<int>());
MemoryPackFormatterProvider.Register(new ReactivePropertyFormatter<float>());
MemoryPackFormatterProvider.RegisterCollection<ReactiveCollection<int>, int>();
MemoryPackFormatterProvider.RegisterCollection<ReactiveCollection<float>, float>();
MemoryPackFormatterProvider.RegisterCollection<ReactiveCollection<string>, string>();

// etc...
```

# おわりに

ここ１週間くらい MemoryPack をちょこちょこと触っていました。
MemoryPack は割と新しいライブラリで情報があまりありません。その中で手探りで見つけた方法なので **MemoryPackの作法として正しくない** 可能性があります。間違い等ありましたらコメントやTwitterで連絡いただけると幸いです。