【Swift】エラーハンドリングを学び直す！ Swift2.2 から Swift3.0での変更点, Swift4のドキュメント紹介まで

2016/08/23 追記：Swift3.0でのErrorプロトコルについて(SE-0112)
2016/10/12 編集：@yusugaさんよりコメントをいただきPerfectの例についての記述を修正
2017/09/30 追記：Swift4についての記述を追加

# はじめに

これまで、エラーハンドリングをしっかりしていなかったので改めて調べてみました。
備忘録として残し、「処理失敗したらとりあえず`false`返す」みたいなことは卒業します。

また、Swift3.0も近づいてきているということもあり、これまでのSwift2.2のエラーハンドリングを振り返り、最後にSwift3.0で変わった点を記していきます。

Swiftの公式リファレンスを基にしていますが、自分なりにまとめた内容なので、語弊や間違った解釈があるかもしれません。そのような点があればご指摘ください :raised_hands: 


# そもそもエラーハンドリングとは？

ググってみると「エラーが起きた時の対応処理」というような解釈がたくさん出てきますが、個人的にしっくりきたのは以下の文脈での使われ方でした。

> ソフトウェアの中で発生するエラーは多岐にわたる。エラーには、代替策をとれば問題なく先へ進めるものや、プログラム自身が問題で修復できるものもある一方で、別のコンピュータへ役割を引き継いだり、人間の介入を待たなくてはならないものまで多種多様である。
~（中略）~
これらの**エラーに適切に対処すること（エラーハンドリング）**は非常に重要である。

引用：[IPA ISEC　セキュア・プログラミング講座：C/C++言語編　第6章 フェイルセーフ：体系だてたエラーハンドリング](https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/c502.html)


つまり、**正常な動作を阻害する挙動が生じた時に、それをエラーとし、そのエラーの内容に応じて適切にエラー状態を回復させる処理を行う**ことをエラーハンドリングというのだと思います。

また、Swift2.2の[公式リファレンス](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html)では、以下のように表現されています。

> Error handling is the process of responding to and recovering from error conditions in your program.

訳すと、「プログラムのエラー状態に応じて、そのエラー状態を回復するプロセス」といった感じでしょうか？（間違っていたらご指摘ください、、）

# エラーハンドリングのポイント

エラーハンドリングがどういうものかを確認したところで、
エラーハンドリングを行うにあたって、どのようなことが重要なのかが少し見えてきました。
（ポイントというか、前提みたいなものも含んでいますが、、）

1. プログラマが、どこでどのようなエラーが発生しうるかを把握できていること
- 1を踏まえて、プログラム上でエラーの内容が事前に定義されている（判別可能な状態になっている）こと
- 2で判別可能な状態にされたエラーの内容に応じて、適切な処理が行われること

例えば、何か処理を行って、その処理が成功した時には`true`を、失敗した時には`false`を返すようなメソッドはよく見るかと思います。

このようなメソッドを呼んだ際に、`false`が返ってきた時の処理が同一のもので対応出来る場合はこのような形でいいかと思います。

しかし、「処理が失敗した」という中にも、何が原因でどのように失敗したかによってリカバリーの処理を分けたい場合には、`false`を返すだけではうまく対応できないでしょう。

このような時に以降で述べるような構文を使ってエラーハンドリングを行うと良さそうです。

ようやく本題ですが、Swiftでのエラーハンドリングを見ていきましょう。


# Swift2.2でのエラーハンドリング

## エラーを定義する

### `ErrorType`プロトコル

Swiftでは、エラーの内容を`ErrorType`というプロトコルに準拠した列挙型で定義することで便利にエラーハンドリングを行えるようです。

```swift
enum VendingMachineError: ErrorType {
    case InvalidSelection
    case InsufficientFunds(coinsNeeded: Int)
    case OutOfStock
}
```

この`ErrorType`プロトコル自体には、何かプロパティやメソッドが定義されているわけではなく、空のプロトコルです。

空のプロトコルである`ErrorType`プロトコルにわざわざ準拠させる理由は、明示的なプロトコルの準拠を行うことでエラーハンドリングのための定義であるということを誰にでも明確に指し示すことができるためです。

## エラーを投げる

### `throw`文

`throw`文を使うことで、エラーを投げることができます。

```swift
throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)
```

### エラーが発生しうるメソッドの定義

エラーを投げるメソッドを定義するときには、次のように、メソッドの`()`の後ろに`throws`を書きます。

```swift
func canThrowErrors() throws -> String
```

