見出し画像

UniTaskでtry-catchを書かずにキャンセルする方法

こんにちは!初投稿はUniTaskに関する記事です!
UniTaskを存じない方へ簡単に説明すると、Unity向けの非同期処理ライブラリです。
UniTaskについて詳しく知りたい方は以下のリンクからどうぞ。



注意書き

今回使用するUniTaskはMonoBehaviourを継承したオブジェクトがDestroyされても、ゲームの再生が止まるか、非同期処理が条件を満たして自力で止まるまで止まりません。
なので、OnDestroy関数などでキャンセルしてあげてください。

キャンセル処理

上記のリンクでもUniTaskのキャンセルについて載っているとは思いますが、簡単におさらい。
UniTaskの非同期処理をキャンセルしたい場合は、CancellationTokenSource型の変数を用意し、そのメンバーであるCancellationToken型”Token”を非同期処理に引数として渡してあげる必要があります。
(※UniTaskが用意してくれる関数なら引数として渡せますが、自作の関数ならまた別です。また下で説明します。)

まずはUniTaskから提供されているWaitWhile関数を利用するサンプルは以下の通りです。

// UniTaskを使うために必要
using Cysharp.Threading.Task;
// CancellationTokenSourceを使うために必要
using System.Threading;

private class SampleTask : MonoBehaviour
{
    CancellationTokenSource m_source;
    
    // Start関数は非同期にしてもUnity側から呼ばれるよ!
    private async void Start()
    {
        m_source = new();
        await SampleTask();
    }

    private async UniTask SampleTask()
    {
        // Application.isPlayingがtrueである限りずっと待機する非同期処理
        // Application.isPlayingはエディターで再生されているか、
        // ビルドされたゲームをプレイ中だとtrueになる。
        await UniTask.WaitWhile(() => Application.isPlaying, CancellationToken: m_source.Token);
    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            m_source?.Cancel();
            ReleaseSource();
        }
    }

    private void ReleaseSource()
    {
        m_source?.Dispose();
        m_source = null;
    }

    private void OnDestroy()
    {
        ReleaseSource();
    }
}

次に、自作の非同期処理を使う場合。

private async UniTask OriginalTask()
{
    // この時点でキャンセルされていたら例外を出して、下の処理をしない。
    m_source.Token.ThrowIfCancellationRequested();

    // 何か非同期処理をする
    ・・・
    m_source.Token.ThrowIfCancellationRequested();
}

以上がキャンセルする簡単なサンプルです。
CancellationTokenSourceはキャンセルされたら使い回しができないので、ReleaseSource関数を定義して、そこでリソースの解放などを行っています。必要な時のみnewでインスタンス化してください。

しかし、上記のコードだと1つ問題があります。
実際に実行してみた方は分かると思うのですが、UnityEditorのコンソールにエラーが出たと思います。
そう、キャンセルすると例外が投げられるのです。
は?欠陥かよって思うかもしれませんが、そうです。上記のコードだと欠陥だらけです。なので、次は例外をキャッチしてエラーが出ないようにしてみます。

try-catch

本記事のタイトルでも書いているのですが、これはあまりお勧めしません。なんたって書く量が多いから…。try-catchが分からない方はググってください。例外を出したくない、キャンセルされたら処理してほしいことがある場合に有効です。

・・・
// OperationCanceledExceptionのために必要
using System;
・・・

private async void Start()
{
    m_source = new();
    try
    {
        await SampleTask();
    }
    catch(OperationCanceledException)
    {
        // キャンセルされたときの処理
        ・・・
    }
}

private async UniTask SampleTask()
{
    await UniTask.WaitWhile(() => Application.isPlaying, CancellationToken: m_source.token);
}
・・・


これでキャンセルしてもEditor側にエラーを出されなくなりました。
catchしているOperationCanceledExceptionは、キャンセルされたときに投げられる例外です。UniTask側の非同期処理をキャンセルした時や、ThrowIfCancellationRequested関数を実行したときに(キャンセルしていたら)投げられます。
(実はUniTaskの関数内にも、ThrowIfCancellationRequested関数が書かれてます。)

キャンセルしてもエラー出ないからいいじゃん!と思うかもしれません。でも非同期処理を書くときに毎回書かないといけないので、なかなか骨が折れます。
そこで、次はForget関数というものを紹介します。

Forget関数

Forget関数は、呼び出し元のUniTask型の非同期処理が例外を投げても無視してくれます。これはただ非同期処理を実行して、キャンセルされても別にいいやって時に使えます。

private async void Start()
{
    m_source = new();
    await SampleTask().Forget();
}

private async UniTask SampleTask()
{
    await UniTask.WaitWhile(() => Application.isPlaying, CancellationToken: m_source.token);
}

try-catchよりも書く量が少なく、スマートに見えますよね?
Forget関数さえあれば大抵は事足りると思います。
しかしForget関数は1つ欠点があります。それは待機できないことです。
1つの非同期処理のあと、あれもこれもやりたいって時に、待つことができません。それこそtry-catchを書くと地獄になります。

// 1つ目(普通に非同期処理を実行する)
try
{
    m_source = new();
    await SampleTask1();
}
catch (OperationCanceledException)
{
    // CancellationTokenSourceは使い切りなので、
    // キャンセルされたらDispose関数でリソースを解放して、
    // 新しいインスタンスを代入しよう
    ReleaseSource();
}
// 2つ目(キャンセルされても実行する)
try
{
    m_source = new();
    await SampleTask2();
}
catch (OperationCanceledException)
{
    ReleaseSource();
    // 3つ目(キャンセルされたら実行する)
    try
    {
        m_source = new();
        await SampleTask3();
    }
    catch (OperationCanceledException)
    {
    }
}

こんなコードになったらそりゃもう地獄です。
そこで今回のメインディッシュ、SuppressCancellationThrow関数をご紹介します!

SuppressCancellationThrow関数

これさえあればUniTaskが格段と使いやすくなります。
SuppressCancellationThrow関数は、呼び出し元のUniTaskからOperationCanceledExceptionがスローされたかを、bool型で返してくれます!サイコー!!
利用例は以下の通り。

・・・
private async void Start()
{
    m_source = new();
   // キャンセル例外がスローされていたらTrue、されていないとFalseが返ってきます
    var canceled = await SampleTask().SuppressCancellationThrow();
    if (canceld) return;

    // 2つ目はお好きなように
  //Sample2().Forget();
    canceled = Sample2().SuppressCancellationThrow();
    if (canceled) return;
}

private async UniTask SampleTask()
{
    await UniTask.WaitWhile(() => true, CancellationToken: m_source.token);
}

private async UniTask Sample2()
{
    // 3秒待つ
    await UniTask.Delay(3000, CancellationToken: m_source.token);
}
・・・

キャンセルの例外がスローされたかどうかをboolで受け取るだけで、非同期処理がかなり分かりやすくなったと思います!

おまけ

上記でさんざん使用してきたCancellationToken型のToken、なんとメンバー関数にキャンセルされるまで待つUniTask、WaitUntilCanceled関数を持っています!
もしキャンセルされるまで待ちたい場合は、以下のようにするとキャンセルまで待てます。

await m_source.Token.WaitUntilCanceled();

さらに、このようにしてキャンセルされるのを待った場合、例外がスローされません!なので、SuppressCancellationThrow関数を使用してもFalseが返ってきます。
キャンセルされるのが想定通りの実装であれば役に立つと思います!

この記事が気に入ったらサポートをしてみませんか?