SwiftにおけるAssertとFatalError

これはSwift Advent Calendar 2017の10日目の記事です。

assertはSwiftでコードを書く時の基本的なデバッグ機能です。 いろいろな会社のiOSプロジェクトを拝見していますが、assertを有効につかっているプロジェクトは意外と少ない印象なんですよね。 書く癖をつけておくと、いざというときに楽ができますよ。

assert、fatalErrorとは

assertとfatalErrorは、実行時に想定外のことが生じた場合にクラッシュしてアプリを停止するデバッグ用の機能です。

与えられた値(下記のコードのvalue)がtrueの時にはなにも発生せず、falseの時にはクラッシュします。 また、表示したいメッセージを付け加えることもできます。

assert(value)
assert(value, "表示したいエラー文言")

fatalError(false)
fatalError(value, "表示したいエラー文言")

どんな時に使うかを例を通してみてみましょう。

たとえば、UITableViewのdequeueReusableCellでは、あらかじめ登録したidentifierでセルを取得しても、取得できるインスタンスのクラスはUITableViewになっています。 セルを初期化するためには、目的のセルのクラスになるように、castをする必要があります。

セルの種類が多い時には、このidentifierとセルのクラスの組み合わせに間違いがあったりして、実行時にエラーになってしまうことがありますよね。

たとえばこちらの例。 ここでは、あらかじめTankCellクラスを"tankcell"というidentifierで登録してあります。 ところが、dequeueReusableCellでそのidentifierを使ってセルを取得した時に、TankCellでキャストすべきところを、間違えてIdolCellでキャストしています。 実際にプロダクトのコードがこうなっていた場合、セルが正しく取得できないため、正しいセルを返すことができません。 (また、セルのキャストを!でforce castしている場合にはクラッシュします)

ここで、下記の例のように assert文を入力しておくと、もしセルが正しくキャストされていなかった場合には、assertが有効となり、ここでクラッシュします。

return valueの型チェック

さきほどのコードをよく見ると、assertのあとにreturn文がありません。 UITableViewCell を返す関数なので、普通はコンパイルエラーになりますが、値を返さなくてもコンパイルエラーが発生することはありません。

このように、assertやfatalErrorを書くと、return valueの型チェックをスルーすることができます。

@_monoさんにご指摘いただきましたが、さきほどのコードはリリースビルドをするとコンパイルエラーとなります。これは、この記事の次の部分で書いた通り、assertはデバッグ時のみに有効でリリース時には無効になってしまうためです。ですので、この書き方はデバッグ時のみにコンパイルがとおる書き方であることをご理解ください。fatalErrorだと、デバッグビルドでもリリースビルドでもコンパイルがとおることになります。)

fatalErrorとassertの使い分け

assertはデバッグ時のみにクラッシュするのに対して、fatalErrorは、デバッグ時・リリース時のどちらでもクラッシュします。

そのまま実行すると、ユーザーの保存データに悪影響がある場合などにfatalErrorを使うべきと言われますが、個人的には、iOS開発では fatalErrorを使う必要はないと思っています。

むしろ、使うことによる弊害のほうが大きいので、なにか理由がない限り、fatalErrorは使わないようにするのが望ましいでしょう。

assertにはメッセージをいれておきましょう。

assertにメッセージをいれておくと、assertが発生した時に、そのメッセージがXcode上でハイライトされます。

メッセージをいれておくと、assert発生時に一目でどこでクラッシュしたかがわかるので、ここはなるべくいれておきましょう。 なにが問題だったかわかるようなメッセージにしておくのが定番です。

assert(false, "tweetsCountがnilになっています。")

でも、所詮デバッグ用のメッセージですし、別にコードをかいている時に思ったことをだらだらと書いてしまってもよいと思います。 コードをかきながら見ているビデオでもいいし、その時のニュースでもいいです。 重要なのは、発生した時にどこのassertなのかすぐ検索できるようなコメントをつけることです。可能な範囲でassertを書くことの心理的障壁をさげましょう!

assert(false, "なんてこった!T-28重戦車がいるぞ!")

毎回適当なことをいれていると、assertをいれるのがあまり苦にならなくなるのでおすすめです。

assertはたくさんいれておこう

たとえば、冒頭のUITableViewのdequeueReusableCellの場合。 はじめはセルの種類が2種類なので、別にassertを書かなくても大丈夫だろうと思っていたら、だんだん増えていつのまにか10種類をこえてしまって、ちゃんとidentifierとクラスの整合性がとれているかだんだんわからなくなってしまった……というのはよくあることです。

たとえセルの種類が一種類でもassertをかいておけば、セルの種類が増えた時にassertを追加するのが楽です。

また、APIで取得した中身に何が入っているかが、いつのまにかコミュニケーションミスなどで変わっていたりすることがままあると思います。 そんなときに、あらかじめassertをいれておけば、取得したデータの中身を一つ一つ確認しなくても変化に気づくことができます。

assert(val > 10, "12/8に聞いたらこの値は10より大きいとサーバー担当者に言われた")

assertはデバッグビルドでは有効ですが、リリースビルドでは無効です。 たくさんいれてもユーザーの環境で操作が遅くなるようなことはありません。 当たり前だと思われることでも念のためにできるだけたくさんいれておきましょう。

assert(sunCount == 1,"太陽は一つだといわれている")

assertは「未実装」のフラグに使うべきではない

assertはreturn valueの型チェックをスルーすることができるので、未実装の関数にassertを書いている方がいます。

とりあえずインターフェースだけ決めて実装したいときなどには便利なのかもしれません。

func madaMijissouFunc() -> MySomething{
  assert(false, "ここはまだ未実装です。")
}

このように、assertを未実装の関数にいれると実装が楽ですが、それは本来のassertの使い方ではありません。 また、これではこの関数をよぶたびにassertでクラッシュしてしまい、実装自体がすすみません。

もしインターフェースだけを書いておきたい場合には、assertを使わずに、仮の値を返すか、別途モック関数を作りましょう。

未実装であることを示すためには、TODO:コメントをつけておきましょう。

func madaMijissouFunc() -> MySomething{
  // TODO: 未実装です。
  return mockMadaMijissouFunc
}

fun mockMadaMijissouFunc() -> MySomething{
  let mySomething = MySomething("仮値")
  return mySomething
}

まとめ

assertを書く癖をつけておくと、後で楽ができます。 まだあまり使っていないかたは是非。

  • fatalErrorは避け、assertを使おう(必要な理由があるならOK)
  • assertを未実装のフラグに使わない
  • assertはできる限りたくさんいれましょう
  • 現時点で当たり前のことでもassertしてみよう