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)で落としてしまって良い気がします。