引き続き The Basics
の Assertions and Preconditions
について眺めていきますけれど、今回はこれらを実現するために実際に使う具体的な機能についての定義を確認していこうと思ってます。定型的に使いがちな機能と思うので、この機にあらためてその初歩的なところをおさらいしてみましょう。どうぞよろしくお願いしますね。
今回は参加者の一般参加者を募って、ゆめみの外の人たちも訪れての開催になる見通しです。
——————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #195
00:00 開始 00:10 今回の展望と心持ち 01:41 理解するのと使えるのと隔たり 02:49 早いと思っても並行しての参加をオススメ 04:19 今回の assert まわりで見ていくところ 05:22 assert のおさらい 06:00 assert 関数の定義 06:29 コード補完のされ方が変わった様子? 07:07 assert 関数の定義 07:37 条件式は @autoclosure 09:01 表示メッセージと @autoclosure 09:38 自前で検査コードを書いたとすると 11:19 自前の検査と assert 関数それぞれの特徴 12:57 最適化による assert の扱い 14:51 条件判定が必ず必要な場面での assert の扱い 16:29 問題となった箇所を報告するための仕組み 18:33 行番号はなぜ UInt 型で扱う? 20:36 Int の上限を超える可能性は? 23:11 SwiftSyntax での行番号 23:56 #line 自体は Int 型? 26:48 #file もリテラル扱い 28:11 assert が C 言語由来という可能性 29:03 謎を残しつつ、クロージング ———————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #195
それでは始めていきます。アサーションとプレコンディションの話を引き続き行います。今までは概要的な部分を見てきましたが、今日はより具体的にアサーションやプレコンディションを行うためにどんな機能を使っていくのか、そんなところを見ていこうかなと思います。基本的な関数を見ていくことになるので、話は簡単です。とてもシンプルな話で終わると思いますが、だからこそいろんなところを細かく見て、いろんな情報を拾っていけたらいいなと思います。
基本の機能をゆっくりと幅広く見ていくこと。特にプロダクトを作るという観点でプログラミングに接している人が大半かと思いますが、そういった方はついついする機会がなかったり、詳細に見ていくということが少ないと思われます。また、どこに意識を向けるかというのは、意外となかなか教わることがない部分です。そういった観点でこの勉強会を活用してもらえるといいんじゃないかなと思います。
最近、勉強会で新しくプログラマーを目指している人や、これから社会人になる新卒の方が参加しました。その際、じっくりと話す機会があり、プログラムの話をして感じたことがあります。教えてもらってわかったこと、実際に使うことの考え方には限界があるということ。本人も分かっているはずなのに伝わっていない、といった状況を垣間見る機会がありました。考え方というのは教わるだけではなかなか習得しにくい部分があると感じました。そういう面でもこの勉強会が役立つのではないかなと思います。
自分がやっている勉強会で、プロダクトを作る力を養った後、もう一度改めてこの勉強会に戻ってきたらすごく勉強になるという人もいました。それは一時的なことだと思いますが、そういったのも勿体ないかもしれないとふと思いました。この勉強会は直接的な効果があるわけではないかもしれませんが、地味に積み重ねていくことで、後に役立つことが何度も経験としてあったわけです。この1年半ほど勉強会を続ける中で、自分としても非常に勉強になると感じることが多いです。ですので、わからなくてもいいから見つつ、プロダクトの方も頑張りながら適度に参加するのが良いのではないかと思います。
さて、本題に戻ります。今日はアサーションの部分を見ていきます。アサーションというのは、assert
を用いて不具合を検出するための機能です。標準ライブラリにあるassert
関数を使ってアサーションを行うことができます。この関数の定義を今日は見ていきます。
assert
関数の記述を見てみると、引数は四つ取るようになっていますが、基本的に使用するのは二つの引数だけで済む場合が多いです。三つ目、四つ目の引数はあまり使わないことが一般的です。この辺りも注目して見ていこうと思います。
一応、今日初めて参加した人もいるかと思いますので、軽くおさらいをしておきます。アサートとは、コード内に埋め込んだ条件式(第一引数)が満たされたときは次の行へ進み、満たされなかったときは次のパラメータに指定したメッセージを表示してプログラムを強制終了させる機能です。一定の条件を満たしていなかったらプログラムを中断させて、プログラマーに修正を求めるという機能です。実際に定義を見ていきますが、標準ライブラリなのでインポートなどは必要なく、assert
関数が基本形になります。
assert(condition, message)
このように、assert
関数を使って条件をチェックし、条件が満たされない場合はメッセージを表示してプログラムを停止させることができます。 今表示されているのはこれだけです。パラメータは四つあるはずなのに、二つしか表示されていません。これはどういうことなのでしょうか。とにかく、これが基本のようです。他の選択肢を探すのに何時間もかけても、一つしか見つからないことがあります。それも面白いので、もう一度試してみましょう。
さて、アサートで補完を聞かせてもらうと、一つだけか、コンディションとメッセージが二つ見つかりました(二つ目がグレーアウトしているので省略可能という意味でしょうか)。使い勝手が向上してきたという印象ですね。これで条件式を何か書き、実行すれば、条件に合わない場合にエラーが表示されます。エスケープキーを押せば次の候補が出ますが、若干使いにくいです。
とりあえず、定義を辿ってみましょう。コントロールキーとコマンドキーを押しながらやると一度で辿れなかったこともありますが、何度か繰り返すことで辿ることができます。アサートの定義を見ると、すべてのパラメータが用意されています。デフォルトパラメーターが三つあるので、自動補完されてこなかったのはそのためですね。アサートでは条件式が@autoclosure
になっているため、評価を遅延させることができます。条件式として渡された真偽値は、即時評価されるのではなく、必要に応じて評価されます。
アサート関数はデバッグビルド時にのみ有効で、オプティマイズフラグがオフのときに無視されます。呼び出し元で評価するよりも、無視される前に評価が行われるため、この仕様が大切です。アサート関数は、Swift標準ライブラリーの機能であり、コンパイルレベルで最適化が行われる前に評価が行われます。ですので、評価を遅延させるために@autoclosure
を使用しているのです。
メッセージも@autoclosure
になっていますね。これにより、デバッグ時のみ評価が行われるので、無駄な評価を避けられます。この仕組みは非常に便利ですが、うっかりすると評価が行われてしまうこともあるので、注意が必要です。
質問もありました。「アサートは何かの条件が成立したときに何かの処理を行うものだと認識していますが、その処理はif
やswitch
でもできるのではないか?」という質問です。確かに、if
やswitch
でも同じような条件分岐が可能です。
ただし、アサートにはassertionFailure
という関数もあり、これはただメッセージを表示してプログラムを落とすためのものです。たとえば、if
文で条件を満たさなかった場合にassertionFailure
を使うと、簡単にエラー処理ができます。
以下のようなイメージです。
if condition {
// 条件を満たす場合の処理
} else {
assertionFailure("エラーメッセージ")
}
このようにすることで、アサートが無効化されるかのように条件分岐が行われ、条件が満たされなかった場合にはエラーメッセージが表示されます。
assert
を使うメリットは、条件が評価される前に延期させることができる点です。条件がデバッグビルド時のみ評価されるため、効率的なエラーチェックが可能です。これに対して、assertionFailure
を使う場合には、条件が即時評価され、その結果に基づいてエラーメッセージが表示されます。どちらを使うかはシチュエーションによりますが、デバッグの際にはアサートをうまく活用するのが良いでしょう。 こうすると重要なポイントとして条件式を評価します。これだと「レッド」だと分かりにくいですね。計算型プロパティや関数にしてみましょう。アクションを実行して、成功したらリターンを返すような関数を作ります。例えば URLSession
機能をインポートしていないかもしれませんが、ここで何かを実行し、ある長い処理を行います。そして、アサートのところでアクションを呼び出すことはあまり一般的ではありません。デバッグ時にアクションが正しく呼ばれないことがあり、その場合、アクションは何もせずにスキップされてしまいます。
こういったコードをデバッグビルドやリリースビルドで処理する場合があります。リリースビルドのときに条件式が無視されると、要素の数に応じて膨大な処理を避けることができ、パフォーマンスが向上します。デバッグ時にしか必要ない機能はリリースビルドでは処理をスキップするのが便利です。ただし、guard
や if
、switch
などを使うとデバッグビルドでも動作してしまいます。#if DEBUG
などを使えば別ですが、これが常に動作することでパフォーマンスが低下する可能性があります。
特にファイル操作を行う場合、ファイルを開いて閉じるときによく問題が発生します。Cライブラリを使用するときなどは、ファイルを開いて閉じる操作を確実に行う必要があります。処理が中途半端に終わらないようにするためです。
他にも、複数の処理をしなければならない場合やリソース管理が必要な場合など、最適化されたアサートやデバッグビルドの活用は役立ちます。例えば、ファイル操作やリソース管理のロジックを含む場合には、落ちてもファイルを閉じるためのコードを含めるなどの工夫が必要です。
さらに、Swift ではコンパイル時に決まる String
型についても話しました。この String
型は特にパフォーマンスを気にする場合に役立ちます。例えば、#file
や #line
を使うと、どのファイルのどの行でアサーションが発生したかを通知することができます。このようにファイル名と行番号を使ってフィードバックを得ることができるのです。すべてまとめると、アサートの用途を考えた場合、このような機能を上手に活用することがパフォーマンスの最適化につながるということです。
もし疑問が残っていたら、Twitterで質問してくださいね。次に進むべきでしょうか。ええ、以前この勉強会でもストリックストリング型についてお話ししました。これはコンパイル時に決まる String
型でありパフォーマンスを向上させます。この基本を理解しておけば他の機能にも応用できます。 だから事実と反することを言ってもいいんですよ。__LINE__
と言ってもパスするだけで、普通は事実と反しませんけどね。事実はこのシャープファイルで得られますよっていうことです。こういった感じになっていて、それが既定値になっています。これ、オートプロパティじゃないので、その事実は呼び出し元に依存します。オートプロパティだったらどうなんでしょうね。ちょっと後でやってみましょう。
もう一つ、__LINE__
についても見ておきます。ここでのLINE
とは、行番号のことです。#line
は現在の行番号として使われます。コメントを見ればわかりますが、メッセージに関連するためにプリントする時に一緒に表示される行です。ここも完全な事実である必要はありません。つまり、事実でない番号も渡せるということです。
さて、アップファイルを見ていくのも面白そうですが、その前に非常に気になることがありました。assert
ってSwift標準ライブラリですよね。APIデザインガイドラインに従って汎用性や統合運用性を高めるためには、数値を渡すときに理由がない限りはInt
型を使うって決まっていたじゃないですか。なのに、どうしてここでUInt
を使っているんでしょうか。誰かわかる人いますか?これはInt
じゃないんですよね。コメントには特に何も書いていませんね。なぜInt
じゃないんでしょうか。すごい違和感ありませんか?
この辺り、皆のコメントを期待しているんですが、なかなか出てこないですね。そうですね、マイナスがないからシンプルにUInt
にしているっていう見方もあります。でも仮にそうであっても、Int
を使うようにしているのがSwiftのAPIデザインガイドラインですからね。この行番号は0未満があり得ないし、ただ#line
の成り立ちがわからないので、Int
にできなかったという理由があるのかもしれませんね。
オブジェクティブCの頃の#line
は確かにNSUInteger
でなければならなかったかもしれませんが、その名残ですかね。ただ、SwiftのAPIに再解釈するときにInt
型を採用してもよかった気がします。Int
型は64ビットですよね。これをUInt
にする合理的な理由があるとしたら、UInt
でないと表現できないということです。
例えば、Int
の最大値を超える量を表現できないからという可能性はあります。ただし、この量を超えるプログラムを許すかどうかという問題もあります。こんな長いプログラムを評価したくないですよね。個人的には実際にこの量を超えるプログラムをコンパイラにかけるとどうなるのか、とても興味があります。やってみる必要はないかな。例えば、別にバッファに全部取るわけじゃないですから、大丈夫かなと。ただちょっと興味が湧いて試してみたい気もします。
次に、非常に興味深いコメントを見てみましょう。オブジェクトロケーションの部分ですが、これは何のファイルでしょうか。Swift Syntaxというものかな。プレザントラインはInt
で扱っているということですね。そうするとなおさら不思議ですね。なお、Int
を普通に使っている部分もありますね。ただ、オーバーロードしていませんし、APIの見ている箇所が間違っているのかもしれません。まずはここまでにしましょう。 とりあえず、今教えてもらった部分を見てみましょうね。「エクスプレッション」のところの「タイプライン」、ここに「タイプイント」って書いてありますね。このシャープラインが現れたところの用番号ですよって書いてあるわけです。そうすると、見てるのが間違ってるのかな…いや、見てるのが間違ってるも何も、見てるのそのものなんですけどね。
uint
と書いてありますよね。シャープラインと書いてありますよね。そのドキュメントが正しかったと仮定すると、ナンバーイコールシャープライン
って書いたとき、このナンバーは何型でしょうっていう話もここでバレちゃいますけどね。イント型ですよね。アソートで、プラグ入れて、メッセージ入れて、ファイル名入れて、ここでラインナンバーを入れる。そうすると、コンパイルエラーが起こるということ。起こったら、uint
に変換しないとダメですよ。
もう1つは何だ?「ファイルライン」「リプレイスファイル withファイル」。これは予約しなさいって言ってるの…これ強引ですね。予約ないって言ってるだけか、間違えた、間違えた。バレてますね。uint
にしなさいって言ってる。ここシャープラインは狙ってる?狙ってないんだ。じゃあ、こっちでいいや。ここをuint
にすると通るの?通るんだ。二回適用されたって言ってたのは何なんでしょうね。もう一回…
プレイグラウンドでこれuint8
は通るんだ。イント型じゃないじゃん。これ、面白いですね。ここのラインのままです。そうですね、ここは今そのままにして、こっちの型推理を変えるので、これがイント型だったらエラーになるわけじゃないですか。これがuint8
でも通るんだ。面白いですね。これがリテラルっぽくなってる。
シャープファイルもそんな動きをするのかな。つまり、ストリング型は自分で使ってみたいな。ストラクト マイストリング
でエクスプレッシブル バイストリングリテラル
にして、イニット
でストリングリテラル、で…プレフィルの型にしたうえでネットSPR
と言って、マイストリング
型に対してシャープファイル
って入れられるのかな…みたいな。こう入るんだ、面白いですね。リテラルっぽい動きをするんだ。だからさっきのページに書いてあるリテラルエクスプレッションって書いてある、面白いですね。リテラルエクスプレッションは通常の文字なんだ。
まあいいや。リテラルで、タイプ、特にそのデフォルトの型は書いてないねとか、不思議な動きで、ちょっとこのドキュメントだけ見てもあんまり分かんないけど、リテラルっぽく暗黙型変換するのかな…ちょっとよく分かんないけど、動くみたいですね。
だから、ここのアサートがuint
でも定義するよって感じか。アサートがC言語版だからなのかな、というコメントいただいたが、アサート関数はC言語なのか。C言語にも確かにありますね。これでC言語からのものなのか。でもC言語からのものだとすると、インポートのダーウィンCとかこっちに入ってないと不自然というのは、エグジット関数なんかあるじゃないですか。これはINT32
を取ってるわけですよ。そうか、C由来だとINT32
とかになるのか、UINT32
とかになったら何かおかしいな。
とにかくエグジットはダーウィンCの中に入ってるわけですよね。C由来だったら多分こっちに入ってきてほしいなという希望が個人的にはあります。だからなんかね、Swift標準のライブラリに入ってるのに、アサート関数はなぜかuint
を取るっていうのがすっごい不思議だなという感じがするところで、時間になったと。
では、ここは進展なさそうな気がするけど、まあそうなんだけどね、もうちょっと。また次回ゆっくりと見ていこうかなと思います。ではこれで終わりにしましょう。お疲れ様でした。ありがとうございました。