Help us understand the problem. What is going on with this article?

Result型を使ってエラーをスマートにハンドリングする方法(Swift)

はじめに

2020/03/29現在、Swiftではスローするエラーの型を指定できません。
そのため、スローするエラーの型が決まっていたとしても、すべてのエラーをキャッチする必要があります。

MyError というエラーの型のみを返す myFunc(isSuccess:) メソッドを例に考えてみます。
(この例では MyError.bar は返ることがないのですが、返る可能性があると考えてください)

enum MyError: Error {
    case foo(_ message: String)
    case bar(_ message: String)
}

func myFunc(isSuccess: Bool) throws -> String {
    if isSuccess {
        return "Success!!"
    } else {
        throw MyError.foo("Foo")
    }
}

myFunc(isSuccess:) メソッドを呼び出します。
エラーの型を MyError だと決めていますが、一番下に } catch { を書いてすべてのエラーをキャッチする必要があります。

before
do {
    let result = try myFunc(isSuccess: false)
    // `result` を使う処理
} catch MyError.foo(let message) {
    // エラーハンドリング
} catch MyError.bar(let message) {
    // エラーハンドリング
} catch {
    // !!!: 絶対に入らないけど必要
}

もし一番下の } catch { を書いていないと、以下のビルドエラーが発生します。

Errors thrown from here are not handled because the enclosing catch is not exhaustive
ここからスローされたエラーは、囲んでいるキャッチが完全ではないため処理されません

Switch文で default: をできる限り書かないようにするのと同様、絶対に入らないとわかっているエラーに対してハンドリングしたくありません。
絶対に入らないことを明確にするため、 fatalError(_:) を使っている人も多いと思います。

} catch {
    fatalError("Unexpected error: \(error).")
}

いずれにせよスマートではないように感じます。
Twitterでスマートな方法を教えていただいたので紹介します。

環境

  • OS:macOS Mojave 10.14.6
  • Swift:5.1.3
  • Xcode:11.3.1 (11C504)

結論

先に結論を述べます。
Result.init(catching:)Result.mapError(_:) を使って実現します。

after
let result = Result { try myFunc(isSuccess: false) }
    .mapError { $0 as! MyError }

switch result {
case .success(let string):
    // `string` を使う処理
case .failure(let error):
    switch error {
    case .foo(let message):
        // エラーハンドリング
    case .bar(let message):
        // エラーハンドリング
    }
}

こうすることで、 MyError 型のみをハンドリングできるようになりました。
エラーを列挙型で定義している場合、Switch文が使えるので、エラーのハンドリング漏れも防げます。

解説

おそらくSwiftに慣れていても、上記のような書き方を知らない人は多いと思います。
少なくとも私は知りませんでした。

1つずつ解説します。

Result.init(catching:)

まずは1行目です。

let result = Result { try myFunc(isSuccess: false) }

こちらは Result 型のイニシャライザを、トレイリングクロージャを使って呼び出しています。

Result 型には Result.init(catching:) という、スローするクロージャを引数に持つイニシャライザがあります。
クロージャの処理が成功した場合は .success を返し、クロージャの戻り値がAssociated Valueに代入されます。
失敗した場合は .failure を返し、エラーがAssociated Valueに代入されます。
つまり、今回は myFunc(isSuccess:)String を返すため、Result<String, Error> という型が生成されます。

Result.mapError(_:)

次に2行目です。

    .mapError { $0 as! MyError }

こちらは失敗時のエラーの型をマップ(変換)するメソッドです。

Result.init(catching:) で生成した型は Result<String, Error> です。
mapError(_:)Error 型を MyError 型に変換し、 Result<String, MyError> を生成します。

ここまでできたら、あとは通常の Result 型と同じように使えます。

switch result {
case .success(let string):
    // `string` を使う処理
case .failure(let error):
    switch error {
    case .foo(let message):
        // エラーハンドリング
    case .bar(let message):
        // エラーハンドリング
    }
}

おまけ:なぜエラーの型を指定できないか考える

個人的な意見ですが、例えば throws の後ろにエラーの型を記述し、スローするエラーの型を指定できると使いやすいと思いました。

func myFunc(isSuccess: Bool) throws MyError -> String {

なぜこれが実装されないか考えたのですが、おそらく普通に実装しても様々なエラーが発生し得るからだと思いました。
自分では MyError 型しかスローしていないつもりでも、実装ミスで他のエラーがスローされる可能性は0ではありません。
実際、今回紹介した方法も as! MyError として強制キャストすることで実現しています。

おわりに

これでエラーをスマートにハンドリングできるようになったと思います!
GitHubにサンプルのplaygroundを上げたので、よかったら実行してみてください。
https://github.com/uhooi/SwiftErrorResultSample/blob/master/ErrorResult.playground/Pages/Sample.xcplaygroundpage/Contents.swift

他にもいい方法がありましたら、コメントなどで教えていただけると嬉しいです。

参考リンク

uhooi
iOSアプリ開発とSwiftが好きです✨ 趣味:テニス、アナログゲーム
https://theuhooi.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away