本日は、これまで見てきていた assertion
と precondition
の残りのところ。それをどのような場面に使うかについてのページを眺めてみる予定です。その中で気になるところに着目しつつ、これまで見てきたこれらのテーマはひととおり見終えることになりそうです。どうぞよろしくお願いしますね。
———————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #206
00:00 開始 01:01 今回の展望 01:50 前提条件の使いどころ 04:01 前提条件を記述してみる 06:37 assert と precondition の違いは? 08:47 理解して使い分けることは重要 09:25 前提条件の記載方法 10:33 precondition の定義 11:41 #line はリテラル 14:36 前提条件で失敗したことを明記 17:44 preconditionFailure の戻り値は Never 20:57 最適化による落ち方の違い 23:16 preconditionFailure が最適化で除かれない理由がわからない 27:31 Paul Hudson 29:08 最適化まわりのおさらい 30:28 クロージングと次回の展望 ————————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #206
では、始めていきますね。今日はアサーションとプレコンディションについて学びます。もうそろそろスライドが終わる感じです。この勉強会の性格上、今日で終わる予定でも終わらないことが多々ありますが、一応今日で終わりそうかなというところです。ただ、終わらせることはあまり気にせずに進めていきます。
今までアサーションとプレコンディションの基本的な書き方を見てきましたが、今回のスライドでは使い方、つまりどんなときに使うかについて説明するようです。アサーションを見ていて、今回はプレコンディションを見ていこうとしています。プレコンディションは、プログラムの特定の条件が満たされていないときに実行を継続しないように強制するために使います。
前提条件の強制について話しましょう。プレコンディションを使うときは、プログラムがその先を実行する前に、特定の条件が確実に満たされていることを保証する必要があります。例えば、以下のような状況でプレコンディションを使います:
- インデックスが範囲内であるかの確認
- 関数に有効な値が渡されたかの確認
アサーションとプレコンディションの違いについても触れておきます。アサーションはデバッグ時のみ有効で、リリースビルドでは無視されますが、プレコンディションはリリースビルドでも有効であり、条件が満たされていない場合にプログラムをクラッシュさせることができます。したがって、ユーザーからの入力などによって動的に変わる状態の確認にはプレコンディションを使います。
具体的な例を見てみます。例えば、範囲の問題を確認する場合、以下のように書くことができます:
func getValue(from array: [Int], at index: Int) -> Int {
precondition(array.indices.contains(index), "Index out of range")
return array[index]
}
このように、precondition
を使ってインデックスが配列の範囲内にあることを保証しています。
また、関数に有効な値が渡されたかを確認する場合、次のように書けます:
func calculate(value: Int) -> Int {
precondition(value > 0, "Value must be greater than zero")
// 計算ロジック
}
この例では、value
が0より大きいことをプレコンディションで保証しています。
以上のように、アサーションとプレコンディションを適切に使い分けることで、プログラムの予測不可能な動作を避けることができます。今日の内容は以上です。質問があればどうぞ。 例えば、このようなケースでは非常に難しいと感じてしまいますね。この例を見ていると、デバッグだけで安全に進めることはやはり重要です。プログラマーにとって最適な要因かもしれません。後はライブラリ提供者の視点でも考えてみると、ライブラリを提供する際にオープンソースの場合、デバッグビルドを行うのでアサートも有効ですが、ライブラリをバイナリ形式で提供する場合、リリースビルドだとアサートは消えてしまいます。そういったときには、外向けの使用と内側で完結する使用の違いが重要になります。
自分自身はあまりアサートやプレコンディションを使っていないのですが、世間的には普通に使われています。この前見たブログでも、普通にアサートが使われていましたし、多くの話を聞く限り、多くの開発者がアサートをよく使っています。そういった経験があると、この感覚が身についてくるのかもしれません。
プレコンディションとアサートの大きな違いは、デバッグビルドで有効なのがアサートで、リリースビルドで有効なのがプレコンディションという点です。この基本的な違いを押さえておくことが重要でしょう。ですが、機能的な部分だけを押さえていると適切な使い方が難しいので、その背後にある意味や意図を理解することが重要です。
実際、「The Swift Programming Language」にはアサートとプレコンディションについてあまり詳しく書かれていません。プレコンディションの基本的な説明だけがある程度です。
次に進みましょうか。前提条件の記載方法です。これはプレコンディションの具体的な書き方ですが、前提条件は上述の通りです。プレコンディションの後に条件式、メッセージ、そしてどのファイルでこの条件が検証されたかの情報を記載することができます。これらの三つは省略可能で、条件式だけを書くことも可能です。
プレコンディションの定義も改めて見てみます。以前にも見ましたが、この関数に定義として評価される式と、条件が false だったときに表示されるメッセージを渡します。ファイルと行番号は省略可能です。定義を見るために、プレコンディションで Command
+ J
を使って辿ってみます。Xcodeの特徴ですが、一度辿れなくてももう一度試してみると、最終的に辿り着けることが多いです。
このように、プレコンディションは非常に強力なツールですが、正しい使い方を理解することが大切です。 プレコンディションの定義はアサーションと似たようなものです。条件式がオートクロージャーになっているため、無視されたときにはほぼ評価しないというメッセージがあります。ただし、評価されていなかったときのみ、または表示する必要が出てくるときまで実行しないかという点が気になります。
ファイルとラインが定義されていますが、ここが面白かったですね。結局エラーは出ていませんが、ラインが UInt
で取るようになっているのに、確か Int
型を渡せたはずです。もう一度確認してみましょう。
プレコンディションを使ってみて、メッセージを付けることができるか試してみます。ライント番号を渡す場合、例えば #line
を使うとします。このとき、型が Int
型ですが、このライン番号が UInt
を必要とする場所に渡せるのですね。このリテラル(リテラルとは少し違う気がしますが)は Int
にも UInt
にもなれるということです。
precondition
や preconditionFailure
関数は特に難しいものではありません。次に進みますが、前提条件の失敗を明記することができ、これを示すために preconditionFailure
という関数が使えます。条件判定を終えた状況で、「前提条件を満たしていませんよ」ということを表明するためのものです。
自分がメモしていた内容を見ていくと、前提条件をデフォルトで書けるか、最適化で落ちなくなることがあるから、そのときにどうなるかを気にしているようです。後でエクスポートで確認してみましょう。
例えば、関数に対して値を Int
型で受け取り、スイッチケースで前提条件を判定するとします。もし前提条件を満たさない場合には、preconditionFailure
でエラーを出すといったことができます。以下のようなコードで試してみましょう:
func someAction(value: Int) {
switch value {
case 0..<someLimit:
// Some valid case
print("Valid case")
default:
preconditionFailure("Value out of valid range")
}
}
このようにして、前提条件をしっかりと管理することができます。 さて、Swiftにおけるプレコンディションフェイラーについて話しましょう。例えば、Int
型のランダムな整数を0から-100から100まで生成するコードを書いたとします。このとき、前提条件を満たしていない場合には、エラーを投げることが求められます。ガードやプレコンディションを使うと、その前提条件を明示的に確認できますが、コンパイル最適化の過程でこれらのチェックが無視される可能性があります。
ここでは、プレコンディションフェイラーが無視されるかどうかについて考えます。Swiftのコンパイラがどのように動作するかを確認するため、サンプルコードを書いてテストします。
まず、プレコンディションフェイラーが無視されないことを確認したいので、以下のようなコードを書いてみましょう。
preconditionFailure("This should fail")
このコードをSwiftでコンパイルし、最適化なし (-O0
) で実行すると、preconditionFailure
は必ず失敗し、プログラムはクラッシュします。
最適化を有効 (-O
) にしても、この動作が無視されないことを確認します。もし他の条件が無視されるとしたら、アサートかもしれないので次のコードで確認します。
assert(false, "This should also fail")
通常、assert
はデバッグビルドでのみ有効で、リリースビルドでは無視される場合があります。これに対し、preconditionFailure
は常に有効です。プレコンディションフェイラーが無視されることはなく、最適化されても強制的にプログラムは停止します。
興味がある方は、「Hackingswift」と呼ばれるサイトで詳しい記事を読むと、より深く理解できるでしょう。その中で、最適化とプレコンディションの関係についての詳細な解説も見つけられます。
以上、プレコンディションフェイラーについての考察でした。これが正しく動作し、最適化されても無視されないことを確認できたことと思います。これからもSwiftの言語仕様についてさらに学びを深めていけるといいですね。他にも疑問や興味がありましたら、ぜひ共有してください。 プレコンディションフェイタルエラーだけは特別にNever
を返すんだよと言うんだったら、全然同じようなものだと意識させない別のネーミングが確かに必要かもしれません。無視されると思うじゃないですか。
O1
チェックでね、最大でツイートは先へ進ませないようにする、要はリターンしない端数はNever
という戻り値があるおかげでコンパイラーが通知できます。しかし、もしNever
という発想がなかったら気づかないわけです。無視されないことがね。データルエラーがNever
を返すなら分かるんですけど、これは自然でいいんですけど、絶対に無視されません。
リンクを貼りたくない、こっちが貼ればよかったんだ。デビューか、なるほど。あれがリピートのか、使うページなのか、自分が見落としてるだけだ。そうそう、それです。こういうローレベルなのに興味ある人は、このあたりをゆっくりと読んでみたらいいかな。自分もちょっとこのセクションが終わったらゆっくり見てみようと思います。もう一回、なんかいわばスッキリしないんですよね。
ありがとうございます。ぜひぜひ見てみてください。ただし、この方はとても夢でハッピングSwiftをやられている方です。コロナが流行る前にはいろんなところで登壇されてた方です。いろんな国で言語仕様周りで面白い話をされる方なので、この方を見かけることがあったら注目すると楽しいかもしれません。
とりあえず、ここにいろいろ事情が書いてあるらしいです。アプリの方ではないはずなんで、多分何か知らないんですけど、あくまでその方の考えでこれだから正当という感じでしょうか。でも、アプリが公式見解としてどうだとか言ったからって、それが正しいとは限りません。その点ではとても中立的な意見です。この方はとても言語仕様周りに詳しい印象の方なので、これを読んでいくときっと良いことがありそうだなと思いました。
さて、スライド1枚があと2分か。読んでみます。最適化についての話です。O1
チェックをしてコンパイルしたとき、普通はアプリ開発を普通にしているときには、意図的にO1
チェックをしない限りは、このフラグがつかないので、基本的には前提条件をチェックされるものと思っておいていいと思います。しかし、こういう特例があるよ、特殊条件みたいな、無視される条件があるよ、ということは一応覚えておくと良いでしょう。
前提条件が常にあるとみなして、それに従って適切に処理を進めていけると思います。ただし、フェイタルエラーについては、もう完全にプレコンディションの話から抜けちゃってますね。フェイタルエラーは適当に言わずに実行中断できる関数という話です。
今日は時間がないのでここまでにしましょうか。フェイタルエラーの関数の話は次回にします。とりあえず今日はプレコンディションのお話を一通り見ていった感じです。プレコンディションフェイタルエラーだけはなぜNever
になっているのか、あとNever
の話もちょっとしたかったけど、それは次回にしましょう。今日はここまでにします。お疲れさまでした。ありがとうございました。