はじめに
こんにちは、Unityエンジニアのイワケンです。Qiitaの閲覧数のタグの割合が公開されるようになりましたね。私はUnityが1位でした。やっぱりなという感じです。Qiitaはこの機能を今後このまま残すのか、色んな意味で楽しみですね (追記:現在非表示になりましたね)
さて、今回の記事は、プロジェクト内のUniTaskのあらゆる非同期処理に対して共通の例外処理を行いたい時に役立つかもしれない記事です。
非同期処理中に例外が起きたらエラーダイアログを表示したい!
非同期処理の例としてResources.LoadAsync(cubeName);
でPrefabを読み込み生成、cubeNameがResoucesフォルダに存在しない場合はエラーダイアログを出す実装について考えてみます。
CubeNameを間違ってしまったら例外発生→エラーのダイアログを表示
こんなエラーダイアログを、あらゆる非同期処理 (例えばHTTPClientでAPIを叩く時など)に対して適用したいのです。
やりたくないのは、全ての非同期処理に対して、例外処理(try-catch文)を書くこと。共通処理はまとめたいですよね。
まずシンプルに非同期処理を書いてみる
ResoucesフォルダからCubeNameの名前のPrefabを非同期に読み込み、Instantiateで生成する処理です。
Forget();
をくっつけることで、awaitを書かなくてもエラーがでなくなる (asyncでない関数内で実行できる)ようになります。
void Start(){
InstantiateCubeAsync("Cube").Forget();
}
async UniTask InstantiateCubeAsync(string cubeName){
GameObject cube = await Resources.LoadAsync(cubeName) as GameObject;
Instantiate(cube,new Vector3(0,1,0),Quaternion.identity);
}
試しに例外処理をそのまま書いてみる[ダメ例]
こちらは極力避けたい例です。
cubeNameがResoucesフォルダに存在しない場合、例外を吐き出します。例外をcatchしてエラーダイアログをだす処理を書きます。(例外の思想として正しいかどうかは今回置いておきます。)
async UniTask InstantiateCubeAsync(string cubeName) {
try {
var a = Resources.LoadAsync(cubeName);
GameObject cube = await Resources.LoadAsync(CubeName) as GameObject;
Instantiate(cube,new Vector3(0,1,0),Quaternion.identity);
} catch(Exception e) {
Debug.Log(e.Message);
Debug.Log($"{CubeName}はResoucesフォルダに存在しない名前です。");
ErrorDialogController.Instance.SetActive(true); //エラーダイアログ表示
}
}
1つ,2つこのような処理を書くのはいいのですが、10個100個非同期処理のメソッドがあり、それぞれにtry-catchを書き、例外時の処理を書くのはしんどいのですよね...
そのために、UniTaskの拡張コードを書き、例外処理を同じコードで行うようにします。
UniTaskを拡張する[今回の記事の推しパターン]
結論次のようなコードを書きます
using System;
using UnityEngine;
using UniRx.Async;
static class UniTaskExtensions
{
public static async UniTask RunTaskHandlingErrorAsync(this UniTask task) {
try {
await task;
} catch(Exception e) {
Debug.Log(e.Message);
ErrorDialogController.Instance.SetActive(true);
}
}
public static void RunTaskHandlingError(this UniTask task) {
RunTaskHandlingErrorAsync(task).Forget();
}
}
すると先程のコードは次のように書けます。
void Start()
{
InstantiateCubeAsync(cubeName).RunTaskHandlingError();
}
async UniTask InstantiateCubeAsync(string CubeName) {
GameObject cube = await Resources.LoadAsync(CubeName) as GameObject;
Instantiate(cube,new Vector3(0,1,0),Quaternion.identity);
}
Forget()
の部分がRunTaskHandlingError()
に変わっただけですが、これだけでtry-catchをいちいち書かずに、例外が発生した時にエラーダイアログを表示する処理を書くことができました。
応用: 複数の例外に対応
実際に例外処理で行いたいこととしては、例外の種類によってユーザに対する反応を変えるということではないでしょうか?それを行うために、複数の例外を定義し、それぞれcatchして例外処理を行うようにします。
具体例として、RESTApiのエラーコードに応じて、ダイアログの表示を変える処理について考えてみます。
順序としては
- オリジナルの例外の定義
- 例外を投げる処理の実装
- UniTaskExtensionを書き換える
- 非同期処理実行時に
RunTaskHandlingError()
を付与する。
という順番になります。
オリジナルの例外の定義
まず、ダイアログの表示に対応する例外を定義してみます。今回は、401 認証エラーと404 NotFoundに対する例外処理を別々で行うとしましょう。
using System;
using System.Runtime.Serialization;
[Serializable()] //クラスがシリアル化可能であることを示す属性
public class NotFoundException : Exception
{
public NotFoundException()
: base()
{
}
public NotFoundException(string message)
: base(message)
{
}
public NotFoundException(string message, Exception innerException)
: base(message, innerException)
{
}
//逆シリアル化コンストラクタ。このクラスの逆シリアル化のために必須。
//アクセス修飾子をpublicにしないこと!(詳細は後述)
protected NotFoundException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
public class UnauthenticatedException : Exception
{
public UnauthenticatedException()
: base()
{
}
public UnauthenticatedException(string message)
: base(message)
{
}
public UnauthenticatedException(string message, Exception innerException)
: base(message, innerException)
{
}
//逆シリアル化コンストラクタ。このクラスの逆シリアル化のために必須。
//アクセス修飾子をpublicにしないこと!(詳細は後述)
protected UnauthenticatedException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
}
}
例外を投げる処理の実装
REST的なApiClientの実装例です。(UniTask便利や...)
ResponseCodeに応じて、投げる例外の処理を変えています。
using UnityEngine.Networking;
using UniRx.Async;
public class ApiClient
{
public async static UniTask<string> GetAsync(string uri) {
UnityWebRequest webRequest = UnityWebRequest.Get(uri);
await webRequest.SendWebRequest();
// エラーコードが返ってきたら、例外を投げる
if(webRequest.isNetworkError || webRequest.isHttpError) {
int responseCode = (int)webRequest.responseCode;
if(responseCode == 401) {
// 認証エラー
throw new UnauthenticatedException();
}
if(responseCode == 404) {
// Not Found
throw new NotFoundException();
}
// ...省略
}
return webRequest.downloadHandler.text;
}
}
UniTaskExtensionを書き換える
例外をcatchしたときの処理を書きます。
:
static class UniTaskExtensions
{
public static async UniTask RunTaskHandlingErrorAsync(this UniTask task) {
try {
await task;
} catch(UnauthenticatedException e) {
Debug.Log(e.Message);
// 「認証されませんでした」のダイアログ表示の処理など
} catch(NotFoundException e) {
Debug.Log(e.Message);
// 「見つかりませんでした。」のダイアログ表示の処理など
} catch(Exception e) {
Debug.Log(e.Message);
// その他の例外....
}
}
public static void RunTaskHandlingError(this UniTask task) {
RunTaskHandlingErrorAsync(task).Forget();
}
}
非同期処理実行時にRunTaskHandlingError()
を付与する
先程同じ感じで、このようにかけます。
void Start()
{
CallApi().RunTaskHandlingError();
}
async UniTask CallApi() {
var json = await ApiClient.GetAsync("www.example.com");
// 続き処理を書く
}
おまけ
UniTaskExtensionsに次のように書くと、UniTask<T>
にも対応します。
public static void RunTaskHandlingError<T>(this UniTask<T> task) {
RunTaskHandlingErrorAsync(task).Forget();
}
public static async UniTask<T> RunTaskHandlingErrorAsync<T>(this UniTask<T> task) {
try {
return await task;
} catch(Exception e) {
ErrorDialogController.Instance.SetActive(true);
Debug.Log(e.Message);
}
return await task; //もっと良い書き方教えて下さい。
}
これで、こんな書き方もできるようになります。
string型を返り値に持つ非同期メソッドUniTask<string>
にも適用できました。
async UniTask CallApi() {
var json = await ApiClient.GetAsync("www.example.comaaa").RunTaskHandlingErrorAsync();
Debug.Log(json);
// 続き処理を書く
}