0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Swiftにおけるエラーハンドリングまとめ

Posted at

アプリにおいては様々な処理が実行されますが、それらのプロセスのいずれかにおいては必ず処理の失敗(エラー)が発生します。

アプリを作る際には、どのようなエラーが起こりうるのか、またそれらをどのようにユーザーに伝えるのかを考えながら実装しなくてはいけません。

この記事はそうしたエラーハンドリングの基本について記述します。

エラーハンドリングの種類

処理が失敗し、目的の値が得られなかったことを伝える方法は大きく分けて2種類のみです。

  1. nilを返す(Optional型を用いる)
  2. Errorをthrowする

また、全ての値は生成または代入によって生じるので、エラーが生じうる選択肢としては
a. 生成においてエラーが発生した場合
b. 代入においてエラーが発生した場合

2通りが考えられると思います。

以下では、a,bそれぞれの発生原因において1,2のエラーハンドリングを行う場合のやり方を紹介していきます。

1. nilを返す

a. 生成におけるエラーのハンドリング

例えば、特定の値しか取り得ないenumで、case外のrawValueを指定した時は、コンパイルエラーではなくnilが返ります。
このように、「型は同じだが想定外の値によって生成した場合」をハンドリングしたい場合には、failable initializerという方法を用いてnilが返るようにします。

enum EnumData: String {
    case a
    case b
}

class Data {
    var enumData: EnumValueData?

    init(enumData: EnumValueData? = nil) {
        self.enumData = enumData
    }
}

class EnumValueData {
    let data: EnumData

    // failable initializerによって、enum外のrawValueを指定した場合、この`EnumValueData`インスタンス自体がnilになる。
    init?(rawValue: String) {
        guard let data = EnumData(rawValue: rawValue) else {
            return nil
        }
        self.data = data
    }
}

print(EnumValueData(rawValue: "a")) // Optional(EnumValueData)
print(EnumValueData(rawValue: "c")) // nil

b. 代入におけるエラーのハンドリング

Swiftは型付け言語なので、代入においては型が必ず期待通りの値になるため、代入そのものでエラーが生じることはほとんどありません。

しかし、Optional型の値からプロパティを読み取る場合のみ、注意が必要です。
例えば、以下のようなdataから、textを読み取りたい場合です。

class Data {
    var stringData: StringValueData?

    init(stringData: StringValueData? = nil) {
        self.stringData = stringData
    }
}

class StringValueData {
    var text: String?

    init(text: String? = nil) {
        self.text = text
    }
}

let data = Data(stringData: .init(text: "this is data"))

この場合、stringDataが存在しない場合がありますので、data.stringData.textという書き方をしてしまうと、textというプロパティがそもそも存在しないのでエラーとなります(実際にはSwiftのコンパイルエラーが出るため、このようなことは起こりません)。

このような際には、Optional Chainingと呼ばれる、nilになりうる値に?をつける方法を用いると、textのような下部プロパティを読み込む前に、その値自体がnilを返すようになります。

print(data.stringData.text) // stringDataがない場合があるため、コンパイルエラー
print(data.stringData?.text) // this is data

複数の値を同時にハンドリングする場合

SwiftにはcompactMapと呼ばれるメソッドが存在し、これによりOptional要素からなる配列をnilを排除した非Optional型配列に変換してくれます。

let dataArray = [Data(enumData: EnumValueData(rawValue: "a")),
                 Data(enumData: EnumValueData(rawValue: "aaa")),
                 Data(enumData: EnumValueData(rawValue: "b"))]
print(dataArray.map { $0.enumData }) // [Optional(EnumValueData), nil, Optional(EnumValueData)]
print(dataArray.compactMap { $0.enumData }) // [EnumValueData, EnumValueData] <- 真ん中のnilは排除されている。

2. Errorを返す

Swiftでは例外処理を発生させる方法として、throwを使うことができます。
それらの例外処理は、throw可能なprotocolであるErrorプロトコルに準拠した構造体を作成することにより定義可能です。

Errorプロトコルの作成方法

送出される例外は複数の選択肢の中の1つのみ、という特性はenumと非常に相性がいいです。
例えば、以下のような「Stringを正の整数に変換する関数」があったとします。

func getPositiveInt(from string: String) throws -> Int {
    if let intValue = Int(string) {
        if intValue >= 0 {
            return intValue
        }
        // 負の場合、エラー。
    } else {
        // Intに変換できない場合、エラー。
    }
}

上記の場合、考えうるエラーとしては、「Intに変換できない場合」「Intが負数の場合」の2パターンです。
それを下記のようにErrorプロトコルに準拠させた形で、全case洗い出します。

enum GetPositiveIntError: Error {
    case negativeIntError
    case notNumberError

    var message: String {
        switch self {
            case .negativeIntError:
                return "negative value detected"
            case .notNumberError:
                return "cannot convert to number"
        }
    }
}

そしてgetPositiveInt関数で上記throwする形に書き換えます。

