前回から引き続き、本日も Swift の 演算子
まわりを見ていきますね。今回も演算子の基本中な基本の特徴から、その概要を掴んでいく感じになりそうです。よろしくお願いしますね。
——————————————————————————— 熊谷さんのやさしい Swift 勉強会 #316
00:00 Start 00:26 雑談:ゆめみってすごいよね 05:02 演算子についてのおさらい 09:33 算術演算子は基本、オーバーフローを許さない 12:33 アンダーフローとは 13:33 計算誤差とその種類 15:06 オーバーフローとは 16:57 オーバーフローを許容することも有用 18:27 オーバーフロー演算子 22:16 オーバーフロー演算子による演算の様子 29:16 範囲演算子 30:08 半開範囲と番兵の親和性 32:50 end と last 35:08 クロージング ———————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #316
今日は演算子について学んでいきたいと思います。前回も基本的な演算子について話しましたが、今回は特にSwiftの基本的な演算子に注目してみます。Swiftは多くの特徴を持つ言語ですが、その中でも演算子は特に興味深いものです。
Swiftの演算子には、イコールやプラスといったおなじみのものがありますが、それ以外にもいくつかフレーズで構成されるものがあります。具体例を挙げると、add
やconsume
、copy
といったものです。これらの演算子は、関数として呼ぶのではなく、言語構文の一部として自然にテキストに馴染む形で使われます。
たとえば、「1と2を足す」という場合、関数としてはadd(1, 2)
のように書くことができますが、Swiftではインフィックス形式で「1 + 2」と記述できます。それとは別に、ポストフィックスやプレフィックスといった他の記述形式もあります。これらは、文の構造によって異なる使い方ができる演算子です。
また、Swiftでは論理的演算子として「アンド」も使用できます。これは一般的にプログラミングで非常に良く使われる演算子です。通常、論理的演算子は条件分岐などで使われ、プログラムの処理を分岐させる重要な役割を果たします。
今回の勉強会では、このような基本的な演算子について、特にSwiftでの具体的な使用方法を探ります。皆さんが普段使っている他のプログラミング言語との違いなども考えながら進めていけるといいですね。興味のある方はぜひ参加してみてください。勉強会はいつ無くなるかわからないので、あるうちに活用していただければ嬉しく思います。オープンで自由な雰囲気の中で学ぶことができるのがこの勉強会の魅力です。 予報処理技術者などの試験では必ず出てくる内容がありますが、それについてはこの勉強会の中でもいずれ詳しく見ていきます。前回の勉強会では最初のサブセクションの部分まで進みましたが、今回のポイントは次の段階に進んでいきます。前回の内容について興味がある方は、アーカイブが公開された際にそちらを確認していただければと思います。今回はそこから少し掘り下げていきたいと思います。
Swift言語は約10年前に登場した比較的新しいプログラミング言語です。そのため、多くのプログラミング言語から得られたノウハウを活用して設計されています。演算子の中でも特によくあるコーディングミスを減らすために改善が行われています。例えば、+=演算子を使用した際など、オーバーフローを検出して許さない仕組みが取り入れられています。これは表現できる範囲を超えたときに問題が発生しないようにするためです。
オーバーフローの概念について、まだ詳しくない方向けに少し説明してみます。例えば、let a: Int8 = 127
として、a + 1
を試みると、Int8
の最大値を超えてしまうためオーバーフローが発生します。Int8
の最大値と最小値は限られた範囲であり、その範囲を超えるとオーバーフローが生じます。
一方、アンダーフローという言葉もありますが、これは別の概念で、浮動小数点数の演算において計算結果が非常に小さくなり、表現できなくなる状況のことを指します。結果として計算誤差が生じ、情報が失われることを情報落ちと呼びます。例えば、大きな数値に対して非常に小さい数値を足したとき、その小さい数が無視され、失われてしまうことを表しています。
このような誤差には種類があり、プログラミングなどで正確な計算が求められる場面では注意が必要です。基本情報処理技術者試験を受けることで、こうした概念についての理解が深まると思います。日々のプログラミングにおいて役立つ考え方ですので、忘れずに学んでいきたいものです。 こういった内容については、アンダーフローというよりもオーバーフローに関する話ですね。ただし、細部については間違っている可能性があるので、興味があれば自分で詳しく調べてみてください。有効な結果が減少する場合と考えると、オーバーフローの方が適切でしょう。
オーバーフローとは、計算結果があらかじめ定められた値の範囲を超えてしまうことを指します。例えば、Int8
での計算の場合、その表現範囲は-128から127までです。この範囲を超えてしまったときに誤差が発生します。例えば、Int8
型を使った計算でオーバーフローが起こると、プログラムは意図しない動作をする可能性があります。したがって、そうした状況をあらかじめ防ぐために、安全設定としてその誤差が発生した時点でプログラムを中断させるのが一般的です。これがプログラムの安全性を高める基本です。
オーバーフローというと多くの人はプラス方向に限った話をイメージしがちですが、実際にはマイナス方向も含まれます。例えば、-129はオーバーフローに該当します。そして、特定の計算例で考えてみると、もしlet c: Int8 = a + b
としたときに、計算結果が155になるとしましょう。a
とb
の和が155であれば、それはInt8
の範囲を超えているので、このような場合もオーバーフローに該当します。 127を超えてしまうといった場合には、Swiftではランタイムエラーが発生してしまいます。それを恐れる人も多いかもしれませんが、Swiftではプログラマーが意図しない結果が出てしまう際に、計算誤差を早期に発見できるようにして止める動作が行われるのです。これはSwiftが安全性を重視している証拠です。
以前のプログラミング言語、例えばC言語のような言語では、ビット表現が非常に重要視されていたため、オーバーフローで処理が止まってしまうことが逆に問題となることがありました。だから、オーバーフローが発生しても、それを受け入れ、処理を続けるアプローチが許容される場合もありました。このようにオーバーフローを許容する考えがコードやアルゴリズムに反映されていることもあります。
このため、Swiftではオーバーフロー演算子を使うことによってオーバーフローを許容する演算が可能です。少し変わった表現ですが、例えば「&+」を用いることでオーバーフローを許容した演算ができるようになります。
プログラマーが意図しないオーバーフローによって、たとえばマイナスのアドレスを指定してしまうと、指定した範囲外にアクセスしてしまう危険性もあります。そして、プログラムが稼働する当時のメモリ配置によっては、プログラム自体が書き換わってしまう危険性もあるのです。特にこれに関連しているのが、バッファオーバーフロー攻撃で、メモリ扱いの不具合が悪用されてしまうことがあります。
現代のプログラミングではメモリ保護がかなり厳重になっているため、それほど深刻な問題ではありませんが、依然として注意が必要です。オーバーフローを防ぎたい場合は基本的にオーバーフローを防ぐための処理を選択しますが、オーバーフローを許容したい場合もあるため、Swiftはその選択肢をプログラマーに提供しています。
ビット表現についても見ていきましょう。たとえば、2進数での表現を取得する際は、String(Int8.min, radix: 2)
といった方法でビットを取得することができます。ただし、これは直接int8の値をビット表現に変換しているわけではないので、特定のビットパターンを意図して取得する場合はさらなる工夫が必要です。例えば UInt8(Int8.min)
を使ってビットパターンを収集し、2進数で表現する方法もあります。
頭に0を付けて8ビット表現にしたい場合は、適切にゼロパディングを行って、見やすくすることができます。こうした細かな配慮もプログラミングにおいては重要です。 まず最初に、GitHubのCopilotを使用してビットパターンに関するコードを試してみたところから始めます。int8の最大値に関連するビットパターンを扱ってみると、GitHubのCopilotが良い感じにコードを補完してくれるので、Copilotに興味がある方はぜひ試してみてください。
ここでは、2進数としてビットパターンを視覚化して、その上で普通の計算をしてみました。最初に、int8の最大値だけでなく、AとB、Cといった変数の値を知りたかったんですが、それを裏側でどういったビット表現がされているかを見ることにしました。コードの書き方が少しひどいので、あまり真似しないでください。今回は便宜上そうしています。2進数が出来上がると、上2つの値を足して下1つになる、という形になります。
計算の過程を少し具体的に見ると、2進数では0か1ですので、たとえば0と1を足せば1になります。次に1と1を足すと結果は10になり、1が繰り上がります。これで次の桁に進むと、今度は0と0の足し算になりますが、繰り上がった1が加わり、その結果1になります。このように進めていくと、最後には符号付き整数において、最後のビットは符号として扱われるので、数値としては7ビット分の表現となります。
これが符号に干渉するとオーバーフローが発生します。Swiftではこういったオーバーフローを検出してくれるので、大変安心です。しかし、C言語などではこのオーバーフローが検出されず、そのまま符号に干渉します。その結果として、たとえば100を55と足して155になるはずが、オーバーフローによってマイナス101になってしまう、ということが起こります。このような問題が実際のプログラムで深刻なバグにつながることは少ないかもしれませんが、注意が必要です。
Swiftはオーバーフローをしっかり検出する仕様になっているので、安全に使用することができます。例えば、パーセントや割り算でオーバーフローが起こらないように設計されています。演算子に関しても安全な設計がなされているので、実際に問題が起こるリスクが低く保たれています。こうした安全設計が安心をもたらしてくれるので、Swiftの良い特徴として押さえておくことで、コードを書く際にも役立つでしょう。 オーバーフローの動作を選択可能にすることは重要ですね。現代のビットで構成されているプログラミングの世界では、オーバーフローもその使い方次第で重要な役割を果たすことがあるため、選択肢があるのは嬉しい点です。Swiftの演算子としては、これまで紹介したC言語など他の言語でよく見られるもののほか、独自のものも用意されています。範囲を表現する演算子がその一つです。これらは便利ですが、全く新しいわけではなく、他言語にも似たものがあります。
範囲演算子について、A..<B
と書くとBを含まないAからB未満の範囲を、A...B
と書くとAからBを含む範囲を表現します。特に、左側のA..<B
の表現はプログラムの世界では重要です。なぜなら、範囲の終端を含まない表現は「半開区間」としてよく使われ、アルゴリズムが簡潔に書けるという利点があるからです。たとえば、配列において中間のインデックス操作を行う際に、この形式がよく利用されます。
具体的な例で言えば、ある配列から特定の範囲の要素を取得する際に、開始インデックスを含み、終了インデックスを含まない形で範囲を指定することが一般的です。このスタイルは「エンド」を含まないという文化に根ざしています。
プログラムでエンドやラストといった単語の使い方についても少し触れておきます。例えば、ラストという言葉は具体的に存在する最後のものを指し、エンドは存在しないもの、例えばファイルの終端やテキストの終端を指します。このように具体的な存在がある場合にエンドと名付けると誤解を招く可能性があり、逆に存在しないものにラストを付けるとやはり誤解が生じるため注意が必要です。
今日は時間となりましたので、ここまでにしておきます。次回は祝日を挟むため月曜日以降にまた続きをお話ししたいと思います。それでは!