実装では、上記の`throw`文と組み合わせて、次のようにエラーを投げることができます。
※`throws`でエラーを投げる可能性があることを宣言していても、必ずしもエラーを投げないといけないというわけではありません。

```swift
func canThrowErrors() throws -> String {
    if 条件文 {
        throw VendingMachineError.InsufficientFunds(coinsNeeded: 5)
    }
}
```

### 実装例

リファレンスの例を紹介します。

```swift

enum VendingMachineError: ErrorType {
    case InvalidSelection
    case InsufficientFunds(coinsNeeded: Int)
    case OutOfStock
}

struct Item {
    var price: Int
    var count: Int
}
 
class VendingMachine {
    var inventory = [
        "Candy Bar": Item(price: 12, count: 7),
        "Chips": Item(price: 10, count: 4),
        "Pretzels": Item(price: 7, count: 11)
    ]
    var coinsDeposited = 0
    func dispenseSnack(snack: String) {
        print("Dispensing \(snack)")
    }
    
    func vend(itemNamed name: String) throws {
        guard let item = inventory[name] else {
            throw VendingMachineError.InvalidSelection
        }
        
        guard item.count > 0 else {
            throw VendingMachineError.OutOfStock
        }
        
        guard item.price <= coinsDeposited else {
            throw VendingMachineError.InsufficientFunds(coinsNeeded: item.price - coinsDeposited)
        }
        
        coinsDeposited -= item.price
        
        var newItem = item
        newItem.count -= 1
        inventory[name] = newItem
        
        dispenseSnack(name)
    }
}

```

この例では`vend`メソッドでエラーが発生する可能性があることを宣言しています。

```swift
func vend(itemNamed name: String) throws
```

そして、`guard`構文を使用して、列挙型で定義した３種類のエラーを投げています。

この`vend`メソッドは、以下のようにエラーを投げない実装も考えられますが、
この場合、失敗は一律で`false`が返ってくるので、どの`guard`文で`false`が返ったのか分からず、エラーに応じた処理ができなくなってしまいます。

```swift:エラーをthrowしない場合
func vend(itemNamed name: String) -> Bool {
    guard let item = inventory[name] else {
        return false
    }
        
    guard item.count > 0 else {
        return false
    }
        
    guard item.price <= coinsDeposited else {
        return false
    }
        
    coinsDeposited -= item.price
        
    var newItem = item
    newItem.count -= 1
    inventory[name] = newItem
        
    dispenseSnack(name)
    return true
}
```

こういった対応が適切でない場合は、前述のように`throws`をつけてエラーを投げるようにした方が良さそうです。

### エラーの伝搬

先ほどの実装例での`vend`メソッドを実行するメソッドを考えます。

```swift
let favoriteSnacks = [
    "Alice": "Chips",
    "Bob": "Licorice",
    "Eve": "Pretzels",
]
func buyFavoriteSnack(person: String, vendingMachine: VendingMachine) throws {
    let snackName = favoriteSnacks[person] ?? "Candy Bar"
    try vendingMachine.vend(itemNamed: snackName)
}
```

`buyFavoriteSnack`メソッドは、中で`vend`メソッドを実行しています。
このように、メソッド内の処理でエラーを投げるメソッドを実行する場合には、

- 後述の`try!`または`try?`を使用するなど、エラーに対応する処理をメソッド内で行う
- `try`のみを行って、エラーを伝播させる（そのメソッド自身も同じエラーを投げて、エラー処理を外側に任せる）

のどちらかのアプローチが必要になります。
上記例では、後者のアプローチを行っているため、`buyFavoriteSnack`メソッドも`vend`メソッド同様のエラーを投げます。


### エラーを投げる`Initializer`

Initializerでも同様にエラーを投げることができます。

```swift
struct PurchasedSnack {
    let name: String
    init(name: String, vendingMachine: VendingMachine) throws {
        try vendingMachine.vend(itemNamed: name)
        self.name = name
    }
}
```


## エラーを検知して処理をする

これまでは、エラーを投げる側の実装を見てきましたが、ここからはエラーを受けとる側の処理を見ていきます。

### do-catch構文

エラーハンドリングを行うときは、`do`ブロックの中で`try`をつけてエラーを投げる可能性のあるメソッドを実行します。
そして、`catch`ブロックでエラーのパターンマッチングを行い、それぞれにあった対応を行うことができます。

```swift
do {
    try canThrowErrors() // エラーを投げる可能性のあるメソッド
    // 成功した時の処理
} catch ErrorPattern { // エラーのパターンマッチング
    // エラーの処理
}
```

