[Swift] Arrayの範囲外アクセスでXCTAssertThrowsErrorを満足させられると思っていた

## 環境

Xcode 9.2
Swift 4.0.3

## 語句

### 例外

本記事では、[こちらの記事](https://qiita.com/koher/items/a7a12e7e18d2bb7d8c77)における「Recoverable error」、
すなわち`throw xxx(何らかのエラー)`と`do~try~catch`で表現されるエラーのみを「例外」と記述します。
(つまり本記事内において、`Logic failure`は、「例外」という書き方はしないことにします。)

## まとめ

- アサーションだとかそれ以前に、範囲外アクセス`(precondition)`でプログラム落ちてる
- **Arrayの範囲外アクセスは「例外」ではない**ので、`XCTAssertThrowsError`は感知しない


## はじめに

先日、1つの記事を契機に、
「Arrayの範囲外アクセスは例外を投げる」と思っていた自身の誤った理解に気づくことができ、
Swiftのエラーにまつわる理解も深まりました。

万が一、同様の疑問を抱いた方がいた場合、なんらかの標になればという傲慢心のもと、本記事を投稿いたします。

## 登場人物

### Array

配列。SwiftのArrayは、範囲外アクセスで実行時エラーとなる

```swift
let array = [1,2,3,4,5]
array[5]  //  Fatal error: Index out of range
```

### XCTAssertThrowsError

`XCTAssertThrowsError(_:_:file:line:_:)`

標準テストフレームワークである`XCTest`のアサーション関数の1つ
第1引数に渡した式が例外を投げたときはアサーション成立(=テスト成功)、
投げなかったときに失敗(=テスト失敗)。

下記の例は必要以上に装飾が多いですが、
`XCTAssertThrowsError`に渡された式が例外を投げ、それによりアサーションが成立し、
テストが成功している、ただそれだけのことをお伝えしたかったのでした。  

```swift
// PlaygroundでXCTestをしている想定(要参考リンク)

import XCTest

enum Error: Swift.Error {
    case myError
}

class Test: XCTestCase {
    static func throwErrorfunc() throws {
        throw Error.myError
    }
    func testInitialize() {
        // 渡された式が例外を投げたらアサーション成功、さもなければ失敗
        XCTAssertThrowsError(try Test.throwErrorfunc()) // 例外発生
    }
}
// テスト実行
Test.defaultTestSuite.run()

// 結果: テスト成功
Executed 1 test, with 0 failures
```

## 配列の範囲外アクセスの場合

以下の例を見てみます。

当初、私の思考回路は、
「Arrayの範囲外アクセスによるエラーのため、アサーションが成立し、テストは成功する」
という流れを辿りました。
「`XCTAssertThrowsError`は、エラーであれば何でも感知し、アサーションが成立する」
そう考えていたのです。

```swift
import XCTest

class Test: XCTestCase {
    func testOutOfRange() {
        let array = [1,2,3]

        // 「範囲外アクセスのエラーをXCTAssertThrowsErrorが捕捉してくれるに違いない。
        //  エラーが発生するのだから、アサーションが成立。よってテストは成功するに違いない。」
        XCTAssertThrowsError(array[3])
}
```

しかし、上記のコードを実行しても、
`XCTAssertThrowsError`が例外時アクセスに感知することはありませんでした。
(テスト結果の判明を待つことなく、
単純に範囲外アクセスが発生し、プログラムが終了してしまいました)

```swift
// テスト実行
Test.defaultTestSuite.run()

// 結果: テストの終了を待たずに範囲外アクセスが発生し、プログラムが終了してしまう
Fatal error: Index out of range
```

「範囲外アクセスを`XCTAssertThrowsError`が捉え、結果、テストは成功するはず」
そんな浅薄な目論見は、こっぱみじんに砕かれました。

どうしてこのような己が不明を恥じる結果になってしまったのでしょうか。

## まとめ(再)

- そもそもアサーションだとかそれ以前に、範囲外アクセス`(precondition)`でプログラム落ちてるので、`XCTAssertThrowsError`は最後まで実行されない
- **Arrayの範囲外アクセスは「例外」ではない** (`Recoverable error` ではなく、`Logic failure`)ので、`XCTAssertThrowsError`はこれに感知しない

## 詳しく

[当該記事](https://qiita.com/koher/items/a7a12e7e18d2bb7d8c77)の語句を用いながら説明してみます。

Swiftには4種類のエラー類型が存在し、
そのうち、今回問題になっているArrayへの範囲外アクセスは`Logic failure`に該当します。

これは、別のエラー類型である `Recoverable error` とは異なるものです。
Swiftで「例外」といった時、一般にはこちらを指す場合が多いように思います。

そして、`XCTAssertThrowsError`のアサーションを成立させることができるのは、
この`Recoverable error`にあたるものだけなんですね。

`Array`の`subscript`は、内部的には precondition を使い、
範囲外アクセスかどうかをチェックし、実行時エラーを発生させているのでしょう。
(該当するソースコードは[これ](https://github.com/apple/swift/blob/master/stdlib/public/core/Arrays.swift.gyb#L925)かな、と思いましたが、
エラーメッセージが微妙に異なる気がしており、確証には至っておりません。)
ただし、`subscript`が発生させているのはあくまで`precondition`によるエラーであり、
例外を投げているわけではない、というわけですね...。

`XCTAssertThrowsError`は「例外」を感知し、アサーションを行う関数なので、
その「例外」に該当しない範囲外アクセスに感知することは、初めからあり得ませんでした。

([その実装](https://github.com/apple/swift/blob/master/stdlib/public/SDK/XCTest/XCTest.swift#L651)はごく簡単にしか追えておらず理解が浅いのですが、
仮にSwiftの範囲外アクセスがRubyのように`nil`を返すような実装だったとしても、
このアサーション関数を満足させられないように思います...
(`nil`に対するアサーション関数は`XCTAssertNil`ないし`XCTAssertNotNil`でしょうし。
この点、誤認がありましたら、ご指摘いただけると幸いです。
XCTestのアサーション関数群を、流れを追えるようXcode上でステップ実行する方法ってあるのでしょうか...)

範囲外アクセスを検知・処理し、復帰させる手段は、
(Swift4.xの時点では)用意されていない(そもそも復帰させてはいけない)、ということになっています。
プログラマは単純にコードを訂正し、範囲外アクセスを引き起こさないコードを書くほかありません。

## おわりに

冒頭からお伝えしているように、`XCTAssertThrowsError`が範囲外アクセスに感知できない理由は、
エラー4類型だとかアサーションだとかそれ以前に**「そもそも範囲外アクセスによりプログラムが落ちてる」**からなのですが、
見当違いの方向なりに調査を進めた結果、はからずも

- 範囲外アクセスは「例外」(`Recoverable error`)ではなく、ゆえに復帰手段も存在しない

そして、

- Swiftには明確なエラー類型が存在し、標準ライブラリもそれに基づき実装されている

ということを知ることができました。

Swiftは、その都度生じた疑問を掘り下げていくことで、
背後に湛える精緻な理論の断片や、納得感のある論拠に幾度も引き合わせ、
書き手に大いなる愉悦をもたらしてくれます。

## links

[Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい](https://qiita.com/koher/items/a7a12e7e18d2bb7d8c77)
[Xcode PlaygroundでUnit Testを走らせる](https://qiita.com/himara2/items/c763dcd3903e75901d7c)