https://www.youtube.com/watch?v=LE6xqlQeTYc
今は Swift.org の About Swift
を眺めていっていますけれど、前回で 追加機能
を見終えましたので、今回は新たに 安全性
のところから見ていきます。安全性は Swift が最も大事にしているとも言えたりしそうな主要な特徴。どうぞよろしくお願いしますね。
—————————————————————————— 熊谷さんのやさしい Swift 勉強会 #9
00:00 開始 00:32 安全性 01:27 波括弧が必ず必要 02:25 Apple 社の事例 04:49 波括弧が省略できる言語でのバグ事例 08:02 break と fallthrough 09:33 変数は初期化が必要 10:45 メモリーに残るデータを覗き見られる可能性 11:29 未初期化な値を参照する可能性 13:27 確定初期化 14:30 NULL 安全 18:37 自動メモリー管理 23:41 インクリメント演算子の廃止 27:35 安全ではない仕様を排除 29:20 配列を初期化しないで使う 35:05 ポインターを初期化しないで使う 36:54 整数型のオーバーフローチェック 42:13 オーバーフローを許容する演算子 46:38 メモリーの自動管理 50:37 次回の展望 ——————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #9
今回は前回、今画面に表示している三つ目の項目について見ていきましたが、先送りにしていた二つ目の項目、つまり「About Swift」という文章の中の「安全性」について、皆さんと一緒に見ていくことにしましょう。
まず、他のC言語ベースのプログラミング言語よりもSwiftが安全に設計されているという点についてです。色々と思い当たる部分が多いかと思いますので、面白いポイントがあればぜひ教えてください。前回もお話ししたところもありますが、その復習を兼ねていきましょう。
まず一つ、前回のZoomのコメントの中で教えてもらった良いポイントについて紹介したいと思います。C言語ベースと比較して、Swiftがどこが良くできているかという点です。C言語をやっている人は共感するかと思いますが、例えば変数があって、if
文を使ったときにSwiftでは括弧が絶対に必要という点です。これは何気ないようでとても大事な安全性の担保の一つです。
具体的にどんなメリットがあるのかというと、以前Appleがやらかしたという話の中で、switch
文と勘違いしていた件がありましたが、実はif
文の問題でした。前回お話ししたのは、switch
文のケースで、C言語などではbreak
を書かないと次の処理へ行ってしまうという問題がありました。例えば、あるケースに条件がマッチした場合に何か処理をした後にbreak
を書かないと次のケースに進んでしまうという問題です。
具体例として、条件が真ならprint("a")
を実行し、偽ならprint("b")
を実行するコードを書いたときに、C言語ベースの場合は必ずbreak
を毎回書いてあげないと本来やりたい処理以外の処理も実行される可能性があります。Swiftでは、デフォルトでbreak
が必要なく、予期しない動作を防ぐための設計がされています。
これによって、Appleがbreak
を書き忘れてやらかした例について、実際にはif
文で発生した問題だったことが分かっています。ここで具体的に説明すると、C言語の頃は並括弧を使わなくてもif
文が書けました。例えば、以下のように書けました。
if (condition)
printf("a");
printf("b");
この場合、if
文の直後に書いた一つのステートメントだけが条件によって実行され、それ以外のコードは条件とは無関係に実行されます。このため間違ったインデントになっていると、条件で処理される部分と無条件で処理される部分が混ざる可能性があります。
こうした微妙な不具合を防ぐために、Swiftでは並括弧を必ず書かせて影響する範囲を明示しています。これにより、print("A")
とprint("B")
がif
文の条件によって実行されるか否かを明確に制御できます。プログラマーが意図した通りの動作を保証するために、Swiftはこのような安全性を高める設計がなされています。
このようにSwiftは、他のC言語ベースのプログラミング言語に比べて安全性を意識した設計がされています。 スイッチ文についてもう一度お話に戻ります。C言語では、処理を止めたいところでbreak
を書かなければならない仕様になっていました。これは危険で、C言語を使っていた方なら危ない目に遭ったことが多いと思います。意図しない動作が発生することがあるため、見落としがちです。
Swiftでは、デフォルトでbreak
が自動的に挿入されるため、安全性が高いです。もしも、次のケース文に処理を進めたい場合は、fallthrough
を明示的に書く必要があります。C言語がfallthrough
をデフォルトにしていたのと対照的ですね。たとえば、条件がtrue
になって、スイッチ文で条件が一致したからAの表示がされ、その後fallthrough
だから続けて次のケースも動作する、というように意図して書くことができます。意図していない場合は、そのケースだけが動作します。このようにSwiftのスイッチ文は安全性を強化しています。
また、Swiftでは変数を初期化せずに使用することは許されません。例えば変数に何も値を入れないまま使うことはできず、必ず初期化が必要です。C言語やObjective-Cでは、初期化を怠ってしまうと、その時点でメモリにたまたま入っていた値が表示されてしまうことがあります。こうした問題を避けるために、Swiftは初期化の徹底が求められています。
安全性に関して、二つの大きなポイントがあります。まず、予期しない値が見られることを防ぐ点です。例えば、以前にメモリ領域に入力されたパスワードのような情報が残っていた場合、その値が意図せず再利用されてしまう可能性があります。これによってパスワードが盗まれるなどのセキュリティリスクが発生するかもしれません。
もう一つは、自分で初期化したつもりでも、初期化されていないまま計算してしまうリスクです。例えば、条件がtrue
だった場合に変数a
に値を設定し、false
だった場合に-1を設定するロジックがあるとします。条件が複雑になり、うっかり初期化を忘れてしまった場合、その変数にはたまたまメモリにあった値が代入されてしまうかもしれません。これにより、思いがけないエラーが発生する可能性があります。
Swiftはこのような初期化ミスを防ぐために、初期化が漏れているときにエラーを表示します。初期化さえ行えば、エラーがなくなり、正常に動作する設計になっています。変数や定数を宣言だけして値を設定しないまま、その先のコードで必ず値を設定してから使用することをdefinite initialization
(確定初期化)と呼びます。
この確定初期化の仕組みについてさらに興味があれば、調べてみると面白いかもしれません。また、他にも多くの重要な概念があります。例えば、オプショナルなどです。C言語ではポインターにnull
が入っていた場合など、たとえばint *p = NULL
など、これがC言語の記法だったと思います。 とりあえず雰囲気だけでも掴んでもらえればと思うんですけど、それでPを参照しようとしたときにヌルだったらランタイムエラーが起こるみたいな、多分C++やJavaのオブジェクト指向のほうがわかりやすいかもしれません。
C言語においては、実際にコードを書いてみたときにヌルだったらランタイムエラーが発生するわけですが、それを防ぐためにプログラマーが事前に「Pがヌルでなければ」などのチェックを入れたりします。しかし、そうしたチェックを意図せずに忘れてしまうと、コード自体はコンパイルを通過してしまいます。Swiftでは、これをオプショナル型で表現して、例えば以下のような形でオプショナル処理を明示的に記述します。
var p: Int? = nil
このようにしておけば、p
を使用しようとしたときに、ヌルの可能性をプロンプトで知らせてくれます。例えば、print(p)
で済む場合もありますが、p + 1
のように加算しようとする場合はオプショナル型を解消しなければなりません。これは、ヌルが入っている可能性があるからです。
プログラマーに対してヌルの可能性を意識させる効果があります。もしヌルが入っているはずがない場合には強制アンラップ(!
)を使ったり、ヌルが入っているかもしれない場合にはオプショナルバインディングを使って値が入っていた場合の処理を記述するとう感じです。
if let p = p {
print(p + 1)
} else {
print("p is nil")
}
これによって、予想外のヌルを未然に防ぐことができ、安全設計になっています。C言語と比べると、このような点が大きなメリットになるでしょう。
他に何か思い浮かぶ方がいますかね? C言語と比べて「ここが安全になった」というポイントについてですが、たくさんありますね。例えば、メモリの管理に関しても触れておきましょう。C言語やC++ではメモリの確保や解放を手動で行います。たとえば以下のようなコードです:
int* arr = (int*)malloc(sizeof(int) * 10);
// 使用後に
free(arr);
しかし、Swiftでは以下のように書くと、メモリのアロケーションとイニシャライゼーションが自動で行われ、スコープを抜けるときには自動的にメモリ解放が行われます。
var array = [Int](repeating: 0, count: 10)
ケアをしないといけない部分が省略されるので、プログラマーの負担が軽減されます。また、オブジェクトの参照も簡単です:
let instance = SomeClass()
let anotherInstance = instance
この場合、instance
が参照型なら、その参照がコピーされます。また、instance
が値型なら、そのメモリの内容がコピーされます。C++とは異なり、特別なメソッドを記述しなくても済むのは大きなメリットです。
さらに、C言語系との違いとして、コメントで教えてもらったのがプリプロセッサの使い方です。例えば、++i
のような、C言語をやったことがある人にはおなじみの操作も、Swiftでは特に意識せずに済みます。
このように、Swiftはメモリ管理やエラー防止機能など、開発者にとって非常に親切な設計がされています。これが、C言語やC++から移行する際に感じる大きなセキュリティと利便性の向上に繋がるのです。 「これとこれの違いを覚える」という点は、とても良い例を挙げてくれています。例えば、「i += 1」と「++i」ですね。この例は非常に分かりやすいです。
まず、var i = 5
としてみましょうか。このときに何が起こるかを考えるわけです。ゆっくり考えれば分かりますが、5に1を足した結果を再度足しているので、最終的な値は6となります。こういったことを一生懸命考えるうちに、もはや分かりにくくなってしまう状況が理解できると思います。
Swiftでは、これを廃止しました。もともとC言語の流れを汲んで設計されたSwiftだったので、初めのうちはこの演算子が存在していました。しかし、Swiftではこの「++」演算子が早々に廃止されました。現在は i += 1
のように記述します。確かに長くはなりますが、先に足す感じで書かれています。こうすることで、風通しの良い、安全で分かりやすいコードになります。
このように、誤解を招きにくくし、プログラマーが意図した通りに動作するようにするために、これら2つの演算子が廃止されたことは非常に重要です。また、C言語における for (int i = 0; i < 100; ++i)
や i++
など、どちらが良いかという宗教論争にまで発展する話題もありますが、Swiftでは合理的でスマートなコードを追求することができます。自分は ++i
の方が理にかなっていると感じていますが、これにも個人差があります。
次に、安全ではないコードの分類に含まれる仕様について説明します。Swiftでは、これらを排除していますが、共存させるための方法も存在します。例えば、変数は使用前に必ず初期化が必要です。これは確定初期化の話に繋がります。また、配列と整数型はオーバーフローチェックが行われ、メモリーは自動管理されます。
具体的に見ていきましょう。まず、配列です。基本的には初期化しないとエラーが発生して通過できません。でも、配列だけに関して言えば、たとえば int
型の配列を初期化しようとするとします。こういう場合には、unsafeUninitializedCapacity
を使って、未初期化の配列を作ることができます。このメソッドでは、キャパシティを指定し未初期化状態の配列を作成します。
例として、キャパシティ10の配列を作成し、initializingWith
でバッファーポインターを使用して初期化します。このメソッドは最初に指定したメモリ領域の長さだけ作成し、その中で初期化を行います。
次に、メモリーの自動管理についても触れます。Swiftでは基本的にメモリーは自動管理され、手動でメモリー管理をする必要がありません。ただし、必要に応じて手動で管理する方法も提供されています。
Swiftのこのような仕様は、安全性と合理性を優先して設計されていますが、プログラマー自身がそのルールを理解し、適切に活用することが求められています。これらの点を理解することで、より安全で効率的なコードを書くことができるようになります。
次に紹介するのは、整数のオーバーフローチェックとメモリーの自動管理についての詳細です。さらに具体的な例を示して、これらの機能を実際にどう利用するかを説明していきたいと思います。 だから、このままLengthにだけ5個初期化が終わったよ、といったことを返してあげて、それでlet values =
とかやって、これをprint(values)
とかすると、デバッグモードではちゃんと全部ゼロだったと思うんですよ。
もうだめですね。こんなふうに未初期化の状態でインスタンス化ができちゃう配列が用意されているので、パフォーマンスが気になるときはこれを使って未初期化のままメモリを確保し、必要なものを計算して入れていく、といったことができるようになっています。特別なこういうイニシャライザを使うことで可能です。だから、一応パフォーマンスも考慮されていて、必要に応じてパフォーマンスを出せる設計になっている感じですね。
これは特別な場合ですよね。こういったものは用意されていない印象があります。ポインターについてどうだったかな。アンセーフミュータブルバッファポインターがありましたよね。例えば、以下のようにします。
let buffer = UnsafeMutableBufferPointer<Int>(start: p, count: 10)
これでprint(buffer)
とすると、初期化しないんですね。できました。こんな感じで初期化せずに確保する方法もあるみたいですね。
理解できない方は意味不明と思って聞いてもらえればOKです。いつか役に立つこともあるかもしれません。
次に、整数型のオーバーフローチェックについてお話します。オーバーフローチェックとは何か、現時点のコンピュータプログラミングでは、整数型の値は必ず有限のメモリの中で表現されています。例えば、10万ぐらいの数字なら表現できますが、これが極端に大きくなるとエラーになってしまいます。
プレイグラウンドが反応してないので、しゃべりますが、Int
型というのはサイズが決まっていて、メモリレイアウトで確認すると8バイト(64ビット)で表示されます。つまり、8バイトで表現できるものまでしか表せません。
ちなみに、自分はちょっと古いプログラマーなので、32ビットのほうが好きです。32ビットの上限は65,535です。それで普通に動くわけですが、例えば、
var value = 65535
value += 1
print(value)
としたとき、value
が32,768になります。int16
型だと、16ビットの上限が32,767なので、32,768を表現できないんですよ。符号付きではね。これでランタイムエラーが起きてしまうわけです。
オーバーフローすると、何も知らずに使っていると大きなバグにつながる可能性があります。そこで、Swiftではランタイムエラーで止めてくれるという安全性のポイントがあります。
その値がどこまで取れるかというのは、int16
型ではmax
プロパティで確認できます。
print(Int16.max) // 32767
オーバーフローは基本的にできないということになっていますが、例えば&+
という演算子を使うとオーバーフローが許されます。
let value = Int16.max
print(value &+ 1) // -32768
これが二進数で表現されます。1
が先頭の符号ビットで、プラスかマイナスかを表現するものですね。 桁が増えすぎてわかりにくいと感じたら、Swiftの場合はアンダースコアをリテラルに入れて桁区切りをすることができます。ビット周りのミスが多いですね。しかもこれは、Swiftがイント型として捉えているので、16として表現しないといけないです。これはオーバーフローしますね。ドツボにはまっていきましたが、これをやる場合は意味的に16でビットパターンにして、このビットパターンを与えてやればいいのですね。こっちの方がちゃんと動きそうですね。まどろっこしいことを書きましたが、本来ちゃんと用意されているものを使っていけばちゃんと動きます。
C言語とかでは&
プラス&
マイナス&
かけるみたいな演算子がありますが、Swiftでも同等の演算子があり、オーバーフローしたらそのまま突き進む、つまりオーバーフローチェックをしないで動くので、高速に演算が行えるというわけです。Swiftは安全性を考慮してオーバーフローチェックを行い、安全性を担保する代わりに処理速度が少し犠牲になっています。もし四則演算で高速に処理する必要があるとか、オーバーフローしても構わない場合は、&
付きの演算子を使うとパフォーマンスを上げることができます。
&
プラスって何をしているのかと思うかもしれませんが、チームで開発するときはそのあたりのバランスを考える必要があるかもしれません。「ここでオーバーフローチェックを飛ばす」と書けば伝わりますね。オーバーフローチェックが気になるときにはこうやって回避する方法もあります。
これでどれくらい処理速度が違ってくるのかは、例えばフォーループで10万回くらい試せばわかるでしょう。これは後で個人的に試してみますね。ただ、オーバーフローチェックが行われるよと言っても、されない方法も用意されています。
あと、メモリーの自動管理についてもお話ししましたが、「アンマネージド」というものがあります。クラスを作って、それをアンマネージドで管理するときにはイニシャライザーなどを使わない方法があり、この場合も「from opaque」「path retained」などを渡す必要がある場合があります。「take unretained value」で、レッドオブジェクトのインスタンスを作成し、通常のインスタンス解放タイミングでリリースされる挙動になりますが、アンマネージドでオブジェクトを渡すとリファレンスカウントを増やさずにキープできます。これで変数に取っておき、「take unretained value」で参照カウンターを増やさずに確保したり、増やして取得することができるわけです。つまり、メモリーマネジメントを自力で行う手段も用意されています。
アンセーフミュータブルポインターのようなものもあり、これでint型のポインターをアロケートして1個だけ作り、そのポインターに値を入れれば、参照リファレンスだけがコピーされていきます。メモリー管理を自分で行う方法もあるんですね。
Swiftは多様な使い方ができるように設計されており、初心者からハイレベルなプログラマーまで、各自のニーズに応じたコードを書くことができるのが面白いところです。安全性を考慮しつつもパワフルな動きができる、それがSwiftの魅力です。About Swift
のページを見ていて面白かったので、紹介しました。
このあたりで時間になりましたので、今回の勉強会はこれで終わりです。次回は「ヌル安全」についてざっくりと見ていこうと思います。それでは、1時間どうもありがとうございました。お疲れ様でした。