[Swift] Task から発生する例外の処理方法

     
⌛️ 2 min.
Swift Concurrency の一部として導入された Task を エラー処理の観点からもう少し理解します。

環境&対象

以下の環境で動作確認を行なっています。

  • macOS Ventura 13.4 Beta
  • Xcode 14.3 RC
  • iOS 16.4 beta

Task

Swift Concurrency の一部としてリリースされました。

Apple のドキュメントは、こちら

使用中のスレッドではなく、別のスレッドを使用するように処理でき、現在のスレッドをブロックしないようにできます。

別スレッドで処理するようにした Task の処理が終わるまで、(現スレッドを) suspend して待つこともできますし、
またずに、別の処理を進めることも可能です。
# Task に渡した closure の実行自体は、いつ行われるかは不明です。

# このあたりは、Google 検索すると解説記事が多く出てくると思います。

Task の返り値

あまり言及されることがないのですが、Task は、返り値を持ちます。

    func myFunc() {
        Task {
            print("Hello")
        }
    }  

上記の myFunc というメソッドは、実行されると 内部で Task を生成し実行させています。

生成された Task では、指定された closure (print 文だけです) が実行されます。

上記のコードを省略(?) せずに書くと以下のようになります。

    func myFunc() {
        let myTask = Task<Void,Never> {
            print("Hello")
        }
    }

このときの型は Task<Void,Never> です。

# Task のインスタンスは、Task の実行をキャンセルするときにも必要になりますので、返り値を変数に保持するコードを見たことはあるのではないでしょうか。

Task は、2つの type parameter を持ちます。1つめの Void は、closure の返り値を表しています。現在のコードは、print するだけで 値を返さないので、Void です。
2つめの Never は、closure が例外を送出しないことを表しています。

closure が値を返すようにしてみます。以下のようにコンパイルエラーが発生します。

    func myFunc() {
        let myTask = Task<Void,Never> {  // - compile error: Cannot convert value of type 'Int' to closure result type 'Void'
            print("Hello")
            return 1
        }
    }

エラーを解消するには、Task<Int,Never> と変更する必要があります。

    func myFunc() {
        let myTask = Task<Int,Never> {
            print("Hello")
            return 1
        }
    }

同様に、closure から 例外を送出してみます。

やはり、コンパイルエラーが発生します。

    func myFunc() {
        let myTask = Task<Void,Never> { // - compile error: No exact matches in call to initializer 
            print("Hello")
            throw MyError.justError
        }
    }

エラーメッセージが少しわかりにくいですが、throw するような closure を持つ時に該当する initializer が存在しないという意味です。

先ほどと同様に、Task の type parameter を修正するとコンパイルエラーが解消されます。

        let myTask = Task<Void,Error> {
            print("Hello")
            throw MyError.justError
        }

このとき、Error を具体的な Error (例えば、MyError) にしたくなりますが、できません。throw するものは、Error としか定義されていないので、Task でも Error での定義しかできません。

# Task が独自に定義/利用するのではなく、closure の throw するものを渡してくれるものなので、throw の振る舞いに従っています。

エラー処理

準備ができた(?) ので、エラー処理を考えてみます。

Task 内で例外を処理, 返り値で反映

Task 内で例外を処理することは、これまでと同じです。

        let task = Task<String,Never> {
            do {
                try throwableTask()
            } catch {
                // handle error
                return "error"
            }
            return "done"
        }

例外を catch したときに return “error” することに議論の余地はありますが、Task の中で、 例外を処理することに 特殊な考慮は必要ありません。

# 内部で例外を catch しているので、Task<String, “Never”> になっていることに気をつけてください。

このような処理をしたときに、呼び出し側は、以下のようになっているはずです。

    func myFunc() {
        let task = Task<String,Never> {  // (1) Task を保持
            do {
                try throwableTask()
            } catch {
                // handle error
                return "error"
            }
            return "done"
        }

        Task {                          // (2) 別 Task を使用
            switch await task.value {   // (3) Task 結果をチェック
            case "error":
                print("error occured")  // (4) エラー処理
            default:
                print("result: \(await task.value)")
            }
        }
    }
  1. Task を生成、保持
  2. 呼び出し側は、sync なので、結果を確認するために 別 Task を生成
  3. task.value に保存される 結果(この場合 String) をチェック。async な プロパティなので、await を使ったアクセスが必要です
  4. 保持されている値によって エラー処理
注意

value を使った エラー処理を推奨しているわけではありません。あくまで、このような渡し方も技術的に可能という意味です。

例外を Task 外部に伝播してエラー処理

上記の例は、例外を 無理やり(?) 返り値に変換しています。

Task 内部で発生した例外を Task 外部で受け取る方法ももちろんあります。

    func myFunc() {
        let task = Task<String,Error> {
            try throwableTask()
            return "done"
        }

        Task {
            switch await task.result {      // (1)
            case .success(let value):       // (2)
                print("Result: \(value)")
            case .failure(let error):       // (3)
                if let myError = error as? MyError {
                    print("error occured")
                } else {
                    // unknown error?
                }
            }
        }

前半の Task 生成は同じなので、2つ目の Task を説明します。

コード解説
  1. task.result に結果と例外情報が保持されています。value 同様 async な プロパティなので、await を使ったアクセスが必要です
  2. result には、Result 形で保持されていて、.success には、value と同様に 結果が保持されています
  3. .failure に Error が保持されています。上記では 型チェックを行なってエラー処理をしています。

このようにすると、Task 内部で発生した例外を Task 外部でもチェックすることが可能となります。

ただし、Task の結果には async なアクセスしかできませんので、チェックする側が async であるか、別 Task を使ってのチェックが必要となります。

まとめ

Task から発生する例外の処理方法を確認してみました。

Task から発生する例外の処理方法
  • Task の返り値 value をチェックすると closure の返り値がわかる
  • result をチェックすると closure の返り値、closure の throw した Error の両方がわかる
  • value/result には async なアクセスしかできない

説明は以上です。
不明な点やおかしな点ありましたら、こちらまで。

SwiftUI おすすめ本

SwiftUI を理解するには、以下の本がおすすめです。

SwiftUI ViewMatery

SwiftUI で開発していくときに、ViewやLayoutのための適切なmodifierを探すのが大変です。
英語での説明になってしまいますが、以下の”SwiftUI Views Mastery Bundle”という本がビジュアル的に確認して探せるので、便利です。

英語ではありますが、1ページに コードと画面が並んでいるので、非常にわかりやすいです。

View に適用できる modifier もわかりやすく説明されているので、ビューの理解だけではなく、どのような装飾ができるかも簡単にわかります。

超便利です

SwiftUIViewsMastery

販売元のページは、こちらです。

SwiftUI 徹底入門

# SwiftUI は、毎年大きく改善されていますので、少し古くなってしまいましたが、いまでも 定番本です。

Swift学習におすすめの本

詳解Swift

Swift の学習には、詳解 Swift という書籍が、おすすめです。

著者は、Swift の初期から書籍を出していますし、Swift の前に主力言語だった Objective-C という言語についても同様の書籍を出しています。

最新版を購入するのがおすすめです。

現時点では、上記の Swift 5 に対応した第5版が最新版です。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です