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

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

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

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

処理が失敗し、目的の値が得られなかったことを伝える方法は大きく分けて**2種類**のみです。
1. nilを返す(Optional型を用いる)
1. Errorをthrowする

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

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

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

## 1. nilを返す

### a. 生成におけるエラーのハンドリング
例えば、特定の値しか取り得ないenumで、case外のrawValueを指定した時は、コンパイルエラーではなく`nil`が返ります。
このように、「型は同じだが想定外の値によって生成した場合」をハンドリングしたい場合には、**failable initializer**という方法を用いてnilが返るようにします。
```swift
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`を読み取りたい場合です。
```swift
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`を返すようになります。
```swift
print(data.stringData.text) // stringDataがない場合があるため、コンパイルエラー
print(data.stringData?.text) // this is data
```

### 複数の値を同時にハンドリングする場合
Swiftには`compactMap`と呼ばれるメソッドが存在し、これによりOptional要素からなる配列を`nil`を排除した非Optional型配列に変換してくれます。
```swift
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を正の整数に変換する関数」があったとします。
```swift
func getPositiveInt(from string: String) throws -> Int {
    if let intValue = Int(string) {
        if intValue >= 0 {
            return intValue
        }
        // 負の場合、エラー。
    } else {
        // Intに変換できない場合、エラー。
    }
}
```
上記の場合、考えうるエラーとしては、「Intに変換できない場合」「Intが負数の場合」の2パターンです。
それを下記のようにErrorプロトコルに準拠させた形で、全case洗い出します。
```swift
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する形に書き換えます。
```swift
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`で囲むことによりハンドリングできます。
```swift
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において例外処理は以下のように記述します。
```swift
var currentValue: String = "-1"
var positiveIntValue: Int {
    get throws {
        return try getPositiveInt(from: currentValue)
    }
}
```
あとは`do-catch`で囲むと、関数の返り値を代入する場合とほとんど同じように書くことができます。
```swift
do {
    value = try positiveIntValue
} catch let error as GetPositiveIntError {
    print(error.message)
} catch {
    print("unknown error occurred")
}
```
### b. 代入におけるエラーのハンドリング
[こちら](#Errorプロトコルの作成方法)で説明している方法です。

### 複数の処理におけるエラーを同時にハンドリングする場合
以下のような関数を複数回呼ぶ場合を考えます。
```swift
func getStringRandomly() throws -> String {
    if Bool.random() {
        return "someValue"
    } else {
        throw GetStringError.fail
    }
}

enum GetStringError: Error {
    case fail
}
```
これを複数回呼び出してエラーをハンドリングするには二通りの方法があります。
一つ目は全てのエラーを内部で処理する方法、二つ目はエラーを`rethrow`する方法です。
#### エラーを内部で処理する方法
```swift
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する方法
```swift
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である`!`を使う方法です。
```swift
// 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する方法でのエラーハンドリング
  - まずは起こりうるエラーを`enum`の`case`で洗いだす
  - 生成時のエラーを処理したい場合はgetterに`throws`を付与する
  - 代入時のエラーを処理したい場合は関数自体に`throws`を付与する
  - どちらの場合も、throwしたエラーは`do-catch`で囲んで処理できる
  - 複数同時に処理したい場合は、内部を`do-catch`で囲むか、又は`rethrows`を使って内部のエラーをそのまま関数のエラーとして送出させる
    - 内部を`do-catch`で囲む場合、指定した回数必ず呼ばれるが、`rethrows`を使用した場合、指定した回数呼ばれない場合がある
- `fatalError`もエラーをハンドリングする一つの方法だが、使用はできる限り控えなければならない

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