Swiftのpreconditionとassertの使い分け

`precondition` と `assert` は似ているので、どういうケースでどちらを使うべきか意識してないと不適切な使い方をしてしまいます。本投稿では、 `precondition` と `assert` をどのように使い分けるべきかについて説明します。

## `precondition`, `assert` って何？

`precondition` と `assert` を知らない方のために、初めにそれらについて簡単に説明します。知っている方は本節は読み飛ばして下さい。

### `precondition`

`precondition` は条件を指定し、その条件が満たされなかった場合に実行時エラーとしてクラッシュさせることができます。

↓のコードでは `x >= 0` という条件がチェックされています。

```swift
func foo(x: Int) {
  precondition(x >= 0)
  print(x) // `x` が 0 以上のときだけ実行される
}
```

たとえば、 `x` に `42` を渡すのは大丈夫ですが、 `-1` を渡すとクラッシュします。

```swift
foo(x: 42) // OK
foo(x: -1) // NG
```

### `assert`

`assert` も条件をチェックし、条件が満たされない場合は実行時エラーになります。

```swift
func foo(x: Int) {
  assert(x >= 0)
  print(x) // `x` が 0 以上のときだけ実行される
}
```

`precondition` 同様に `42` を渡すのは OK ですが `-1` を渡すとクラッシュします。

```swift
func foo(x: Int) {
  assert(x >= 0)
  print(x) // `x` が 0 以上のときだけ実行される
}

foo(x: 42) // OK
foo(x: -1) // NG
```

`precondition` のときとまったく同じですね。では、この二つは何が違うのでしょうか？

## `precondition` と `assert` の違い

`precondition` と `assert` は最適化を行った際の挙動が異なります。

Swift コンパイラには

- `-Onone`
- `-O`
- `-Ounchecked`

の三つの最適化レベルが用意されています。 `precondition` は `-O` のときもチェックが行われますが、 `assert` は `-O` でチェックが行われません。これが `precondition` と `assert` の違いです。

| 関数 | `-Onone` | `-O` | `-Ounchecked` |
|:---|:--:|:--:|:--:|
| precondition | ○ | ○ |  |
| assert | ○ |  |  |

（※ ○が付いていない組み合わせはチェックが省略される。）

### `precondition`

実際にさっきのコード（↓）を `-O` で実行するとどうなるか見てみましょう。

```swift:precondition.swift
func foo(x: Int) {
  precondition(x >= 0)
  print(x) // `x` が 0 以上のときだけ実行される
}

foo(x: 42) // OK
foo(x: -1) // NG
```

これを `-O` を付けて実行してみます。

```
$ swift -O precondition.swift
```

そうすると↓のように、 `42` は表示されますがその直後で実行時エラーとなっています。

```
42
0  swift                    0x000000010760d58a
PrintStackTraceSignalHandler(void*) + 42
1  swift                    0x000000010760c9c6
SignalHandler(int) + 662
2  libsystem_platform.dylib 0x00007fffa16cdb3a
_sigtramp + 26
...
Illegal instruction: 4
```

### `assert`

`assert` でも同じことををやってみます。

```swift:assert.swift
func foo(x: Int) {
  assert(x >= 0)
  print(x) // `x` が 0 以上のときだけ実行される
}

foo(x: 42) // OK
foo(x: -1) // NG
```

```
$ swift -O assert.swift
```

なんと `-1` が表示されます。

```
42
-1
```

`assert` のチェックが利いていないことがわかります。

もちろん `-O` をなくすと（デフォルトで `-Onone` になります）

```
$ swift assert.swift
```

↓のように実行時エラーとなります。

```
42
Assertion failed: file assert.swift, line 2
0  swift                    0x000000010535b58a
PrintStackTraceSignalHandler(void*) + 42
1  swift                    0x000000010535a9c6
SignalHandler(int) + 662
...
Illegal instruction: 4
```

## `precondition` と `assert` の使い分け

これで、 `precondition` と `assert` の違いがわかりました。では、この二つをどのように使い分ければいいのでしょう？


### `precondition`

`precondition` の使い所はわかりやすいです。 `precondition` とは名前の通り **事前条件** をチェックするための関数です。

一番よく使うのは関数やメソッドの引数のチェックです。↓は `Array` にアクセスするときにインデックスが範囲外でないかをチェックする例です。

```swift
struct Array<Element> {
  ...
  subscript(i: Int) -> Element {
    // インデックスが範囲外なら実行時エラー
    precondition(0 <= i && i < count)
    ...
  }
  ...
}
```

引数だけでなく、メソッドを呼び出すときのインスタンスの状態も事前条件です。 `removeLast` メソッドを空の `Array` に対して呼び出すと実行時エラーになるようにするには↓のようにします。

```swift
struct Array<Element> {
  ...
  mutating func removeLast() -> Element {
    // この `Array` が空なら実行時エラー
    precondition(!isEmpty)
  }
  ...
}
```

どちらにも共通しているのは、事前条件を満たす責務はその関数やメソッドを呼び出す側が負っているということです。逆に言えば呼び出し側がその条件を破ることができるということです。たとえば、あなたがライブラリを作っているとして、事前条件が満たされるかは使い手次第なので、常にそれが満たされることを保証するのは不可能です。

