【Unity】MessagePipeのSubscribe, Dispose処理について

## はじめに
以下の環境で実行しています
- MacOS
- Unity2021.2.0b2
- MessagePipe v1.6.1
- Zenject

前回のMessagePipeの記事

https://qiita.com/Toshizabeth/items/f4d429fe1ac566ed7924

こちらのMessagePipeのReadmeを元に学んだことを記事にまとめています

https://github.com/Cysharp/MessagePipe

## Subscribe, Dispose

MessagePipeはSubcribeでイベント受信（購読）を行います。

```cs
public class Hoge
{
	public Hoge()
	{
		// MyEventの通知を受け取る
		var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
		subscriber.Subscribe(ev =>
		{
			// 購読処理
		});
	}
}

```

Subscribeの戻り値は`IDisposable`
このIDisposableを適切に処理しなければ<b>リーク</b>が発生します。

つまり、<b>クラスが破棄されたとしてもイベント購読し続けることになる</b>
（バグが出る可能性が非常に高い！）

基本的には購読しているクラスの破棄と同時にイベントの購読も消したいはず。
今回はイベントの破棄を担当する`Dispose`について書いていきます。

## 非Monobehaviourクラスでイベントを破棄したい場合

```cs
public class Hoge
{
	private readonly IDisposable _disposable;

	public Hoge()
	{
		var bag = DisposableBag.CreateBuilder();
		
		// MyEventの通知を受け取る
		{
			var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
			subscriber.Subscribe(ev =>
			{
				// 購読処理
			}).AddTo(bag);
		}

		// stringの通知を受け取る
		{
			var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<string>();
			subscriber.Subscribe(ev =>
			{
				// 購読処理
			}).AddTo(bag);
		}

		_disposable = bag.Build();
	}

	public void Close()
	{
		// _disposable.Disposeですべて破棄される。
		// 必ずクラス終了前に呼び出すこと
		_disposable.Dispose();
	}
}
```

Subscribeの戻り値に対してAddToメソッドを呼び出す

そこに `DisposableBag.CreateBuilder()` で作り出した `DisposableBagBuilder` を渡します。
そして `_disposable.Dispose` を呼び出すことでイベントすべての購読が破棄されます。

イベント単体の破棄であればSubscribeの戻り値をそのままメンバ変数に持ち、最後にDisposeするだけでも良


<b>Disposeは明示的に呼び出す必要があるので注意</b>

## Monobehaviourを継承しているクラスでイベントを破棄したい場合
UniRxを組み込んでいる場合、Monobehaviourクラスはもう少し楽に破棄の処理ができます

```cs
namespace UniRx; // UniRx必須

public class Hoge : Monobehaviour
{
	void Awake()
	{
		// MyEventの通知を受け取る
		var subscriber = MessagePipe.GlobalMessagePipe.GetSubscriber<MyEvent>();
		subscriber.Subscribe(ev =>
		{
		}).AddTo(this); // 自分を渡してGameObjectの破棄時に削除をしてもらう
	}
}
```

UniRxではおなじみの処理ですが
AddToに自分自身を渡してGameObjectのライフサイクルに結びつけます。
これで**GameObjectが破棄されるときにイベントも破棄されます。**


## Subscribeを適切にハンドリングしてない場合エラーを出す

Disposeの仕方はわかりましたが<b>『Subcribeを適切に処理しているかどうか』</b>は結局コードを書く人に委ねられている状態です。

