post Image
Swiftのpreconditionとassertの使い分け

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

precondition, assert って何?

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

precondition

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

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

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

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

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

assert

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

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

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

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

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

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

preconditionassert の違い

preconditionassert は最適化を行った際の挙動が異なります。

Swift コンパイラには

  • -Onone
  • -O
  • -Ounchecked

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

関数 -Onone -O -Ounchecked
precondition
assert

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

precondition

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

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 でも同じことををやってみます。

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

preconditionassert の使い分け

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

precondition

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

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

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

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

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

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

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

関数 -Onone -O -Ounchecked
precondition
assert

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

-Ounchecked

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

関数 -Onone -O -Ounchecked
precondition
assert

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

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

$ swift -Ounchecked precondition.swift

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

42
-1

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

assert

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

関数 -Onone -O -Ounchecked
precondition
assert

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

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

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

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

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

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

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

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

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

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

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

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

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

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

まとめ

  • precondition

    • -O でもチェックされる
    • 外部から与えられる値のチェックに使う
  • assert

    • -O ではチェックが省略される
    • 内部的に満たされるべき条件のチェックに使う

参考

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

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

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

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

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

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


『 Swift 』Article List