precondition
と assert
は似ているので、どういうケースでどちらを使うべきか意識してないと不適切な使い方をしてしまいます。本投稿では、 precondition
と assert
をどのように使い分けるべきかについて説明します。
precondition
, assert
って何?
precondition
と assert
を知らない方のために、初めにそれらについて簡単に説明します。知っている方は本節は読み飛ばして下さい。
precondition
precondition
は条件を指定し、その条件が満たされなかった場合に実行時エラーとしてクラッシュさせることができます。
↓のコードでは x >= 0
という条件がチェックされています。
func foo(x: Int) {
precondition(x >= 0)
print(x) // `x` が 0 以上のときだけ実行される
}
たとえば、 x
に 42
を渡すのは大丈夫ですが、 -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
のときとまったく同じですね。では、この二つは何が違うのでしょうか?
precondition
と assert
の違い
precondition
と assert
は最適化を行った際の挙動が異なります。
Swift コンパイラには
-Onone
-O
-Ounchecked
の三つの最適化レベルが用意されています。 precondition
は -O
のときもチェックが行われますが、 assert
は -O
でチェックが行われません。これが precondition
と assert
の違いです。
関数 | -Onone |
-O |
-Ounchecked |
---|---|---|---|
precondition | ○ | ○ | |
assert | ○ |
(※ ○が付いていない組み合わせはチェックが省略される。)
precondition
実際にさっきのコード(↓)を -O
で実行するとどうなるか見てみましょう。
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
でも同じことををやってみます。
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
にアクセスするときにインデックスが範囲外でないかをチェックする例です。
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
でエラーになることはないはずです。そのため、リリース時に -O
で assert
のチェックを取り除いてしまっても問題ないわけです。リリース時に取り除かれるということは、チェックのオーバーヘッドを気にすることなく 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
を返したり、 Error
を throw
したりということも考えられます。 precondition
か assert
かというピンポイントな話ではなく、エラー処理全般を広くみたときにどのように API を設計すればいいかという話については、↓の投稿にまとめてあるのでそちらを御覧下さい。