前回から入っていった The Basics
の Assertions and Preconditions
の項を引き続き眺めていきます。ここでは assert
や precondition
の基本的な特徴を確認しながら、改めてその存在感に意識を向けてみようと思います。どうぞよろしくお願いしますね。
今回は参加者の一般参加者を募って、ゆめみの外の人たちも訪れての開催になる見通しです。
————————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #191
00:00 開始 01:28 実行時に行われる検査 02:22 表明と前提条件の特徴 03:05 実際の現場で使ってる? 03:50 リリースビルドで落ちなくなったときを考慮して使っている・ 05:04 どんな場面で表明を使うと判断する? 06:24 仕様変更を検出するために活用する 06:41 けっこう活用されている印象 07:19 問題の発見を早める効果 07:59 前提条件は最適化で除かれないので勝手が違う 10:07 予期しない使われ方を防ぐために前提条件を使う 11:11 fatalError の代わりに precondition を検討してみても良いかも? 11:49 preconditionFailure は最適化しても中断される 13:24 試行錯誤しながら使い分けてみるのもオススメ 13:57 表明や前提条件はドキュメントにもなる 16:06 エラーからの復帰は想定されていない 18:01 プログラマーに直してもらうために落とす 19:24 復帰する手段は提供されない 20:27 プログラマーがエラーを直す必要性に迫られる 22:21 エラーをより予測可能なものにする 23:18 NULL 安全もエラーを予測可能にする 25:08 無効な状態が続いたことで問題が発生する可能性は? 26:55 プログラムを予測可能に終了する 27:20 本番環境でアプリを落とす必要がある場面は? 30:10 ライブラリーで予期しない呼び出しを阻止するのに使う 30:52 実際のアプリで落とそうと思う場面は? 34:40 コンソールアプリだと効果的に落とせたりする 35:34 OS の都合でアプリが強制終了させられることも 37:24 今回の所感とクロージング —————————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #191
では始めていきましょう。前回からアサーションとプレコンディションについて話してきましたが、無理やり日本語に訳して「表明」と「前提条件」としてみました。ただ、表明という日本語はあまり使う必要がないかなと改めて思っています。アサーション自体の存在感がまだ自分には捉え切れていないところもあるので、その辺りも特徴を見ながら考えていけたらいいかなと思います。
前提条件については日本語としてもそれなりに適切な表現になっているかなと思うので、「プレコンディション」でも「前提条件」でもいい感じです。この勉強会ではある程度「表明」と言ったり「アサーション」と言ったりするかもしれませんが、適当に聞いてもらえればと思います。
前回このスライドを見て、特徴を確認していきましたので、今日はその特徴をより丁寧に見ていくという感じで話を進めていきます。改めてこのスライドのおさらいをしておくと、実行時に行われる検査だというところが大きな特徴ですね。再度強調しますが、コンパイル時と実行時という二つの別ができるようになると、プログラミングの感覚が結構変わってくると思います。実行時に行われるということだけでも片隅に覚えておいてもらえれば理解が深まると思います。
アサーションの方は、開発時のミスや間違った過程を見つけるのに役立ちます。前提条件についても、開発時の問題を検出するのに役立ちます。この辺りを気にしつつ、詳細を見ていこうと思います。
ところで、この勉強会でも何回か聞いていますが、アサーションとプレコンディションってどれくらい使っているものでしょうか。思い返すと自分はあまり使っていないですね。今は結構アサートを使っています。デバッグビルドではクラッシュするので気づきやすいですが、リリースビルドでは落ちないから安心ですね。その反面、リリースビルドとデバッグビルドで挙動が変わるというところは若干怖いですね。クラッシュしないで進んだ場合、問題が後で見つかるかもしれませんが、それは仕方ないですね。
アサーションフェイラーを使うときは、テストの幅に落ちることで問題が見つからないといけないと思うので、リターンや早期リターンの場面で使います。ユーザー入力による条件など、制御が容易でない場合は使わないほうがリスクが少ないです。
例えば、ダイアログに最低一つボタンが入っていないと閉じられない場合など、ボタンを配列で受け取ったらアサーションを使います。ユーザー入力など不確定な要素に対するアサーションは危険ですので、注意が必要です。
以上が今日の内容です。これからの議論にも続けていきましょう。 こんな感じで、とりあえずアサーションとプレコンディションの特徴について説明するスライドに進んでみましょう。この特徴はどちらの特徴なのかというと、両方の特徴ですね。では、読んでみます。
実行時の期待と確認をするだけでなく、つまりアサーションやプレコンディションをコード内に埋め込むことで、条件式が真になることを期待するだけではなく、コード内にドキュメントとして埋め込む効果もあります。これは前回お話しした内容です。プレコンディションやアサーションを使うことで、プログラマーがどの条件を期待しているのかをコードに明確に埋め込めます。GitHubなどでレビューを行う際にも、意図がコードとして明確に反映されるのです。
コメントとの違いは、実行時に無視されずに意図が反映される点です。特にアサーションはデバッグ時のみ落ちるため、プレコンディションを使うと基本的に実行時に落ちてくれます。このため、コメントよりもプロダクトに影響を及ぼす意義が大きく、コードに明確に現れることが特徴とされています。
これは大きなメリットですね。自分はあまり使っていなかったのですが、積極的に使っていくと良いでしょう。特にアサーションはリリースビルドでは取り除かれるため、アプリの性能に影響を与えることもありません。どんどん使っていくのが賢明ですね。
次に、エラーハンドリングとの違いについて説明します。エラーハンドリングとは異なり、アサーションとプレコンディションは復帰可能なエラーや予期されるエラーには使えません。この「予期されるエラー」というのは復帰可能なエラーのことだと思います。起こり得るエラーという意味でしょうか。とにかく、エラーが発生したときにどうするか考える必要がないという点が異なります。
つまり、エラーハンドリングを使う場面ではエラーが起こったときにリトライをするなどの復帰処理が想定されています。しかし、アサーションやプレコンディションはそうしたシナリオを扱いません。エラーが発生したらプログラムが落ちることになります。
実際に運用する中で、ユーザーの入力によってエラーが発生するケースでは、そのエラーをユーザーに修正してもらう必要があります。例えば、ATMのエラーで「初めからやり直してください」と表示されるものはリカバリーの一種です。ATMのエラーは非常に洗練されているように感じますが、それもまたリカバリーの一部です。
アサーションやプレコンディションが発生するエラーは、プログラムの論理的なミスに帰するものが多いです。そのため、それが引き金となってプログラムが停止することは避けられません。しかし、それで終わりではありません。プログラマーは、エラーを修正して再度プログラムが正常に動作するように調整を行います。このように、プログラマーのためにエラーを落として表示することでプログラムが改善されることを目指しています。
エラーハンドリングを多用するとプログラムが複雑になりがちです。そのため、アサーションやプレコンディションを適切に使ってプログラムのロジックを明確に保つことも一つの方法です。おおむねエラーハンドリングとは異なる用途で使われるべきものと理解しておくといいでしょう。 あとは、失敗したアサーションやプレコンディションはプログラムの無効な状態を示すものです。失敗したアサーションやプレコンディションも同様に補足する手段がないと、エラーが起こったときにハンドリングする機能が提供されていないため、確実にプログラムが落ちるという約束が成されていると考えられます。他のオブジェクティブCのような言語では、致命的なエラーをハンドリングする機能が用意されていたような気がしますが、Swiftではそれが用意されていません。要するに完全にリカバリができないということです。この仕様のおかげで複雑さが減り、分かりやすいと感じます。プログラムが落ちる際にメッセージを添えられるというのが大きな違いです。
アサーションやプレコンディションを使っても無効な状態が発生しにくいコードの代わりにはなりません。これは非常に重要な説明です。アサーションやプレコンディションをいくら入れても、それだけでは安定したコードにはなりません。この上でちゃんとデバッグをしていかないと、安定して予期しない状況にならないコードにはなりません。しかし、これを入れておくことで、デバッグビルド時に確実にプログラムが落ちるため、改善が必要であることを無理やり作り出すことができます。この点も大きいです。
Swiftのコンパイラーもケースを網羅していないときにコンパイルエラーを出し、プログラマーがコードを修正せざるを得ない状況を作ります。例えば、オプショナルな変数に対してnil
が入っているか分からないにもかかわらず、nil
チェックをしていないときにはコンパイルエラーになるというものです。これにより、プログラマーが問題を修正せざるを得ない状況を作り出します。これは特にObjective-CからSwiftに移行した人にとっては、最初は面倒に感じるかもしれませんが、その良さを実感できると思います。
スイッチブロックでケースを網羅していないと、コンパイルエラーになります。これにより、プログラマーはコードを直さざるを得ない状況を作り出します。同じように、オプショナルな変数をnil
チェックせずに使ったりするとエラーになります。これは特に良い感じで、すぐに問題を見つけることができます。
昔はオブジェクト指向プログラミングでは、nil
も許容されるし、そうでないインスタンスも許容されるという風潮がありました。しかし、Swiftではオプショナルという概念が導入され、nil
が入るかどうか明確に区別されています。これにより、未定義や不明な状態が発生しにくくなりました。このようなアサーションやプレコンディションを積極的に使うことで、エラーを早く発見し、修正する助けになります。
ただし、アサーションやプレコンディションを過剰に入れると、コードの可読性が下がることもありますので、必要以上に入れることは避けた方が良いでしょう。適切な範囲でこれらを活用し、使いこなすように意識していきたいと思います。 次に無効な状態になったのを検出したら、速やかにそれを停止することで無効な状態に起因する被害を抑えられます。確かにこれは特徴として挙げられていてメリットと言えばメリットなのですが、無効な状態になったときに特に起因する被害は思ったより少ないかもしれません。ないことはないですが、例えば「アンデファインド」のままデータベースに書き込んでしまうような場合は被害が発生しますよね。
実際に値があるのに関数呼び出しを間違えたり、未定義変数名を間違えたりすることはコンパイル時には問題になりませんが、実行時には問題になります。無効な状態に起因する被害は少ないかもしれませんが、確かにあり得ます。ただし、リリースビルドになると無効な状態に起因する被害は普通に発生すると思うので、アサーションを利用してデバッグ環境では問題を見つけ、本番環境ではそれが起こらないようにするのが理想です。
無効な状態になったのを検出したら停止することは確かに大事な効能です。要は無効な状態に起因する被害を防ぐための機能です。速やかに停止し、予測可能に終了させることで、被害を抑えることができます。こういった機能をうまく使えば、多くのメリットがあります。
次に質問があります。本番リリースでアプリを落とすという場面はどういった場面で必要になるのでしょうか?これについて説明します。テストのときなら分かりますが、本番でアプリを落とすとユーザーに不満をもたれ、レビューでも低評価を受けてしまいます。また、アプリの審査でもリチェック対象になります。実際、自分も何度かこの理由で申請を繰り返されたことがあります。
リリースタイムにアプリを落とすという選択肢は基本的に避けられるべきでしょう。ただ、ゲームアプリなどでは調整アップデートを促すためにアプリを意図的に落とすケースもあるかもしれません。これも理想的ではありませんが、実施されることはあります。
元の議題に戻ると、本番環境でアプリを落とす方がコード上の安全性を確保できる場合もあります。例えば、初期化後に大規模な保証審査を行う際に問題が検出されればアプリを落とすことが必要かもしれません。これをリチェックされるようにすることで、絶対に問題が発生しない環境を作るということです。
プレコンディションについても触れておきます。プレコンディションは、ライブラリや関数を作る際に、その関数が予期しない引数を受け取った場合には例外を投げて停止するというものです。このような設計にすると予期しない状況を防ぎ、コードの安全性を高めることができます。
最後に、最適化がかかっているときにも落ちる可能性があるという点について説明します。最適化がプロダクションに適用される場合でも、チェックを行って安全に落とせるように設計することが重要です。これにより、本番環境に近いコードでチェックを行い、予期しない問題を防ぐことができます。 どっちみちプレコンディションを使おうがフェイルウェアを使おうが、本番アプリが落ちてしまうのは避けられません。ですから、本番アプリではこれらを採用しないかもしれません。これって最適化の話だけなので、プロダクションで動かさないようにできるでしょうか。逆にデバッグ環境で最適化をかける設定にしてチェックを入れれば動作するので、そういう使い方はあるかもしれません。
理想的には、ユーザーにダイアログを出して、「エラーが発生しました」と伝えることが望ましいですが、それは実現が難しいです。その場合にアプリを落とすか、それとも動いているふりをするかのどちらかになります。例えば、ユーザーデータを保存するときやサーバーにデータを送信するときにエラーが発生すると、取り返しのつかないことになる可能性があります。それよりはアプリを落とした方がまだマシかもしれません。
データの整合性が取れない状況なら、クラッシュさせるのも一つの手段です。データベースならロールバックなどの手段もありますが、限定的です。そのようなケースの場合には、どうしても落とさざるを得ませんね。
基本的にはアプリケーションの段階でクラッシュさせるのは避けたいです。コマンドラインツールとは状況が異なります。アプリケーションではユーザーの状態設定や通知設定の変更で落ちる場合、プログラマーが何も考えていないのが原因です。例えば、オーディオのルート変更などで落ちることもありますが、これは故意にクラッシュさせているわけではなく、完全にバグですね。意図的にクラッシュさせる場合はリジェクト対象になりやすいです。
プライバシー設定の変更でアプリが落ちるのも面白い視点です。これはOSの仕様とも関係があります。強制的にアプリを落とすか再起動するかの選択肢もありますが、ここはもっと深く考えてみたいところです。
アサーションとプレコンディションは別の話ですが、強制的にアプリを落とすという選択肢もいろいろ考えられます。今のところ、アプリケーション自体がクラッシュしないようにするのが主ですが、プログラマーのためにある機能として考え直すと面白いかもしれません。
今日はこのくらいにしておきましょう。お疲れ様でした。