Swiftには、assert
と似た役割を持つメソッドとして、precondition
やfatalError
があります。
これらのメソッドの使い方について考えてみます。
assert/asertionFailure
assert
は、デバッグビルドなどコンパイラの最適化が無効な時(-Onone
)のみ条件式を評価して、false
だった場合に、
C言語のassert
のようにデバッグ情報を残して、プログラムの実行を停止するメソッドです。
// hogeがnilだった際に終了する assert(hoge != nil, "Unexpected nil")
第一引数の条件式は、-Onone
の場合のみ評価されるため、
リリースビルドなどでは、仮に条件式がfalse
相当でも、プログラムは停止しません。
第二引数にString
を入れておくと、Debug時に表示されるので、どのような場合に起きるかなどを記載しておくと、デバッグがしやすくなります。
assertionFailure
は条件式がfalse
相当で、-Onone
の場合通った際にプログラムが停止します。
// リリースビルドでは通っても何も起きない assertionFailure("Illegal state")
precondition/preconditionFailure
precondition
はassert
とは異なり、コンパイルオプションで-Ounchecked
を指定しない限り、条件式を評価してfalse
ならばプログラムを停止します。
すなわち、リリースビルドでも、条件を満たさない場合はプログラムは停止します。
precondition(hoge != nil, "Unexpected nil") // リリース時でも通れば落ちる preconditionFailure("Illegal state")
preconditionFailure
は、-Ounchecked
でない限り、通れば落ちるということになります。
fatalError
fatalError
はコンパイルにどの最適化オプションが指定されていても、通ればプログラムが停止します。
// 通れば必ず落ちる fatalError("Illegal state")
fatalError
は上の二つと異なり、そのあとの行が実行されることがないので、guard
節でreturn
やthrow
などを書かなくて良いという利点があります。
中身は同じ
実装を見てみると中身は同じで、_assertionFailure
を呼んでいます。
swift/Assert.swift at 3b99358592cc8e955ac90cbdf5eb2094dced8d3c · apple/swift · GitHub
_assertionFailure
の実装はAssertCommon.swift
にあります。
swift/AssertCommon.swift at 0205150b8f41db7fc220d19e46be86669e06cb02 · apple/swift · GitHub
どれをいつ使うか
落とせる場合は、
メソッド | -Onone (Debug) |
-O (Release) |
-Ounchecked |
---|---|---|---|
assert |
○ | - | - |
precondition |
○ | ○ | - |
fatalError |
○ | ○ | ○ |
のようになります。
明示的に-Ounchecked
を使うことがなければ、precondition
とfatalError
はほぼ同じと言えます。
assertの使いどころ
assert
はリリースビルドでは素通りするので、ノーコストで挟むことができます。
したがって、異常が起きそうな場所には、積極的に使っていくことができます。
ただし、これだけではリリースビルドで起きたエラーは検知できないです。
リリースビルドでのエラー検知
リリース時のエラーを検知したい場合は、例外やfatalError(precondition)
を使っていくことになります。
例外は、
catch
する側の実装に委ねられる部分もありますが、処理を継続しつつ、エラーログを集めることが可能です。
ただ、独自例外を定義したり、catch
する際の処理を書くなど、実装コストはかさみます。fatalError
は、未知の動作や、回復不能なエラーが起きる場所に適しています。
即座にクラッシュしてしまうので、頻発するとユーザーにネガティブな影響があります。
例外でカバーする方が、ユーザーと開発者双方にとって良いのですが、すべてのケースに対して例外を用意するのは、コストがかかることが多いので、本当に検知したいエラーかどうか吟味する必要があります。
IMO
ビジネス的な要件やUXも関わってくるため難しいところですが、クラッシュを避けるがあまり握りつぶしが多発するのも、技術負債が発生する大きな要因となるため、落とすべきところは落とした方が良いと思います。
例えば、想定しない失敗にもかかわらず、guard
節でreturn
しているだけの部分は、問題が起きた際に気付きづらいので、
guard let hoge = hoge else { assertionFailure("Unexpected nil") return } // あるいは、一行で済ませるなら guard let hoge = hoge else { return assertionFailure("Unexpected nil") }
とするだけで、リリースには影響なくエラーに気付きやすくなります。
具体的にはdequeueReusableCell
やUIViewController
のダウンキャストなどは、個人的にはfatalError
(あるいはforced unwrap, force cast)で落としてしまって良い気がします。