忘れないように心がけていても**抜けは必ず発生します(必ず）**


しかし！ (Unity2020.2以降であれば)**Subcribeの戻り値をハンドリングしてないものがあれば、ビルド時にエラーになる`MessagePipe.Analyzer`という便利機能が公開されているようです**


MessagePipe.Analyzerは<b>Roslyn analyzers</b> という、ユーザー独自のコード解析を組み込める仕組みを利用されています(以下記事参考）

https://docs.unity3d.com/2020.2/Documentation/Manual/roslyn-analyzers.html

----
結果からいうと <b>MessagePipe.Analyzer.dll</b>  を組み込むことで
Subscribeに対して何も処理をしていない場合<b>Unityのコンソール上やVisualStudio上でエラーが出るようになります</b>

- VisualStudio
↓ Subscribeに対して何もしていない
![20210704173534.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/2d1e1541-a1be-e00a-5a8f-f28330e1d069.png)


- Unity
![20210704173442.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/a6146111-6408-0321-cec6-bb58cd0b025f.png)

こうしてエラーが出てくれれば忘れることもない(実行できないので)

### 導入手順
++++++++++++++++++++++++++++++++++++++++++++++++
<b>Unity2021.2.0b2</b> で動作を確認しています。
<b><font color="#ff0000">Unity2020.3.X系だとUnityでエラーが出ませんでした...(VisualStudioではエラーが出る)</span></b>

バグか自分の設定が悪いのか.. <font color="Green">調査中</font>
++++++++++++++++++++++++++++++++++++++++++++++++

#### 1. MessagePipe.Analyzer.dll を Unityに入れる
MessagePipeのGithubに<b>MessagePipe.Analyzer.dll</b> が上がっています。
(以下GithubのReleaseを参照)

https://github.com/Cysharp/MessagePipe

DLLをダウンロード
![20210630233610.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/5667b1d4-a17d-99b2-a474-30c5215c43df.png)

そしてAsset以下のどこか適切なディレクトリにぶっこむ。
![20210704174052.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/81063ade-4843-fb7b-c495-aadaa3b17478.png)

#### 2. 設定変更
MessagePipe.AnalyzerのInspectorで以下のように設定変更すること

- `SelectPlatforms for Plugin` のチェックをすべて外す
- 右下のAssetLabelsボタンをクリックして `RoslynAnalyzer` を入力してEnterを押してラベルを付ける

![20210704174456.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/e16061e6-22b1-b00a-2252-d29f3517bfb6.png)
ロード終了後、<b>Subscribeを処理していない箇所に対してUnityがエラーを出すようになります</b>


#### 3. Visualstudioでもエラーを出す
VisualStudio上でもエラーを出すためには、Unityに対して更に一工夫する必要がありました。

同じくCysharpが公開している<b>CsprojModifier</b>を導入します

https://github.com/Cysharp/CsprojModifier

公開されている最新バージョンのパッケージをインストールします

https://github.com/Cysharp/CsprojModifier/releases/tag/1.1.1

`Unity > Project Settings > C# Project Modifier` を開き、`Add Roslyn Analyzer references to .csproj` にチェックを付け、 **Regenerate project files** を押して .csproj を作り直します。

![20210704175014.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/86b4beda-0f59-1ac9-c2d1-222427d424c2.png)

<b>終了後再度VIsualStudioを開き直したらVisualStudio上でも検知できるようになりました</b>

(現状、Unity2020.2以降のバージョンを使用している場合VisualStudioCode, Rider ではこの機能は使用できないようです。
MacのVisualStudioでは正常に確認ができました）
Unityでエラーが出る以上実行ができないのでUnityでカバーしていればまあよし！とも捉えられる

## MessagePipeDiagnostics を使用する

MessagePipeは更に機能があり(凄い)、実行中のプロジェクトで現在Subscribeされている数や情報を取得できる機能があります。(MessagePipeDiagnosticsInfo)

そして気軽に情報を見れるようにSubscribeしている箇所のモニターが出来る拡張機能が用意してあります(凄い)

`Window > MessagePipeDiagnostics` を開くと、EditorWindowが立ち上がります。

![20210701100112.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/8d222db6-be4b-8437-3bf6-dbb34216ecfb.png)

そして実行中にWindowを見みると以下画像のようにSubscribeしている箇所が確認できます

![20210704181756.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/3c8ee51a-619d-6b50-9384-b7e1b7ecbaaf.png)

この機能を利用するには `GlobalMessagePipe.SetProvider` を予め設定しておく事と、optionの`EnableCaptureStackTrace`をtrueにしておく必要があります。

![20210704181941.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553138/4ee7928f-a30e-8949-37e9-08cc418eaa84.png)


## 終わりに
今回はSubscribeした結果のイベント購読破棄について書きました。
Subscribeは適切に処理をしないと不具合の原因になるので標準にAnalyer機能、モニターEditor機能が用意されているのは本当に素晴らしいと感じました。

今回のDispose周りについて更に理解したことができたら追記していきます

##### お試しコード
<details><summary>Disposeで破棄</summary><div>

```cs
using NUnit.Framework;
using MessagePipe;
using Zenject;
using UnityEngine;
using System;

namespace XXXX
{
	public class DisposableTest
	{
		// イベント送る方
		public class Publisher
		{
			[Inject] private IPublisher<MyEvent> _publisher;

			public void Send(MyEvent ev) =>
				_publisher.Publish(ev);
		}

		// イベント受け取る方
		public class Subscriber : IDisposable
		{
			private readonly IDisposable _disposable;

			public Subscriber(ISubscriber<MyEvent> subscriber)
			{
				var bag = DisposableBag.CreateBuilder();

				subscriber
					.Subscribe(x => Debug.Log($"{x.Message}"))
					.AddTo(bag);

				_disposable = bag.Build();
			}

			void IDisposable.Dispose()
            {
				_disposable.Dispose();
			}
		}

		// 送るイベント
		public class MyEvent 
		{ 
			public string Message; 
		}


		private DiContainer _container;

		/// <summary>
		/// SubscribeのIDosposableをかならず何らかの形でハンドリングする必要がある。
		/// しなければリークする
		/// </summary>
		[Test]
		public void イベントを破棄する()
		{
			_container = new DiContainer();

			var option = _container.BindMessagePipe();
			_container.BindMessageBroker<MyEvent>(option);

			// イベントを受ける方
			using (_container.Instantiate<Subscriber>())
			{
				// イベントを投げる方
				var publisher = _container.Instantiate<Publisher>();
				publisher.Send(new MyEvent { Message = "テストメッセージ" });
			}
		}
	}
}
```

</div></details>