https://youtu.be/7Mgsvm_N9hc
今回は The Basics の 定数と変数
の中から、その定義方法みたいなところを見ていきます。初歩的な内容で、慣れてしまえば空気を吸うように何気なくできるところですけれど、今に振り返るとどんな風に見えてくるのか。また、慣れてない人にも Swift の特徴を窺い知れたりするところ、そんな辺りが見どころになりそうな予感がします。どうぞよろしくお願いしますね。
—————————————————————————— 熊谷さんのやさしい Swift 勉強会 #88
00:00 開始 00:40 変数や定数とは 01:06 名前と特定の値とを関連づける 03:04 変数の値とメモリー領域 04:41 ポインター 06:02 メモリー領域を意識しない時代 08:54 値と参照の雰囲気 11:29 実際のインスタンスの扱い 12:56 クロージャー内で外側の変数を使う 13:50 Copy-In Copy-Out 15:14 どこまで突き詰めるかで違ってきそう 17:46 inout と参照渡し 20:36 キャプチャーとクロージングオーバー 22:40 参照型とクロージングオーバーの差異 24:51 究極的にはポインターに行き着く 26:21 キャプチャーリストは定数扱い 30:50 値型とスタック領域 35:04 構造体のデイニシャライザー 35:56 スタックの概念とスタックポインターの概念 38:29 ポインター型で管理する値は後始末が必要 40:20 現代のスタックの印象 40:44 構造体の生存期間延長 43:50 次回の展望 ——————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #88
はい、じゃあ今日は定数と変数、基本について話します。セクションは「ザ・ベーシックス」の部分にあたります。このスライドを具体的に見ていきます。前回はタイトル的なスライドから話が脱線して、disset
の話に進んでしまいましたが、今日はもっとシンプルな基礎的なところを見ていきます。
まず、定数や変数とは何かという話から始めます。こういったものは当たり前になってしまい、思い返すことが少ないのですが、スライドを作りながら振り返ると特に難しいところはないですね。公式のSwiftの本には、「名前と特定の値とを関連付ける」と書かれています。この「関連付ける」という言葉が重要で、ある意味で「代入する」とも言えます。要は、入れ物かラベルかと捉え方が違うだけですね。
昔は変数と言えば「入れ物」と例えられることが多かったですが、最近は「ラベル」として説明されることも増えてきました。例えば、let a = 10
とした場合、「変数a
に10を代入する」という言い方もあれば、「変数a
と値10を関連付ける」という言い方もあります。どちらでも同じように捉えられる気がします。
a
と指定すると、その値が導き出せるわけです。変数a
のポインターを確保する実体があり、そのポインターが具体的な値を指し示します。関連付けるという考え方も、ポインターと同じように見えるかもしれません。視点の違いによって、メモリーや物理的な入れ物としても捉えられますが、それが仮想的な概念であるため、少し異なる表現にもなり得ます。
ただ、メモリーを意識する人にとっては、やはりポインターという考え方がしっくりくるかもしれません。Swiftでは変数がどこに実際に置かれているかを気にしなくて良いので、自由にメモリーを管理できるようになっています。変数a
がどのメモリー番地に入っているという具体的な話が不要になり、プログラマーは抽象的に捉えることができます。
具体的な例を挙げると、ストラクトとクラスのインスタンスを考えてみましょう。もしstruct
の値とclass
のオブジェクトが存在して、それを変数に代入した場合、ストラクトの値は直接値を持っている一方で、クラスのオブジェクトはポインターを持つことになります。
例えば、
struct MyStruct {
var value: Int
}
class MyClass {
var value: Int
}
こうしたとき、
let a = MyStruct(value: 10)
let b = MyClass()
b.value = 20
それぞれ、a
が持っているのは実際の値ですが、b
はポインターを持っています。これをもとに変数c
を定義すると、
let c = b
この場合、c
はb
と同じオブジェクトを指しています。ストラクトの場合は値がコピーされますが、クラスの場合はポインターがコピーされるため、同じメモリー空間を指し示すことになります。
このように、Swiftでは変数と値の関連付けがメモリーと切り離され、より抽象的かつ直感的になっています。これにより、プログラマーは具体的なメモリー管理から解放され、効率的なコーディングが可能になります。
いずれにしても、変数が「入れ物」として説明されることは徐々に減り、名前と値を関連付けるという表現が主流になってきています。この方向性は、より現代のプログラミングに適した考え方と言えるでしょう。 ときに、CはPを向いているのかオブジェクトを向いているのかという疑問がありますね。えーと、9行目の構造体の発想も組むとこうなるはずですけれど、ああ、そうか。見ているのは一緒ですね。そのCも絶対にBPはあるんですかね。なんかPのイメージがなかったですね。なんとなくBが直接オブジェクトを見ていて、Cもオブジェクトを見るという感じで、新しいポインターCがオブジェクトを向いているようなイメージです。
そうすると、9行目的にはAはバリューと同値になってくるという感覚にもなります。なんか一緒なのかなって思っています。その値型も参照型も基本的にはその内部構造はわかりませんけど、Aもバリューに対するポインターで向いていて、ただ制御上、値型は偶然というか参照型みたいにはなっていないという制御です。バリュー型とかはスタックに置くことが多くなりがちで、オブジェクトみたいなのはアプリケーションヒープに置きますよね。なので構造が一緒かどうかはちょっとわかりませんが、やっぱり扱いは違うんだろうなと思います。
もちろん、バリュー型をヒープに置くこともあるでしょうけど、やっぱり場所が違って扱いも違います。スコープなども結構自動的に制御されるので、コンパイラーがバリュー型をそう扱いやすい形で管理すると考えると、同じかどうかはわからないですよね。
クロージャーにAを渡すとき渡すというか、クロージャー内でAを参照した場合はどうなるんでしょうね?
これはキャプチャーするので、メモリを別空間で取るみたいなことになります。クロージャーAは参照渡しになります。キャプチャリストを使えば値渡しになりますが、基本的には参照渡しです。僕のメモにはそう書いてあります。
クロージャーの場合、このAは参照渡しになって、キャプチャリストを使うと値渡しになります。inout
を使うと参照渡しになりますが、値渡しはないという話です。以前の勉強会で熊谷さんが言ってたことですけど、確か「キャプチャリストは値渡しじゃなくて変な挙動をする」と聞いた覚えがあります。
例えば、inout
で渡したときにランタイムエラーが起きる可能性があります。ランタイムエラーが起こりそうな場合はマルチスレッドの状況が絡むことが多いです。以前話していた内容を思い出しながらですが、ひとまず今までの話を総合すると、結局どれも見方次第で、そう見えるという話になります。
つまり、構造体もクラスも含めて、このAとかBをポインターと見て、その先に値があってどっちも同じなんだけれど、コンパイラーの制御によるという感じですね。スタックやヒープの関係で違ってくることもあるし、例えば、構造体のself
はint
型のサイズに匹敵しますが、クラスのself
はポインター1個分のサイズになるというメモリサイズの違いが出てきます。
つまり、
let a = self
としたとき、構造体とクラスではサイズ感が全然違うということです。これは非常に重要な違いです。サイズ感が違うことで、管理のされ方やパフォーマンスに影響を与えます。
また、Swiftのinout
はC言語などで使われる&
記号と異なり、コピーイン・コピーアウトのアプローチを採用しています。バリュー変数を確保して値を関連付け、距離が終わった後に書き戻すというプロセスです。このアプローチは一昔前には「ライトバック」とも呼ばれていましたが、今ではコピーイン・コピーアウトとして認識されています。
以上が皆さんの質問やコメントに対する説明です。勉強会では引き続き、このような言語特性についての理解を深めていきましょう。 実際のところ、これを最適化コンパイラでコンパイルすると参照渡しで済ませます。でも、構文上はそんな感じで、参照渡しというのは値型の話ですね。クラスは絶対に参照渡しです。これは着目点によりますが、参照渡しはあくまでも値型の話ですね。
クロージャについてちょっと面白いことがありましたので、ここも補足しておきます。コードがボリューム増えてきましたが、ここだけでいいんです。クロージャの面白いところとして、キャプチャリストがないときには、中で使った外の変数をクロージングオーバーして、そのまま使うという特徴があります。この厳密な動きまでは把握していないんですが、基本的にはコピーもしていないし、参照でも渡されていないんじゃないかと思います。
例えば、コードの19行目で A
が使われているとします。これでクロージャの中で A
の値を変更したら、外側の A
にも影響が出ます。具体的には、例えば、以下のようにします。
var A = 0
let closure = {
A = 5
}
closure()
print(A) // 5が出力されます
このように、クロージャの中で A
を変更すると、外側の A
にも変更が反映されます。これは、クロージャが外の変数を参照しているためです。
もう少しシンプルな例にすると、例えば Int
型で初期値を 0
にしといて、クロージャの中で 5
に変えたとします。外側で A
をプリントすると、多少コードをカットしてこんな感じになります。
var A = 0
let closure = {
A = 5
}
closure()
print(A) // 5が出力されます
つまり、中のものが完全に外と一致しているんです。参照なので、クラスも同じような動きをします。したがって、値型と参照型の差がないようにも見えます。
例えば、以下のような関係でポインタの先を変更するのか、新しいオブジェクトをコピーして新しいポインタを持ってくるのかはコード次第です。もしキャプチャリストで A
をコピーした場合、
var A = 0
let closure = { [A] in
A = 5
}
closure()
print(A) // 0が出力されます
このようにすると、クロージャ内で A
がコピーされ、新たに独立した変数になります。
この考え方をすると、Structも参照型も結局はポインタでやり取りしていると整理するのが自然だと思います。どこまで考えるかという問題でもありますが、究極的にはポインタであるというのは間違いないでしょう。
クラスと構造体の違いを出すとしたら、クラスは強参照や弱参照などの管理ができるという部分があります。しかし、値型だとそのような参照の概念は言語仕様上出てきません。リテインカウンタのようなものも存在しません。 次に、Swift言語のキャプチャリストに関する話をしました。
例えば、変数 A
に代入が禁止されている場合についてです。本来ならば、5を書き換えた際に、10行目の値が5に変更されることを期待していたのですが、キャプチャリストを用いた途端に10行目の値が0のままになってしまうという話ができたかもしれませんが、現行の仕様ではそれは許可されていません。
変数のシャドーイングについても話しました。現在の仕様では、新たな変数 A
をキャプチャする際に var
という表現ができなくなっています。これにより、元の変数に影響するのかキャプチャされた変数に影響するのかが分かりやすくなったため、この仕様が廃止されたようです。
もし、4行目で var A = A
とすれば、10行目の外では0のままとなります。この話を理解していれば、シャドーイングについても理解できるでしょう。
次に、構造体とクラスの違いについても簡単に触れました。例えば、構造体 S
の場合、A.value
に5を代入すると、構造体側の値は変更されますが、外部には影響しません。一方で、これをクラスに置き換えると、両方の値が5に変更されます。これは参照の仕方が若干異なることに起因しています。
ここまでの話をまとめると、構造体はスタックに積まれて不要になるとスタックポインタを戻すだけで解放できる。一方、クラスはヒープ領域にインスタンスを確保して、参照がなくなったときに解放されるという違いがあります。
更に、スタックとヒープの管理方法について説明しました。例えば、スタックメモリ10バイトが必要な場合、スタックポインタを10足して処理を行い、使い終わったら10戻すのが基本的な流れです。しかし、クラスのオブジェクトを内包している場合、スタックだけで管理するのは難しいと考えられがちです。このとき、ARC(Automatic Reference Counting)が働いて、オブジェクトが解放されることを確認しました。
要するに、ARCは参照カウンタを減らすためのリリースメソッドを呼び出し、オブジェクトが解放される前にカウンタを減らす必要があります。そのため、コンパイラが適切にリリースメソッドを差し込むことが重要です。 うん、そう、賢い感じです。ARCのオートは基本的にはコンパイラーがそこにrelease
を差し込みます。だから、バリューが解放されるときにバリューのオブジェクトのrelease
まで、しっかりとコンパイラーが入れてくれるということになります。その場合、たぶんそうでしょうね。ARCが働いていて、ここに勝手にコンパイルすると勝手に入れてくれるはずです。賢いですね。
ウィークの場合はその振る舞いが変わります。プロパティを持っている場合に、そのプロパティを何かチェックしてrelease
するかどうかとかに関しては、コンパイラーがうまく処理するのでしょう。バリューが持っている全プロパティは把握できるため、賢くやっていると思います。
28行目に直接入れるというよりは、バリュー型の管理の方に何か特殊な事情があるのでしょうか。そうですね、deinit
はないけれど、実は内部で処理をしているようです。なので、スタックを戻す前に全てデアロケートを行う動きになっていると考えられます。
つまり、deinit
は公開されていないけど、コンパイラーが適当に生成してくれる感じです。その結果、スタックポインターがサクサクっと動作するように見えますが、実際には高度なスタックになっています。メモリポインターとしては、UnsafeBufferPointer
、UnsafeMutableBufferPointer
、UnsafeMutablePointer
などがあり、これらにも面白い特徴があります。
例えば、デアロケートを行うメソッドと、その前に呼ばなければならないdeinitialize
というメソッドがあります。このdeinitialize
を呼ばないと中のクラスが解放されません。
実際にやってみましょうか。例えば、以下のようにコードを書きます:
let value = [11]
let pointer = UnsafeMutablePointer<Int>.allocate(capacity: value.count)
pointer.initialize(from: value, count: value.count)
この状態でdeinitialize
を呼ばずにdeallocate
することは推奨されません。以下のように呼ぶ必要があります:
pointer.deinitialize(count: value.count)
pointer.deallocate()
これによって、クラスのデアロケートが正常に行われます。このdeinitialize
を省略すると解放されなくなります。
値型の場合には、deinitialize
を呼ばなくても問題ありませんが、参照型の場合には必ずdeinitialize
とdeallocate
をセットで呼ばないとメモリーリークが発生します。 ですので、必ずアンセーフミュータブルポインターを使用する場合は、deinit()
とdeallocate()
の両方をセットで呼ばないといけません。その時は問題がなくても、その構造体が参照型を内包するようになった場合にメモリーリークが発生することがあります。ここは注意が必要なポイントかもしれませんね。先ほど須藤さんが指摘していた、deinit
の中に追加するという話と合致する内容です。
deinit
が呼ばれないとメモリーリークするということですね。これは、本当に気を付けないといけない部分ですね。型に関連づいているということで、暗黙的にdeinit
があるということが察しやすい部分かもしれません。
また、関数があり、その関数something
が戻り値としてクロージャーを返す場合があります。例えば次のようなコードです:
func something() -> () -> Int {
var a = 10
return {
return a
}
}
この場合、変数a
はInt
型なので値型です。普通はスタックに保存されると思いますが、クロージャーを返すということは、この先でsomething()
を呼び出して得たクロージャーを利用する際、a
がその場で使われます。つまり変数のライフサイクルがクロージャーにより延命されます。
例えば、
let g = something()
print(g()) // ここでaが使われる
このようにしてa
がスタックの生存範囲を超えて使用される場合、変数a
はヒープに置かれる可能性があります。このような状況が起こるときには、スタックに値を保存しておくわけにはいかないのでヒープに保存されるのです。
尚、クラス型のオブジェクトは基本的にヒープに置かれますが、状況に応じてこのように変わりますので、メモリの使い方やライフサイクルについても理解が深まるかと思います。次回からも引き続き、このような興味深い話題を取り上げていきましょう。
それでは、今日の勉強会はこれで終了です。思いのほかポインター周りの理解が深まりましたね。次回もこの続きからまた勉強していきましょう。興味があればぜひご参加ください。お疲れ様でした、ありがとうございました。