4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Swift】Result<Success, Failure>型でエラー処理を行う

Last updated at Posted at 2020-12-24

#Result<Success, Failure>型によるエラー処理
Optional<Wrapped>型によるエラー処理では、
値が存在するか否かで成功か失敗かを決めます。

それに対し、Result<Success, Failure>型でのエラー処理では、
成功を結果の値で、失敗をエラーの詳細で表します。

つまり、Result<Success, Failure>型は、
Optional<Wrapped>型のエラーの詳細を表すことができないというデメリットを克服できます。

##実装方法

Result<Success, Failure>型は型引数を2つとる列挙型で、
.success.failureの2つのケースを持ちます。

列挙型のSuccessは成功時の値の型を表し
Failureは失敗時のエラーの型を表します。


public enum Result<Success, Failure> where Fairule : Error {
    case success(Success)   // .successの場合はSuccess型の連想値を持つ
    case success(Failure)   // .failureの場合はFailure型の連想値を持つ
}

Swiftの標準ライブラリにはエラーを表すErrorプロトコルがあるらしいのですが、
Result<Success, Failure>型の型引数Failureはエラーを表すため
Errorプロトコルに準拠する必要があるらしいです。

実際にエラー処理を手順ですが、
Result<Success, Failure>型を関数の戻り値などに指定することになります。

しかし、その前にSuccessとFailureを定義する必要があります。
Successは成功時に返す値を、
Failureは失敗時に返すエラー内容を定義します。


// Failure
enum DatabaseError : Error {
    case entryNotFound   // データが見つからないエラー
    case duplicatedEntry   // 重複したデータによるエラー
    case invalidEntry(reason: String)   // 不正なデータによるエラー
}

// Success
struct User {
    var id: Int
    var name: String
}

次に、IDからユーザを検索する関数のfindUser(byID:)を定義します。
見つかった場合はUser型の値を、
見つからない場合はDatabaseError型のentryNotFoundケースを返します。


let user1 = User(id: 1, name: "Yamaguchi")
let user2 = User(id: 2, name: "Tanaka")

var users = [user1, user2]

func findUser(byID id: Int) -> Result<User, DatabaseError> {
    for user in users {
        if user.id == id {
            return .success(user)
        }
    }
    return .failure(.entryNotFound)
}

引数に0を渡しfindUser(byID:)関数を実行しています。

関数の戻り値がresultに入るので、
バリューバインディングパターンで値を取り出します。


let id = 0
let result = findUser(byID: id)

switch result {
case let .success(user):   // 戻り値がsuccessだった場合
    print(".success: \(user)")
case let .failure(error):   // 戻り値がfailureだった場合
    switch error {   // failureの内容を取り出す
    case .duplicatedEntry:
        print(".failure: .duplicatedEntry")
    case .entryNotFound:
        print(".failure: .entryNotFound")
    case .invalidEntry(let reason):
        print(".failure: .invalidEntry(\(reason))")
    }
}

全部まとめるとこんな感じになります。


enum DatabaseError : Error {
    case entryNotFound
    case duplicatedEntry
    case invalidEntry(reason: String)
}

struct User {
    var id: Int
    var name: String
}

let user1 = User(id: 1, name: "Yamaguchi")
let user2 = User(id: 2, name: "Tanaka")

var users = [user1, user2]

func findUser(byID id: Int) -> Result<User, DatabaseError> {
    for user in users {
        if user.id == id {
            return .success(user)
        }
    }
    return .failure(.entryNotFound)
}

let id = 0
let result = findUser(byID: id)

switch result {
case let .success(user):
    print(".success: \(user)")
case let .failure(error):
    switch error {
    case .duplicatedEntry:
        print(".failure: .duplicatedEntry")
    case .entryNotFound:
        print(".failure: .entryNotFound")
    case .invalidEntry(let reason):
        print(".failure: .invalidEntry(\(reason))")
    }
}

実行結果
.failure: .entryNotFound