func getPositiveInt(from string: String) throws -> Int {
    if let intValue = Int(string) {
        if intValue >= 0 {
            return intValue
        }
        throw GetPositiveIntError.negativeIntError // 追加
    } else {
        throw GetPositiveIntError.notNumberError // 追加
    }
}

定義したエラーは、以下のようにtryを付与した関数をdo-catchで囲むことによりハンドリングできます。

var value: Int?
do {
    value = try getPositiveInt(from: "-1")
} catch let error as GetPositiveIntError {
    print(error.message) // negative value detected
} catch {
    print("unknown error occurred")
}
print(value)

a. 生成におけるエラーのハンドリング

computed propertyにおいて例外処理は以下のように記述します。

var currentValue: String = "-1"
var positiveIntValue: Int {
    get throws {
        return try getPositiveInt(from: currentValue)
    }
}

あとはdo-catchで囲むと、関数の返り値を代入する場合とほとんど同じように書くことができます。

do {
    value = try positiveIntValue
} catch let error as GetPositiveIntError {
    print(error.message)
} catch {
    print("unknown error occurred")
}

b. 代入におけるエラーのハンドリング

こちらで説明している方法です。

複数の処理におけるエラーを同時にハンドリングする場合

以下のような関数を複数回呼ぶ場合を考えます。

func getStringRandomly() throws -> String {
    if Bool.random() {
        return "someValue"
    } else {
        throw GetStringError.fail
    }
}

enum GetStringError: Error {
    case fail
}

これを複数回呼び出してエラーをハンドリングするには二通りの方法があります。
一つ目は全てのエラーを内部で処理する方法、二つ目はエラーをrethrowする方法です。

エラーを内部で処理する方法

func executeGetStrings(for times: Int,
                       execution: () throws -> Void) {
    for _ in 0..<times {
        do {
            try execution()
        } catch {
            print("failed to get string")
        }
    }
}

executeGetStrings(for: 7) {
    print(try getStringRandomly())
} // someValue or failed to get string x7

こちらのように、エラーをキャッチした時の処理を指定してあげることで複数回の呼び出しに対して同時にエラーの対処ができます。
関数内部で起こったエラーは関数自体に影響を及ぼさないので、指定した回数必ず実行されます。

エラーをrethrowする方法

func executeGetStringsThrows(for times: Int,
                             execution: () throws -> Void) rethrows {
    for _ in 0..<times {
        try execution()
    }
}

do {
    try executeGetStringsThrows(for: 7) {
        print(try getStringRandomly())
    }
} catch {
    print(error)
} // someValue x0 ~ x7 -> fail

上記のように、rethrowsという文法を使ってあげると、関数内部のエラーをそのまま関数のエラーとして送出してくれます。
do-catchで囲んであげた場合には、一度でも失敗したらcatchに行くので指定した回数に満たないままエラーが送出されます。

番外編: クラッシュを引き起こす

Swiftで例外を処理する方法として、プログラムをストップさせると言った方法も存在します。
それがfatalError()を使う、またはOptional型のforce-unwrappingである!を使う方法です。

// fatalErrorを使う
do {
    try executeGetStringsThrows(for: 7) {
        print(try getStringRandomly())
    }
} catch {
    fatalError("fail!!")
} // Fatal error: fail!!

// force-unwrapping(!)を使う
try! executeGetStringsThrows(for: 7) {
    print(try getStringRandomly())
} // Fatal error: 'try!' expression unexpectedly raised an error

しかし、これらは(特にプロダクションで)極力使うべきではありません。
可能な限り、上記二つのエラーハンドリングのうちどちらかを使い、プログラムが停止するのを避けるべきです。

まとめ

  • Swiftにおけるエラーハンドリングは2種類:
    • nilを返す方法
    • Errorプロトコルに準拠したstructをthrowする方法
  • nilを返す方法でのエラーハンドリング
    • 生成時のエラーを処理したい場合はfailable initializerを使う
    • 代入時のエラーを処理したい場合はoptional chainingを使う
    • 複数同時に処理したい場合はcompactMapを使う
  • Errorをthrowする方法でのエラーハンドリング
    • まずは起こりうるエラーをenumcaseで洗いだす
    • 生成時のエラーを処理したい場合はgetterにthrowsを付与する
    • 代入時のエラーを処理したい場合は関数自体にthrowsを付与する
    • どちらの場合も、throwしたエラーはdo-catchで囲んで処理できる
    • 複数同時に処理したい場合は、内部をdo-catchで囲むか、又はrethrowsを使って内部のエラーをそのまま関数のエラーとして送出させる
      • 内部をdo-catchで囲む場合、指定した回数必ず呼ばれるが、rethrowsを使用した場合、指定した回数呼ばれない場合がある
  • fatalErrorもエラーをハンドリングする一つの方法だが、使用はできる限り控えなければならない

最後に

こちらは私が書籍で学んだ学習内容をアウトプットしたものです。
わかりにくい点、間違っている点等ございましたら是非ご指摘お願いいたします。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?