こちらも先ほどの例で見た方がわかりやすいかと思います。

```swift
var vendingMachine = VendingMachine()
vendingMachine.coinsDeposited = 8
do {
    try buyFavoriteSnack("Alice", vendingMachine: vendingMachine)
} catch VendingMachineError.InvalidSelection {
    print("Invalid Selection.")
} catch VendingMachineError.OutOfStock {
    print("Out of Stock.")
} catch VendingMachineError.InsufficientFunds(let coinsNeeded) {
    print("Insufficient funds. Please insert an additional \(coinsNeeded) coins.")
}
```

`buyFavoriteSnack`メソッドを`do`ブロック内で`try`をつけて実行しています。
もしエラーが投げられた場合は、`do`ブロックの下に続く`catch`ブロックで処理がされます。
例の場合、`catch`ブロックが３つ設けられていますが、投げられたエラーにマッチするブロックの処理が実行されます。


### その他の処理の仕方

上記のようにdo-catch構文を用いて、エラーの内容に沿った処理を行うこともできますが、それ以外に、以下のような処理も可能です。

#### `try?`文

これは、`try`でエラーが投げられた際に、エラーをオプショナルに変換して処理することができます。
次の`x`と`y`は同じ結果が得られます。

```swift
func someThrowingFunction() throws -> Int {
    // ...
}
 
let x = try? someThrowingFunction()
 
let y: Int?
do {
    y = try someThrowingFunction()
} catch {
    y = nil
}
```

#### `try!`文

`try!`文は、エラーの伝播を停止します。
確実にエラーが投げられないと分かっている場合には、わざわざエラーハンドリングをする必要がないので、`try!`文を使用して通常の処理のみを書いていくことができます。

ただし、`try!`文で実行されたメソッドでエラーが投げられた場合には、実行時エラーになってしまうので、気をつけて使用した方が良さそうです。

```swift
let photo = try! loadImage("./Resources/John Appleseed.jpg")
```


# Swift3.0でのエラーハンドリング

Swift3.0では、エラーを定義した列挙型を準拠させていた`ErrorType`プロトコルが~~`ErrorProtocol`~~`Error`プロトコルに変更されます。

[SE-0006](https://github.com/apple/swift-evolution/blob/master/proposals/0006-apply-api-guidelines-to-the-standard-library.md)


==追記：2016/08/23=====
SE-0112でErrorProtocolではなく、Errorになっているようですね。
- https://github.com/apple/swift-evolution/blob/master/proposals/0112-nserror-bridging.md
- https://github.com/apple/swift/commit/823c24b355017e3797c665c656b48fc61f01bc85
==追記ここまで==========


また、エラーハンドリングとは直接的に関係ないですが、Swift3.0では、型とプロトコルの名前以外は`lowerCamelCase`で書くようにガイドラインが作成されています。
そのため、`Error`に準拠した列挙型を作成する際には、その点の注意も必要です。

```swift:前述の例をSwift3.0で書き直すと
enum VendingMachineError: Error {
    case invalidSelection
    case insufficientFunds(coinsNeeded: Int)
    case outOfStock
}
```

## Swift3.0で`Error`プロトコルを使用している例 (2016/10/12時点)
例えば、Server-Side Swiftのフレームワークである`Perfect`でも、Swift3.0のコードとして、`Error`プロトコルを使用しています。

https://github.com/PerfectlySoft/Perfect/blob/master/Sources/PerfectLib/JSONConvertible.swift#L100


# Swift4 でのエラーハンドリング

Swift4ではSwift3からの変更はないので、これまで通り書くことが出来そうです。

公式のドキュメントは下記になります。

https://developer.apple.com/library/content/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html

# 終わりに

以上、エラーハンドリングについて調べてみました。
正しくエラーハンドリングを行ってより良いプロダクトを作っていきたいですね。

間違っている点などありましたら、是非ご指摘ください。 :pray: 

# 参考
- [The Swift Programming Language (Swift 2.2): Error Handling](https://developer.apple.com/library/ios/documentation/Swift/Conceptual/Swift_Programming_Language/ErrorHandling.html)
- [swift-evolution/0006-apply-api-guidelines-to-the-standard-library.md at master · apple/swift-evolution](https://github.com/apple/swift-evolution/blob/master/proposals/0006-apply-api-guidelines-to-the-standard-library.md)
- [#swiftlang 💥 Examples of Swift 2.2 & 3.0 Compatibility - JPMartha's Pancake](http://jpmartha.hatenablog.jp/entry/2016/03/28/220739)