LoginSignup
1
3

More than 3 years have passed since last update.

【Swift】do-catch文でエラー処理を行う(その3)

Last updated at Posted at 2020-12-26

この記事は、【Swift】do-catch文でエラー処理を行う(その2)の続きです。

前回の記事では、tryキーワードについて記載しました。
tryキーワードについてよく分かっていない型はぜひご覧ください。
また、do-catchについて理解していない方は、
【Swift】do-catch文でエラー処理を行う(その1)をご覧ください。

では続きになります。

do-catchを利用すべきとき

エラーの詳細を提供する

do-catch文では、catch節でエラーの詳細を受け取ります。
したがってエラー発生時にはエラーの詳細に合わせて処理を行うことができます。

そういった点では、Result<Success, Failure>型と同様ですが、
Result<Success, Failure>型は型引数Errorと同じ型のエラーしか扱えないのに対し、
do-catch文ではErrorプロトコルに準拠している型であればどのようなエラーでも扱えます。

Result<Success, Failure>型はどのようなエラーがあるのか予測しやすく、
do-catch文は複数の種類のエラー処理を1カ所で扱うことができるというメリットがあります。

成功か失敗のいずれかであることを保証する

Result<Success, Failure>型と同様に、do-catch文によるエラー処理も、
処理の結果が成功か失敗かのいずれかに絞られるというメリットがあります。

エラーが発生しているにも関わらずdo節が実行され続けることはありませんし、
エラーが発生していないがcatch節が実行されることは言語仕様上あり得ません。

次のサンプルコードでは、
引数にどんな値を渡されても、戻り値は成功か失敗かの二択になります。


enum SampleError: Error {
    case someError
}

func someFunc(value: Int) -> String {
    do {
        guard value < 10 else {
            throw SampleError.someError
        }
        return "Success"
    } catch {
        return "Failure"
    }
}

print(someFunc(value: 1))
print(someFunc(value: 11))

実行結果
Success
Failure

連続した処理のエラーを扱う

エラーが発生し得る処理を連続して行う時は、
Result<Success, Failure>型よりもdo-catch文の方が記述が楽になります。

今回エラーが発生し得る処理は下記の2つです。
① IDでユーザを検索し該当するユーザがいるかどうかの処理を行う
② メールアドレスの形式が正しいかチェックし正しかった場合はローカルポートの値を取得する

ローカルポートとは、testAddress@gamil.comtestAddressの部分です。

まず、Result<Success, Failure>型で記述してみます。


import Foundation

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

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

// ① IDに該当するユーザを検索
func findById(byID id: Int) -> Result<User, DatabaseError> {
    for user in users {
        if user.id == id{
            return .success(user)
        }
    }
    return .failure(.userNotFound)
}

// ② メールアドレスの形式が正しいかチェックし正しかった場合はローカルポートの値を取得
func localPort(email: String) -> Result<String, DatabaseError> {
    // メールアドレスを「@」の部分で二分割する
    let components = email.components(separatedBy: "@")
    guard components.count == 2 else {
        return .failure(.invalidEntry(reason: "メールアドレスの形式が正しくありません。"))
    }
    return .success(components[0])
}

let user1 = User(id: 1, name: "佐藤", email: "satou@gmail.com")
let user2 = User(id: 2, name: "田中", email: "tanaka.com")

let users = [user1, user2]

for user in users {
    switch findById(byID: user.id) {
    case .success(let user):
        switch localPort(email: user.email) {
        case .success(let localPort):
            print("\(user.name)さんのメールアドレスのローカルポートは、\(localPort)です。")
        case .failure(let error):
            print("Error(\(user.name)さん): \(error)")
        }
    case .failure(let error):
        print("Error: \(error)")
    }
}

実行結果
佐藤さんのメールアドレスのローカルポートはsatouです
Error(田中さん): invalidEntry(reason: "メールアドレスの形式が正しくありません。")

次にdo-catchを使用した場合のコードです。

do-catch文の場合は、エラーが発生し得る関数にはthrowsキーワードを追加しています。
そして、関数内でエラーを発生させる際にはthrowキーワードを使っています。

do-catch文のdo節内では、tryキーワードを使い関数を実行しています。
田中さんはメールアドレスの形式が正しくないのでcatch節に移行します。


import Foundation

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

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

func findById(byID id: Int) throws -> User {
    for user in users {
        if user.id == id{
            return user
        }
    }
    throw DatabaseError.userNotFound
}

func localPort(email: String) throws -> String {
    let components = email.components(separatedBy: "@")
    guard components.count == 2 else {
        throw DatabaseError.invalidEntry(reason: "メールアドレスの形式が正しくありません。")
    }
    return components[0]
}

