本日は再び The Swift Programming Language の本流に戻って、これまで見てきていた assertion
と precondition
続きのところを眺めていきます。今回はその具体的な記載例あたりを見ていく感じで、これまでの話のおさらい的なところになりますけれど、寄り道をしていて間が空いたのもあって思い出すのにちょうど良いかもしれないです。どうぞよろしくお願いしますね。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #205
00:00 開始 00:10 ブログで見かける表現についての雑談 01:50 assertion を用いた不具合修正 03:56 assert のメッセージは省略可能 04:28 散文的? 05:36 ランタイムでのエラーを扱う 06:09 状況をチェック済みなときの assertion 07:13 条件が少し複雑な印象 09:03 条件式を書き換えられる? 13:56 UInt で 0 未満を除外する? 15:30 マイナスは 0 歳扱いするという発想もアリかも 17:12 通常は UInt ではなく Int を用いる 17:57 ワードサイズ 19:28 API レベルの互換性を意識すると Int 型 22:31 明示的な型変換が必要になる 24:01 型に囚われない時代も訪れつつある 28:36 型に影響を受けないコードなら、型を縛らない引数が可能 30:40 独自の型に仕立てる手法 34:35 クロージング ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #205
はい、では始めます。
久しぶりにSwift Programming Languageに戻ってきました。ここまで見てきた内容は、記憶の彼方にある方も多いかもしれませんね。個人的にブログなどを読んでいるときに、何々の人は多い、私もそうだという書き方をするブロガーが多いと感じます。しかし、それは結局「私はそうだ」と言っているだけに過ぎませんよね。なので、他の意見を表明するときも、同じような感じで自己中心的な表現に過ぎないと解釈することが大切だと思います。
さて、本題に戻りますが、今日はアサーションとプレコンディションの具体的な使い方について見ていきます。昔の内容を思い出しつつ、おさらいとして進めていきましょう。
まず、スライドを見ていくと、「表明」という言葉が使われていますが、これはアサーションのことを指しています。アサーションを用いた不具合修正の例として、もし年齢にマイナス3が設定されていたときには、実行を停止させるといった内容です。具体的に言うと、次のようなコードです:
assert(age >= 0, "年齢は負の数ではいけません")
この例では age >= 0
が真と評価される場合にのみ実行が継続されます。もし負の数だった場合には、式が偽(false)と評価され、アサーションが失敗し、アプリケーションが終了します。ただし、これはデバッグ実行時(オプティマイズなし)に限り有効です。オプティマイズをかけてしまうと効力を失います。
これはデバッグ時のみ有効ですが、プログラマーはこれにしっかり対処すべきです。デバッグ時にアプリケーションを終了することで、リリース時には問題がないことを確認しているわけです。リリースビルドの時にはアサーションが無視されても全く問題ないのです。
次に、アサーションメッセージの省略についてです。前述のコードではアサーションの後ろにメッセージを設定していましたが、これは省略できます。具体的には、以下のように書くことができます:
assert(condition)
ここで「三分的に条件を繰り返したいとき」などの表現がありましたが、「三分的」という言葉が馴染みがない方も多いかと思います。調べたところ、「三分」自体は国語で習ったことがあるような表現です。詳しい意味は国語の教科書などで確かめることができますが、基本的には部分的に繰り返すことを意味しています。
スライドを進めていくと、アサーションの使いどころや具体例などが紹介されているので、それらも一緒に見ていきましょう。 とりあえず簡単な例を挙げると、「年齢は0以上」とか「1以上」とか、そういう前提条件を設定してコードを書くことが一般的です。その際、前提条件が非常に多くなる場合には、メッセージを省略した方が便利な場合もあります。
前述の内容を踏まえて、アサーション(assert
)はプログラマー向けのメッセージになります。コンパイルタイムでエラーが発生する場合は、コンパイルタイムでチェックされるのが理想的です。これは開発効率の向上やアプリケーションがクラッシュしないなど、多くの点で都合が良いためです。しかし、ランタイム中にエラーを検出する必要がある場合には、アサーションが有効です。このようにして、エラーハンドリングを行うための関数としてAssertionFailure
があります。
AssertionFailure
はコード内で既に条件をチェック済みのときに使用します。例えば、プログラム内で「年齢が10歳以上である」というチェックを実行済みで、「年齢が0歳未満である」ということはあり得ない場合に、この関数が使われます。ここではアサーションフェイラーを使って、「年齢が0歳未満であることは許されない」という表示を行うことができます。
具体例として、ローラーコースターに乗れる年齢制限について考えます。もし、乗り物に乗るための年齢が10歳以上であるとチェックされていた場合、対称的に「年齢が0歳未満である」といった条件チェックは不要になります。このような場合に、AssertionFailure
が適用されるのです。
この例では、プログラムが正常に動作しない場合には、プログラムの終了処理を行うことになります。そのため、アサーションフェイラーを使うことで、安全性を確保するのです。リリースビルド時にもしこのチェックが甘い場合、プログラムが予期しない動作をして、バグが生じることがあります。例えば、チェックが不足していることで、乗れないはずの乗り物に乗れる状態になるなどです。
コードの例を見てみると、AssertionFailure
を使ってメッセージを出す場合と条件式を分離して書く場合があります。この書き方が整理されているか、あるいはもっと読みやすいコードになるのか気になるところです。結局のところ、状況によるかもしれません。
例えば、年齢が0歳未満の場合をガード文で処理する方法について考えます。
guard age >= 0 else {
// エラーメッセージを表示するなどの処理
}
このように、ガード文を使って条件をチェックすることで、コードの可読性が向上します。assert
も同様に直感的であると言えますが、使い方によっては複雑になることもあるでしょう。
以上が、アサーションと条件チェックについての基本的な説明になり、コードの書き方についての一例です。 さて、例のコードを少し整理しました。この部分ではアサーションを使っているので、その処理方法について話していきます。具体的には「アサーションでエラーを起こして、それから10歳より上かどうかをチェックする」という内容です。
アサーションを使うことで、条件に合わない場合は実行を止めることができます。具体的には、年齢が0以上であれば、乗り物に乗るといった処理を実行します。もし年齢が0未満の場合、誤って処理を続行してしまうことを防止できます。ガード文を使うことで、この先の処理には絶対に進まなくなります。乗るというアクションに対するチェックを確実に行えるので、安全性が保たれます。特に、リリースフィールドでこれが漏れてしまった場合でも、悪影響をある程度抑えられます。
とはいえ、元のコードの書き方にも良い面はあります。元の方法では、最初にエッジが0より大きいかどうかをチェックし、失敗した場合は別の処理を行うという形になります。一方で、アサートを使うと本番環境でエラーが起こるリスクがあります。状況に応じて、どちらの方法が適しているかを選択する必要があります。
特に UInt
の使用については、APIデザインガイドラインによれば特別な理由がなければ Int
を使うことが推奨されています。UInt
を使う場合、マイナス値を扱えないため、想定外のエラーが発生するリスクが伴います。例えば、UInt
を初期化する際にマイナス値を与えるとコンパイルエラーになります。また、UInt
のイニシャライザに無理やりマイナス値を入れると、0
に丸められるため、意図しない結果になることがあります。
UInt
の使用ガイドラインにも「プラットフォームのワードサイズと同じ大きさの非符号付き整数が必要なときにのみ使う」とされています。プラットフォームによってワードサイズが異なるため、その点にも注意が必要です。
例えば 3歳以上
などと具体的な条件を設定して、その範囲内でのみ使う場合も考慮するべきです。特に観覧車の乗車年齢など、安全性を考慮した具体的な使用シーンでは、状況に応じた型の選択とアサーションの使い方が非常に重要になります。
APIデザインガイドラインに従って適切に型を選び、ロバストなコードを書くことが求められます。それでは、引き続きコーディングを進めていきましょう。 Swiftの言語仕様を説明する際に「ワードサイズ」という概念が出てきます。ワードサイズが不適切な指定数の場合、値が誤って格納される可能性があります。そのため、特定の状況ではINT
型を使用することが望ましくない場合があります。
例えば、INT
の最大値は決まっていますが、UINT
(符号なし整数)の最大値はそれよりも大きいです。つまり、UINT
を使えばマイナスを取る必要がないので、より大きな値を扱うことが可能です。しかし、ワードサイズがネイティブなタイプでない場合はUINT
などを使う必要が出てきます。APIデザインガイドラインでは互換性の問題も考慮する必要があります。APIレベルの互換性を持たせるために数値を扱う際にはINT
を多用します。
例えば、ある関数がUINT
を取る場合、その関数を呼ぶためには互換性を保つ形で変換が必要になります。ここで問題となるのは、コードの再利用性とメンテナンス性です。UINT
を使うと、ソースコードを書く手間やインスタンスの作り直しなど、実行時の手間が増えます。そのため、全体的なコーディング体験にあまり良い影響を与えません。よって、特別な理由がない限りINT
型を使うことが推奨されているわけです。
INT
を使うべき理由としては、コードの総合運用性が向上する点が挙げられます。異なる数値型の間での変換を避けることで、コードの保守や運用が楽になります。たとえば、Objective-CやC言語では暗黙型変換が許されていますが、Swiftでは基本的に明示型変換が原則です。これにより、エラーを未然に防ぎ、予期しない動作を避けることができます。
暗黙型変換があると、UINT
を取る関数に対してもエラーなく通るため、プログラマーが煩わされることが少なくなります。しかし、Swiftでは明示型変換が基本であるため、この点に注意が必要です。結果として、SwiftのデザインガイドラインではINT
型の使用が推奨されています。この方針によって、コードの信頼性や保守性が向上するとされています。 この話は本当に興味深かったですね。何が出てきたかな?タスクのsleep
関数だったかな?この勉強会では何度かString
プロトコルの話をしてきました。String
やSubstring
でもお構いなしに扱えるという内容ですね。ここで言いたいのは、Swiftという型に厳格な言語でありながら、ジェネリクスを活用することでプロトコルに準拠していればどんな型でも問題ないという考えが根付いてきている状況があるということです。
数値に関しても同じです。以前の勉強会で、ある値がInt16
型でBという値がInt32
型の時、型が違っても比較ができるという発想があるとお話しました。確かこれは例としてsleep
関数を使ったと思うのですが、Duration
とも関連する話です。ミリ秒単位で待機する場合に、たとえば100
ミリ秒待つと書くとコンパイルエラーが出るといった状況があります。しかし、これもリテラルとして100
をInt
として扱っているからです。
ここで面白いのは、スリープ関数についてパラメータとしてInt16
でもUInt32
でも渡せるということです。Swiftの標準で提供されている全ての整数型はBinaryInteger
プロトコルに準拠しています。これを利用することで、どの整数型でも受け入れることができるわけです。この点が非常にクールだと感じます。
具体的に言うと、UInt
を取るように書いてしまうとSwiftの都合でビルドエラーになりますが、ここでBinaryInteger
型を取るようにしてあげればコンパイルが通るわけです。当然、コードが破綻しない限り問題なく動作します。例えば、年齢を扱う時のage
が整数型であれば問題なく受け入れられるようにする、そういった形でコードを書いていくことができます。 世の中に存在している以上、年齢は0歳以上だろうということで、age
がバイナリーインテジャーで取られるわけです。0歳以上だと判明したら、それ以降はバグの対象となります。ここから先の話は先ほどと同じですね。条件文を3段階にするか、ガードで弾くとか、その後は元の話に戻ります。
いずれにしても、型に影響を受けないコードの場合、型に影響を受けないパラメータを積極的に取ることが問題にならないということですね。統合運用性の観点から、一般的にint
を選んでUInt
は避けるという発想が基本となります。さらに、どんな数字でも良ければSomeBinaryInteger
を取りましょうという感じです。実際、こうやって書ける場面は結構あると思います。
StringProtocol
と同じ感覚で、特にreadonly
のように確実に使う場面では、このように書くと良いですね。通常はint
を使うことが多いので、大抵の場合は問題ありませんが、タスクのスリープのように汎用的な処理ではUInt
も検討できます。
他のケースでも、例えばUInt
の代わりにMyUInt
というラップされた型を使うことはできます。MyUInt
の中で実際にはint
を持ち、0以上であるチェックを行って、失敗したらエラーを返すといった実装です。この場合、変換コストも発生しませんね。assert
を使って、プログラムが完成した時点で問題ないようにしておくのも良いでしょう。
色々な考え方やアプローチがありますが、それによってコストを重視するか、汎用性を重視するかが変わってくると思います。例えばunsafeUnwrap
やEmpty
についての議論も含まれていました。コレクションが渡されて、空チェックを行う場合、それが成功すればnonEmpty
なコレクションを得られるといったものですね。
int
が汎用性が高いからという観点もありますが、それぞれのシナリオによって解釈が変わることもあります。型補助を活用したチェックを行い、コストを抑えるか、汎用性を求めたコードを書くかのどちらかになります。
今回の議論をまとめると、age
のようにint
とUInt
で迷うこともあるかもしれませんが、条件に応じて適切な型を選ぶことが重要だということです。では、今日はこの辺で終わりにしましょう。ありがとうございました。