https://youtu.be/fI4XDT-RT5A
前回に precondition
と assertion
を眺める機会になりましたけれど、眺めていった最後の最後で「もしかして、今まで話したことは間違っているのでは?」みたいな展開になりました。そのまま終わりの時刻を迎えた都合で、この間は「調べて次回にお知らせする」とお話ししましたけれど、せっかくならその調べる経過も含めてみんなで眺めて行けたら楽しそう。そう思って今回は少し趣向を変えて precondition
と assertion
の様子を眺めていく回にしてみようと思います。よろしくお願いしますね。
—————————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #139
00:00 開始 00:16 今回の展望 02:11 Swift の assert 系メソッドと fatalError の使い方 02:55 assert 系の関数 03:33 assert と assertionFailure 04:04 assert は Release ビルドで無視される 04:19 precondition と preconditionFailure 04:50 fatalError 05:02 今回の話題 05:48 中身は同じらしい 06:58 assert, precondition, fatalError のどれを使うか 07:32 Assertion を使う場面 08:00 リリースビルドで内部情報が漏れないように使う 08:40 リリース先でクラッシュするのを回避する 09:20 precondition でエラーチェックのオーバーヘッドを削減 09:50 本番環境のメモリー空間を意識するようなとき 10:54 assert の使いどころと問題点 12:25 エラーハンドリング 13:07 エラーハンドリングの実装コスト 14:01 fatalError の特色 14:42 エラーの意味合いも踏まえて判断する 15:57 落とすべきところは落とすという判断 18:32 前回からの話の続き 18:48 fatalError と precondition の戻り値の型 19:54 Void と Never の特色 23:05 precondition の継続性 27:21 preconditionFailure を用いたときの挙動 28:55 precondition は、取り除かれる? 35:58 preconditionFailure は必ず実行される様子 38:20 -Ounchecked ではなく -Ouncheck の可能性? 39:52 クロージング ——————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #139
じゃあ、今日も引き続きという感じではあるんですが、ちょっと思考を変えてみます。前回の勉強会では、フェータルエラーやプレコンディション、アサーションについて話していたら、オプショナルの話に流れてしまいました。自分が把握している限りの知識を話していたんですが、最後の最後にAPIのデザインを見たら、あれっ、何か勘違いしてたかな、と不安になったまま勉強会が終わったんです。そのときに、事前に調べて結果を報告するという話をしましたが、どうせなら一緒にみんなで調べたほうが意義があるかなと思いました。今日はそのアサーションやフェータルエラーについて、どの辺で違和感を感じたかを含めて話していけばいいかなと思います。
いきなり手あたり次第に突っ込んでいくのもいいかと思ったんですが、少し時間が空いたのでウェブで少し調べたら、よくまとまっているサイトがありました。結論ありきになっちゃうかもしれませんが、まずは別の方が調べてまとめてくれた情報を見て、その後で試行錯誤してみるという流れにしようと思います。
この方が書かれているブログをまず見てみましょう。パッと見で見やすく、読んでみると理解しやすい文章になっています。個人的にポイントが高いのは、この1行のサブタイトルです。他の技術系ブログでも、短い中にしっかり説明が詰まっている内容が多いので、良いことが書かれているんじゃないかと思います。これをざっくりと見ていく感じにしましょうか。
アサート系のメソッドと言っていますが、正確には関数ですね。アサート系の関数といっても、プレコンディションを含めています。これは見方次第で、三つを一まとまりと考えたり、プレコンディションを含めてアサート系と捉えることもできますが、どんなふうに使うかを見ていきましょう。
まずはアサートとアサーションフェイラーについてです。アサートはコンパイラの最適化が無効なとき、つまりオプティマイズされていない状態のときに条件式を評価して、フォルスならデバッグ情報を残してプログラムを強制終了させます。だいたいそんな感じですね。
プレコンディションとプレコンディションフェイラーは、オプティマイズを無視するフラグが立っていない限り、条件判定をしてプログラムを終了します。プレコンディションフェイラーはさらに厳格で、コンパイルされた場合には必ず条件を評価して強制終了させます。
一方、フェータルエラーはどんな場面でもプログラムを強制終了させます。フェータルエラーはガードやスローを使う必要がない利点があります。
前回引っかかったのはここですね。もう一回ちゃんと見てみないと分からない部分もありますが、プレコンディションやアサーション関数がフェータルエラーと同じような使い方ができる気がして違和感を感じたんです。今日はここを重点的に詳しく見ていこうと思います。
あともう一つ面白いことが書かれてありました。実装を見てみると、アサーションフェイラーを呼んでいる部分があって、こういった一文を見るとすごく安心感がありますね。このブログを書いている方が細かいところまで意識しているのが伝わってきます。
今日はこんな感じで調べていこうと思います。 それが正しいかどうかというよりも、意識が向いているかどうかが重要な分かれ目だと思います。こういった一文は好きですね。パッと見たところで、確かに読んでいるとしか分からないのですが、色々書いてあって、結構まとめられています。
「どれをいつ使うか」という点で、オプティマイズに着目します。デバッグスキームではデフォルトでオプティマイズしない設定ですが、リリーススキームではオプティマイズします。また、カスタムスキームがない場合でも、-Ounchecked
というフラグを付ければこの状態になります。
デバッグのときだけ表示すれば良い状況では、アサートの使用が有効です。どの場面でアサートとオプティマイズを使うかというと、個人的には安全性を重視するならフェイタルエラーのほうが適しているように思いますが、デバッグ時には詳細情報を得たい場合、アサートは便利です。
逆にプリント関数の使用は、リリースビルドでも含まれてしまうことがあり、重要な内部情報が含まれる場合には注意が必要です。例えば、Web APIのリクエストキーなどをデバッグ情報として表示するコードをリリースビルドに残してしまうと、セキュリティリスクになります。しかし、アサートはデバッグビルドに限定されるため、このような用途には適しています。
コメントの中で、「本来ここに来ちゃいけないけど、お客様の環境でクラッシュして欲しくない」という状況がありました。本番環境でクラッシュさせるのではなく、エラーメッセージを出して継続することで、ユーザの印象が大きく変わります。その際のバランスを取るためにアサートを利用することも一つの手段です。
プレコンディションも前提条件としてリリースビルドに含まれるため、-Ounchecked
を使用して、エラーを出さないことでパフォーマンスを重視する環境を考慮する場合があります。この場合、エラーハンドリングのオーバーヘッドを減らすことが目的です。
また、本番環境ではメモリ空間が非常に小さい場合や読み込み性能が重要な場合なども考慮する必要があります。デバッグ時には機能を落として問題を深刻に捉え、シミュレーターで適用しやすいようにするためにプレコンディションを使用し、本番リリースでは-Ounchecked
を用いてコンパクトにする場合もあるでしょう。
このような表が頭の中でイメージできると、使いどころの判断材料になりますね。アサートの使いどころとしては、リリースビルドでは無視されるので、ノーコストで済む点が大きな利点です。異常が発生しそうな箇所には積極的にアサートを使うべきでしょう。
ただし、リリースビルドで起きたエラーは検知できないという問題もあります。最近ではコアダンプやポールスタックなどで情報が豊富に得られるケースも多いですが、それでもフェイタルエラーやアサートを有効に使っても、ユーザーサポートの観点からは難しい面もあります。 エラー検知に関しては、リリースビルドでのエラー検知としてフェイタルエラーやプレコンディションが使われることが議論されています。例外のキャッチについては実装側に委ねられている部分もありますが、それを継続してエラーログを収集することは可能です。
Swiftにおけるエラー処理というのは、成功法として用意されていて、特定のエラー型を定義するなど、実装時にはある程度のコストがかかることがネックとなります。エラーを精密に設計することが求められるため、実装コストはかかるものの、重要な部分で手を抜かないことが大事です。
フェイタルエラーについては、未知の動作や回復不能なエラーを通知するのに適しています。特に回復不能なエラーを通知するために非常に役立ちますが、この種のエラーが頻発するとユーザーにネガティブな影響があることもあります。
エラーハンドリングは、開発者およびユーザー双方にとって重要ですが、全てのケースに対して例外のないエラーハンドリングを用意するのにはコストがかかります。特にXcodeが頻繁にクラッシュしていた頃を例に出すと、ユーザー体験が大きく損なわれていたことを思い出しますね。
前回の勉強会でも話題になった四つのエラータイプについても、Appleの方がまとめていました。これを考慮しつつエラーハンドリングの方法を検討することが重要です。単純にコストがかかるからと安易に片付けるのではなく、実際に検知したエラーがどのタイプに該当するのかを把握する必要があります。
エラーハンドリングの失敗例として、握りつぶしてしまうコードが挙げられます。これは技術的負債の大きな要因となるため、エラーは適切なタイミングで処理し、無視してはいけない部分は避けるべきですね。責任の所在が曖昧になり、外部に向けられると無関心になりがちですが、それを防ぐためにも注意が必要です。
フェイタルエラーの定義を見ると、戻り値がNever
になっているのがわかります。これが非常に重要なポイントで、Swiftでは戻り値をNever
に設定した関数は決して戻り値を返さないという仕様になっています。この仕様を理解しておくことがエラーハンドリングを適切に行う上で重要です。
具体的に、フェイタルエラーの実行結果を変数に受け取ると、実際には何も戻り値が返ってこないことがわかります。これは、Void
からのタプルが返されるようになっており、SwiftではVoid
がタプルのエイリアスとして定義されています。 関数は必ず呼び出し元に制御を返すという原則がありますが、戻り値が Never
型の場合は制御を呼び出し元に返さない仕様です。通常、Void
型の戻り値の関数は適切に制御を返します。しかし、Never
型の場合、制御が返らないため特別な処理が必要です。
例えば、Void
型の戻り値がある関数では実行が終了した後、呼び出し元に戻ります。しかし、Never
型の戻り値を持つ関数ではそのまま制御が返されません。具体的に、fatalError
関数を使用すると、Never
型の戻り値を返すことになります。この関数は呼び出された時点でプログラムが終了するため、制御が返らないことになります。
次に、Null
型の例を見てみましょう。以下のようなコードがあるとします。
func exampleFunction() -> Never {
fatalError("This is an error")
}
このコードはコンパイルが通りますが、実行時には必ずエラーで終了します。したがって、この関数が呼び出されると、その後のコードは一切実行されないことが保証されます。
また、Never
型のサイズはメモリーレイアウト上ではゼロです。つまり、Never
型のインスタンスを作成することは実際にはできません。以下のコードでこれを確認できます。
let neverValue: Never = unsafeBitCast(0, to: Never.self)
このコードは実行時にクラッシュするため、実際には制御が戻らないことが確認できます。このように、Never
型はプログラムが実行され続けることがない状況を示すために使用されます。
Never
型は、関数が正常に終了せず、制御が呼び出し元に戻らないことを強く示すために非常に重要です。コンパイラはこの情報を用いて最適化を行い、不要なコードの生成を避けることができます。
以上がリターン型 Never
の説明です。Never
型の戻り値を持つ関数は、プログラムの制御フローに大きな影響を与えるため、正しく理解し、適切に使用することが重要です。 なので、ビルドセッティングのまずここは、やはり正規どおりにする必要がありますね。フォロー選択として、もう一度ワザにするとオーサイド、やはりオーアンチェックズって言いますよね。この選択が間違っていないか確かめて、ビルドを当てて、もう一回ランしてみます。
フェイタルエラーの場合、ちょっとターミナルを使ってみますかね。例えば、Swiftコードを打ってですね、それでこう書いてあげて、swiftc -Ounchecked Test.swift
ってやって、Test
表示が出ますよね。これがオーアンチェックズじゃないとどうなるか見てみましょう。でも、警告も出ないですね。どういうことだこれは。
オプティマイズでは、警告が出るはずですよね。今はまだ引っ越してないからですが、今回の場合はswiftc
ですね。この時に黄色の警告がXcodeに出るはずです。ターミナルにも出ますね、多分。これによって安定感も変わってきますが、フェイタルにすると違うメッセージが出るのが面白いですね、Xcodeとね。
で、オーアンチェックズをこれで実行したいんですよ。ちょっと出ますね。ネバーだから出ますよね。警告は出るけど、実行はできる。これはトラップで、警告が出るけど実行は問題なく進む感じですね。ではこうすればいいんだね、と。こうして実行してオーアンチェックズをビルドしてテストすると、落ちてしまいますね。もしかしたらプレコンディションによって発生してる問題ですね。
コメントを見てみると、オーアンチェックズの場合、ネバーコードが間違っているでしょうね。ポイントを返す形にして無視しないといけないのに、ネバーって書いてあるためにオーアンチェックズを付けてもエラーが出る。これはAPIの間違いです。
なんか、今日の勘違いが激しいので自信がないですが、データとしてはここが間違っているという結論で良いでしょうか。プレコンディションの問題がありましたね。ネバーをコメントアウトした状態で、これでオーアンチェックズを落としたときにテストすると、アラートが出ます。だから何かが間違っているのです、きっと。
アサートとアサーションフェイラーで何かコピペして間違えたという可能性がありますね。結論として、プレコンディションフェイラーのAPIの戻りが間違っている可能性が高いと思います。プレコンディションとアサーションに関しては、前回お話しした内容で問題なかったです。
時間になってしまったので、今日の勉強会はこれで終わりにしますね。お疲れ様でした。ありがとうございました。