let user1 = User(id: 1, name: "佐藤", email: "satou@gmail.com")
let user2 = User(id: 2, name: "田中", email: "tanaka.com")

let users = [user1, user2]

for user in users {
    do {
        let user = try findById(byID: user.id)
        let local = try localPort(email: user.email)
        print("\(user.name)さんのメールアドレスのローカルポートは、\(local)です。")
    } catch {
        print("Error(\(user.name)さん): \(error)")
    }
}

実行結果
佐藤さんのメールアドレスのローカルポートはsatouです
Error(田中さん): invalidEntry(reason: "メールアドレスの形式が正しくありません。")

コードの行数的には同じくらいですが、
より直感的なコードであるかなと思います。

ただ、do-catch文は非同期処理では使用することができないのでそこはご注意を。

エラー処理の強制

do-catch文はエラー処理を強制させることができます。
え?Result<Success, Failure>型もそうじゃないの?と思ったかもしれません。
実際私も最初はそう思いました。

しかし、実はResult<Success, Failure>型はエラーを無視することができます。

例えば次のようなサンプルコードになります。

checkAndAddUser( )関数では引数にUser型の値をもらいます。
引数のIDとusers配列のIDを照らし合わせていき、
IDが被っていない時は配列に追加し、IDが被っていたらエラーを発生させます。

この関数の引数は、Result<Void, DatabaseError>型になります。
成功時の値はVoid型なのでSwitch文を使い成功時の値にアクセスすることがありません。

Switch文を使わないということは、失敗時の値にアクセスすることもなくなります。
うっかりエラー処理を書くのを忘れていた!!という可能性が出てくるわけです。


import Foundation

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

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

var users = [
    User(id: 1, name: "Sato Tarou"),
    User(id: 2, name: "Tanaka Tarou")
]

func checkAndAddUser(argumentUser: User) -> Result<Void, DatabaseError> {
    for user in users {
        if user.id == argumentUser.id {
            return .failure(.duplicatedEntry)
        }
    }
    users.append(argumentUser)
    print("下記ユーザを追加しました。\nユーザID:\(argumentUser.id)\nユーザ名:\(argumentUser.name)")
    return .success(())
}

let newUser1 = User(id: 3, name: "Kondou Tarou")
checkAndAddUser(argumentUser: newUser1)

let newUser2 = User(id: 2, name: "Hakase Tarou")
checkAndAddUser(argumentUser: newUser2)   // 処理に失敗しているがエラーを無視している

実行結果
下記ユーザを追加しました
ユーザID3
ユーザ名Kondou Tarou

もし、エラー処理を追加するのであれば次のような記述をしなければなりませんでした。


switch checkAndAddUser(argumentUser: newUser2) {
case .success():
    print("")
case .failure(let error):
    print(error)
}

これに対し、do-catch文ではエラーの処理を強制されます。

do-catch文は処理の結果がどのような型であっても、
正しくエラー処理がされているかどうかがコンパイラによってチェックされています。

エラーが発生しそうな関数を定義する際はthrowsキーワードをつけなければいけませんし、
throwsキーワードを持つ関数を定義する際はtryキーワードを追加しなければなりません。

tryキーワードがついている場合は、エラー処理を意識するようになります。
do-catch文によるエラー処理はエラーを無視しづらい仕組みになっています。


import Foundation

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

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

var users = [
    User(id: 1, name: "Sato Tarou"),
    User(id: 2, name: "Tanaka Tarou")
]

func checkAndAddUser(argumentUser: User) throws -> Void{
    for user in users {
        if user.id == argumentUser.id {
            throw DatabaseError.duplicatedEntry
        }
    }
    users.append(argumentUser)
    print("下記ユーザを追加しました。\nユーザID:\(argumentUser.id)\nユーザ名:\(argumentUser.name)")
    return
}

let newUser1 = User(id: 3, name: "Kondou Tarou")
let newUser2 = User(id: 2, name: "Hakase Tarou")

do {
    try checkAndAddUser(argumentUser: newUser1)
    try checkAndAddUser(argumentUser: newUser2)
}

以上でdo-catch文の説明を終わりにしたいと思います!
3記事にもおよぶ長い説明になってしまいすみませんでした。

この記事をご覧になってコードの解決につながりましたら幸いです。

また、他のエラー処理についても記事にしているのでぜひご覧ください。
【Swift】Optional<Wrapped>型でエラー処理を行う
【Swift】Result<Success, Failure>型でエラー処理を行う
【Swift】fatalError関数によるプログラムの終了
【Swift】アサーションによるプログラムの終了

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

1
3
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
1
3