【UniTask】他のクラスが初期化されるまで待つ - UniTaskCompletionSourceの使用例

この記事は、サイバーエージェントの26卒エンジニア有志が開催する
[CyberAgent 26th Fresh Engineer's Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ca-26th) の4日目です

## はじめに

シーン開始時の初期化順序は、かなり頭を悩ませる問題です。
何かの初期化を待って初期化処理を行いたい、ということがよくあるからです。

DefaultExecutionOrderAttributeなどを用いて実行順序を指定することこそ可能ですが、
複雑化すると混乱やミスを産みやすいため、極力避けるべきでしょう。

また、シーン開始直後から通信などを得て設定される情報をもとに処理を行いたい場合などについても、待つという処理が必要になります。

今回は、UniTaskのUniTaskCompletionSourceを用いて、このように非同期で対象が初期化されるのを待つ方法について解説します。

```csharp
await target.WaitForReady();
```

尚、UniRxには `AsyncSubject<T>` というクラスがあり、このような形で初期化を待つことが可能です。

```csharp
target.OnInitialized.Subscribe(_ => Initialize());
```

`AsyncSubject<T>`は最後に発行されたイベントをキャッシュし、既に発行されていた場合は講読時点でコールバックを呼び出します。
これによって、初期化順序、講読タイミングに関わらずデータの初期化を待つことができるわけです。
詳細は、以下の記事が参考になります。

https://qiita.com/toRisouP/items/1bd2f3b07bf868953178

しかし、次世代UniRxであるR3では、この`AsyncSubject<T>`に相当する機能がありません。
そこで今回使用するのが、UniTaskCompletionSourceです。

## UniTaskCompletionSource

まず、単純な実装がこちらになります。

```csharp
public class NeedToReady : MonoBehaviour
{
	private UniTaskCompletionSource taskSource = new();

	private void Awake()
	{
		taskSource.TrySetResult();
	}

	public UniTask WaitForReady()
	{
		return taskSource.Task;
	}
}
```

UniTaskCompletionSourceを用いることで、
任意のタイミングで完了するUniTaskを生成することが可能なため、
それを初期化待ちに利用した形です。

また、UniTaskCompletionSourceには値を設定することもできるため、以下のように初期化に際して情報を返すことも可能です。

```csharp
public class NeedToReady : MonoBehaviour
{
	private UniTaskCompletionSource<Data> taskSource = new();

	private void Awake()
	{
		...
		taskSource.TrySetResult(data);
	}

	public UniTask<Data> WaitForReady()
	{
		return taskSource.Task;
	}
}
```

## ラップしてみる
このようなWaitForReadyの関数を用意するのは少し冗長なため、
ラップして汎用的なデータコンテナクラスを作っておくといいかもしれません。
また、この実装ではAttachExternalCancellationを用いて値の待機をキャンセルできるようにしています。

```csharp
public interface IReadOnlyAsyncStorage<TContent>  
{  
	UniTask<TContent> GetAsync(CancellationToken cancellationToken = default);  
}

public class AsyncStorage<TContent> : IReadOnlyAsyncStorage<TContent>  
{  
	private readonly UniTaskCompletionSource<TContent> _completionSource = new();  
	  
	public async UniTask<TContent> GetAsync(CancellationToken cancellationToken = default)  
	{  
		return await _completionSource.Task.AttachExternalCancellation(cancellationToken);  
	}

	public bool TrySet(TContent content)  
	{
		return _completionSource.TrySetResult(content);  
	}
}
```

これを用いることで、読み取り専用の形で公開することができ、簡単かつ安全に利用することができます。

```csharp
// 初期化側
public class NeedToReady : MonoBehaviour
{
	private AsyncStorage<Data> initializeDataStorage = new();
	public IReadOnlyAsyncStorage<Data> InitializeData => initializeDataStorage;

	private void Awake()
	{
		...
		initializeDataStorage.TrySet(data);
	}
}

// 利用側
public class User : MonoBehaviour
{
    [SerializeField] private NeedToReady needToReady;

    private async void Start()
    {
        var data = await needToReady.InitializeData.GetAsync(destroyCancellationToken);
        ...
    }
}
```

## おわりに
今回は、UniTaskCompletionSourceを用いて
他のクラスの初期化を待つ方法について解説し、
また簡単に利用できるようにしたラッパークラスを紹介しました。

UniTaskCompletionSourceは任意のタイミングで終了するUniTaskを作ることができ、
非同期でないものを非同期的に扱うことができます。

よければ是非活用してみてください。

## 参考
https://qiita.com/toRisouP/items/1bd2f3b07bf868953178

https://www.hanachiru-blog.com/entry/2021/07/10/221719