今回は、.failure: .entryNotFoundという結果にしかなりませんが、
関数内の処理によってエラーを変えたい場合はこのように記述すればいいと思います!


enum DatabaseError : Error {
    case entryNotFound   // データが見つからないエラー
    case duplicatedEntry   // 重複したデータによるエラー
}

struct User {
    var id: Int
    var name: String
}

let user1 = User(id: 1, name: "Yamaguchi")
let user2 = User(id: 2, name: "Tanaka")
let user3 = User(id: 2, name: "Takeda")
let user4 = User(id: 4, name: "Akimoto")

var users = [user1, user2, user3, user4]

func findUser(byID id: Int) -> Result<User, DatabaseError> {
    var userCount = 0
    var userInfo: User?
    
    for user in users {
        if user.id == id {
            userCount += 1
            userInfo = user
        }
    }
    if userCount == 0 {
        return .failure(.entryNotFound)
    } else if userCount >= 2 {
        return .failure(.duplicatedEntry)
    }
    
    return .success(userInfo!)
}

let idArray = [1,2,3,4]
for id in idArray {
    let result = findUser(byID: id)
    switch result {
    case let .success(user):
        print(".success: \(user)")
    case let .failure(error):
        switch error {
        case .duplicatedEntry:
            print(".failure: .duplicatedEntry")
        case .entryNotFound:
            print(".failure: .entryNotFound")
        }
    }
}

実行結果
.success: User(id: 1, name: "Yamaguchi")
.failure: .duplicatedEntry
.failure: .entryNotFound
.success: User(id: 4, name: "Akimoto")

実行結果から、
id:2のユーザは2つあるので重複エラーがでます。
id:3は存在しないのでデータ不明エラーがでます。
id:1と4は該当するユーザがいるので結果が表示されます!

もっと簡潔に書く方法があったかもしれませんがすぐに作れるのはこの程度でした・・・。

##利用タイミング
エラーの詳細を提供する

Optional<Wrapped>型とは違い、Result<Success, Failure>型では
連想値を通じて失敗時のエラーの値を返します。

したがって、エラー発生時には必ずエラーの詳細を受け取ることができる訳です。
そして詳細に応じてエラー処理をコントロールすることが可能になります。

エラー処理のコントロールとは、通信エラーなら何度かリトライしたり、
サーバ側のエラーならリトライせずにアラート表示をするといったような操作です。

また、エラーの内容はこちらで定義することができるので、
発生し得るエラーの内容をこちらで把握することが可能です。

このようにエラーの詳細を提供する必要のある場面では、
Result<Success, Failure>型を使用した方がいいと思います。

###補足

こんな感じでクロージャを使用したエラー処理もアリかも?
と思いましたので追記しておきます。


enum SomeError: Error {
    case error(reason: String)
    case warning(reason: String)
    
}

struct User {
    var name: String
    var age: Int
    var income: Int
}

func checkStatus(name: String, age: Int, income: Int, result: (User?, SomeError?) -> Void) {
    if age >= 18 && income <= 250 {
        result(nil, .warning(reason: "\(name)さんの年収は規定より少し低いです。"))
    } else if age < 18 {
        result(nil, .error(reason: "\(name)さんの年齢は18歳未満です。"))
    }
    
    result(User(name: name, age: age, income: income), nil)
}

checkStatus(name: "Tanabe", age: 18, income: 300) { (user, error) in
    guard error == nil else {
        print(error!)
        return
    }
    
    print(user!)
}

実行結果
User(name: "Tanabe", age: 18, income: 300)

Result型で行うエラー処理の説明は以上になります。

正直難しいですね・・・!
使って慣れないとなーと思ったので積極的にエラー処理を定義してみます。

エラー処理は絶対になくてはならない物なので皆さんも使えるようになってください!

【Swift】Optional<Wrapped>型でエラー処理を行う
【Swift】do-catchでエラー処理を行う(その1)
【Swift】fatalError関数によるプログラムの終了
【Swift】アサーションによるプログラムの終了

以上、最後までご覧いただきありがとうございました。

4
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?