しおメモ

雑多な技術系ブログです。ニッチな内容が多いです。

Swiftのassert系メソッドとfatalErrorの使い方

Swiftには、assertと似た役割を持つメソッドとして、preconditionfatalErrorがあります。
これらのメソッドの使い方について考えてみます。

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

preconditionassertとは異なり、コンパイルオプションで-Ouncheckedを指定しない限り、条件式を評価してfalseならばプログラムを停止します。
すなわち、リリースビルドでも、条件を満たさない場合はプログラムは停止します。

precondition(hoge != nil, "Unexpected nil")
// リリース時でも通れば落ちる
preconditionFailure("Illegal state")

preconditionFailureは、-Ouncheckedでない限り、通れば落ちるということになります。

fatalError

fatalErrorはコンパイルにどの最適化オプションが指定されていても、通ればプログラムが停止します。

// 通れば必ず落ちる
fatalError("Illegal state")

fatalErrorは上の二つと異なり、そのあとの行が実行されることがないので、guard節でreturnthrowなどを書かなくて良いという利点があります。

中身は同じ

実装を見てみると中身は同じで、_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を使うことがなければ、preconditionfatalErrorはほぼ同じと言えます。

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")
}

とするだけで、リリースには影響なくエラーに気付きやすくなります。

具体的にはdequeueReusableCellUIViewControllerのダウンキャストなどは、個人的にはfatalError(あるいはforced unwrap, force cast)で落としてしまって良い気がします。