事前条件は使い手次第でいつ破られるかわからないので、たとえ最適化したからといって省略したくありません。なので、 `-O` のときにもチェックが行われるようになっています。

| 関数 | `-Onone` | `-O` | `-Ounchecked` |
|:---|:--:|:--:|:--:|
| precondition | ○ | ○<br />↑ |  |
| assert | ○ |  |  |

（※ ○が付いていない組み合わせはチェックが省略される。）

### `-Ounchecked`

ちなみに、ちょっと脱線しますが `-Ounchecked` では `precondition` のチェックも省略されます。これは C 言語並のパフォーマンスが必要なときに使います。

| 関数 | `-Onone` | `-O` | `-Ounchecked` |
|:---|:--:|:--:|:--:|
| precondition | ○ | ○ | <br />↑ |
| assert | ○ |  |  |

（※ ○が付いていない組み合わせはチェックが省略される。）

さっきのコードを `-Ounchecked` で実行してみると

```
$ swift -Ounchecked precondition.swift
```

`precondition` でもチェックが働かず `-1` が表示されてしまいます。

```
42
-1
```

`-Ounchecked` は危険なので、画像処理や機械学習なんかの本当にパフォーマンスが重要な場合や、 `precondition` のオーバーヘッドがボトルネックになっている場合以外は使わない方がいいと思います。

### `assert`

本題に戻って `assert` ですが、 `-O` のときにもチェックが省略されます。これは何の役に立つのでしょう？

| 関数 | `-Onone` | `-O` | `-Ounchecked` |
|:---|:--:|:--:|:--:|
| precondition | ○ | ○ |  |
| assert | ○ | <br />↑ |  |

（※ ○が付いていない組み合わせはチェックが省略される。）

結論から言うと、 `assert` は **内部的な条件** のチェックに使います。

たとえば、↓のコードでは引数として与えられた `xs` の各要素の 2 乗の和を計算しています。

```swift
func foo(xs: [Int]) -> Foo {
  // `xs` の各要素の 2 乗和を計算
  let squareSum = xs.reduce(0) { $0 + $1 * $1 }
  ...
}
```

この式はちょっと複雑なので、ぱっと書いただけでは正しいか自信が持てないかもしれません。単体テストを使えば関数やメソッド単位で実装が正しいことを検証することができますが、テストに失敗しても関数の中のどの行がおかしいかまではわかりません。

その手助けをしてくれるのが `assert` です。もしこの式が正しければ各要素を 2 乗しているので `squareSum` は必ず 0 以上の値になります。

↓のように `squareSum` が満たすべき条件を `assert` で記述しておけば、式が間違って条件が満たされなかった場合に早期に検出することができます。

```swift
func foo(xs: [Int]) -> Foo {
  // `xs` の各要素の 2 乗和を計算
  let squareSum = xs.reduce(0) { $0 + $1 * $1 }
  assert(squareSum >= 0)
  ...
}
```

このように、ちょっと複雑なロジックを書いたときには `assert` を入れておくようにすればデバッグが捗ります。また、 `assert` は実行できるコードなので、古くならない生きたドキュメントとなって可読性も高めてくれます。

↑の例のように、 `assert` は外部から渡される値のよらず必ず満たされるべき内部的な条件のチェックに利用されます。たとえば、あなたの書いたライブラリをリリースするとして、そのライブラリが十分にテストされていれば、どんな使い方をされても `assert` でエラーになることはないはずです。そのため、リリース時に `-O` で `assert` のチェックを取り除いてしまっても問題ないわけです。リリース時に取り除かれるということは、チェックのオーバーヘッドを気にすることなく `assert` を書きまくれることができるということです。

その他の `assert` の使い所として、 `internal` な関数やメソッドの事前条件のチェックがあります。 `public` な関数の事前条件はいつ破られるかわからないので `precondition` でチェックするのが望ましいですが、

```swift
public func foo(x: Int) -> Foo {
  precondition(x >= 0)
  ...
}
```

`internal` な関数はモジュールの内部からしか呼ばれないので、事前条件が必ず満たされることが期待でき、`assert` でチェックするのに適しています。

```swift
internal func foo(x: Int) -> Foo {
  assert(x >= 0)
  ...
}
```

## まとめ

- `precondition`
  - `-O` でもチェックされる
  - 外部から与えられる値のチェックに使う
- `assert`
  - `-O` ではチェックが省略される
  - 内部的に満たされるべき条件のチェックに使う

## 参考

`Array` だとインデックスが範囲外のときに実行時エラーになりますが、

```swift
let array: [Int] = ...
let value: Int = array[-1] // 実行時エラー
```

`Dictionary` のキーがヒットしなかった場合には実行時エラーではなく `nil` が返されます。

```swift
let dictionary: [String: Int] = ...

// `"key"` にヒットする値がなければ実行時エラーではなく `nil`
let value: Int? = dictionary["key"]
```

実行時エラーの代わりに `nil` を返したり、 `Error` を `throw` したりということも考えられます。 `precondition` か `assert` かというピンポイントな話ではなく、エラー処理全般を広くみたときにどのように API を設計すればいいかという話については、↓の投稿にまとめてあるのでそちらを御覧下さい。

- ["Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい"](https://qiita.com/koher/items/a7a12e7e18d2bb7d8c77)