https://youtu.be/pD9sk6QF1Ws
今回はひとまず前回に話したあたりの補足やおさらいをした後で、A Swift Tour
の 構造体
についての残りのページを眺めていきます。そこからさらに前に扱った 列挙型
についても練習問題を通して見渡していく流れになりそうです。そして時間が残るようなら、次の項 プロトコルと拡張
についても見ていくかもしれません。どうぞよろしくお願いしますね。
—————————————————————————— 熊谷さんのやさしい Swift 勉強会 #61
00:00 開始 01:18 前回の訂正 02:07 クラスクラスター 07:58 ヒープ領域とスタック領域 11:00 値渡しと参照渡し 15:24 同一演算子 17:19 値型のアドレスを取得 20:40 Copy-On-Write の挙動を探る 24:24 配列の再確保をさせないためには? 27:45 代入文の挙動 30:43 内部情報の扱いについて考える 35:24 構造体と列挙型に関する練習問題 37:28 関数の定義場所を考える 38:27 関数にするか、プロパティーにするか 39:13 実装を考える 42:25 関数の所属位置を見直してみる 43:29 適切そうな所属場所を考える 45:51 全てのケースを列挙する 49:00 実装してみた印象として 50:20 クロージング ——————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #61
今日は構造体のお話ですかね。先へ進もうかなと思ったんですが、その前にちょっと前回の続きについて触れたいと思います。前回は構造体とクラスの違いについて、ひとつひとつ具体的な事例を挙げながら話しました。その中で気になったことがないか、勉強会が終わってからでも構いませんので、ぜひコメントや声で教えていただけたらと思います。
前回の内容に触れる前に、訂正したい点があります。前回、自分が「クラスファクトリー」という言葉を使ったと思うのですが、これは間違いで、正しくは「ファクトリーメソッド」と「クラスクラスター」です。この2つの言葉を混同してしまいましたので、ここで訂正させていただきます。
まず、「クラスクラスター」ですが、これはインターフェースを共通化させて汎用的なものを設計し、裏方で最適なものを返すといったアプローチです。例えば、Swiftの既存機能を使うと複雑になるので、自分で作る場合を考えてみましょう。
クラスがあり、例えば String
型があってイニシャライザーを作成します。イニシャライザーは自分の型を返しますが、前回はセルフが書き換えられた場合の話をしました。ストリング型に対して、文字列の長さを取るサイズというプロパティがあり、サイズを返す他に、内部バッファーとして UnsafeBufferPointer
を持たせるようにします。
例えば、EmptyString
クラスを定義し、その中でプライベートバッファーを持つようにします。このバッファーには UnsafeBufferPointer
型を持たせ、ヌルを持たせることができるようにします。そして、EmptyString
クラスのイニシャライザーでは、スーパークラスのイニシャライザーを使い、バッファーをヌルに設定します。
このように特定の条件下で特化したクラスを用意し、イニシャライザーで空文字列 (String
) を受け取った場合に空文字クラス (EmptyString
) を返すようにするなど、特定の条件に応じて返すインスタンスを切り替える設計をします。
次に、「ファクトリーメソッド」ですが、これはメソッドが呼び出されると、引数に応じて適切なインスタンスを返す関数のことを指します。どちらも似たような概念ではありますが、違いを理解しておくことが重要です。
また、スタックとヒープのメモリ管理についても少し話しておきます。一般的に、ヒープとスタックは同じメモリ空間に配置され、ヒープを上から使い、スタックを下から使っていく構造になっています。これにより、メモリ空間が尽きるとエラーになる設計が多いそうです。この点についても共有しておきます。
以上、前回の補足と訂正でした。次回の内容も楽しみにしていてください。 CPUがサポートしていたのがスタックだけだった時代において、ヒープのように自分でメモリを管理する必要がある空間を使う場合、本当に手動でメモリを管理していました。具体的には、アドレスやサイズをメタデータ的にプログラムのどこかに記録しておくか、物理的なノートに書き留めるなどして管理していたのです。
ヒープやスタックについての管理方法は、言語処理系やCPU、ハードウェアに依存する部分もあります。コンパイラが管理する場合もありますので、基本的な考え方は一貫していますが、詳細については言語や環境により異なることがあります。例えば、C言語ならこうだ、という具合です。
では、スライドに戻りますが、構造体とクラスの話について、前回お話した内容の中で何か分からない点や気になる点があれば教えてください。また、スライドにはもう少し特徴の違いについて書かれていますので紹介しておきます。
クラスと構造体の大きな違いは、インスタンスを受け渡したときの扱い方です。構造体は基本的に常にコピーされます。コピーオンライト(Copy-On-Write)という技術により、表向きにはコピーされているように見えても、実際には必要になるまでコピーされない場合もあります。対して、クラスは必ず参照渡しされます。
この特徴の違いが面白い点です。例えば、let A = "文字列"
を作成した場合、この時点で値型(構造体)を作ったことになります。これを別の変数に代入する際に、通常はコピーが行われます。しかし、これが参照型(クラス)のインスタンスを作った場合、=
の代入文では参照が代入されます。このように、同じ=
の代入文でも動きが大きく変わる点が興味深いです。
また、メソッドのパラメータとして渡す場合も同様です。引数として値型を渡すと、その時点で値が複製され、参照型であれば参照が渡されます。この動作は非常に直感的でないこともあり、最初は理解しづらいかもしれません。同じような疑問を持っている方がいれば、両者が新たに変数に代入されているという意識が必要です。
おとといの勉強会での質問ですが、自分で試して解決したので共有します。熊谷さんがサンプルコードでA === B
のようなオブジェクト一致比較を行っていたことについてです。ストラクトで同じことを試した場合、同じメモリ領域を指しているはずですが、値を変更すると結果が変わります。
結局、構造体には===
が使えないと理解しました。これはアイデンティティ演算子(===
)があくまでもクラス型の参照が一致するかどうかを確認する演算子であるためです。標準ライブラリにその定義がないことも確認しましたが、他にも興味深い点がありました。
このような話題が出ると非常に面白いですね。 さて、今回はアドレスの比較について実験しています。パラメーター、つまり変数を var
で定義した構造体があり、その値を関数で受け取るという状況を考えます。ここで、アンセーフポインターを用いてストリングではなく、イント(整数型)でパラメーターを受け取る例を試してみました。
例えば、次のようにイント型のパラメーターを受け取る関数を定義します:
func exampleFunction(a: UnsafePointer<Int>) {
print(a)
}
そして、次のようにして関数を呼び出すと、変数 a
のアドレスが出力されます:
var a = 5
exampleFunction(a: &a)
この結果、整数型の変数 a
のアドレスを取得できました。次に、==
(イコール)演算子がアドレス比較に使えるかどうかを検証しましたが、クラスに限るもので、構造体のアドレス比較には使えないようです。
一方、配列(アレイ)で試してみると、次の通りです:
var arrayA = [1, 2, 3]
var arrayB = arrayA
arrayA
と arrayB
が等しいかどうかをチェックしたところ、初期状態では等しいですが、arrayA
へ新しい値を追加すると、arrayA
と arrayB
は異なるアドレスを持つようになっています。これは、Swift のコピーオンライト(Copy-on-Write)の動作によるものでしょう。
次に、予めキャパシティを設定して比較してみることにしました。リザーブキャパシティを設定しておけば、リアロケート(再配置)が発生しないはずです。以下のコード例では、キャパシティをあらかじめ10に設定しています:
var arrayA = [Int]()
arrayA.reserveCapacity(10)
arrayA.append(1)
このようにしておけば、アドレスが変わらないかどうかを確認できます。もちろん、リアルタイムでアドレスが変わらないことを確認するためには、適切な出力を実装する必要があります。
この疑問に対しては、詳細なメモリ管理の知識や実践的な経験が必要となりますが、この検証を通じて、基本的な理解を深めることができました。分析と検証の結果、特定の状況でコピーオンライト機能が正しく機能していることを確認できたのは有意義な成果です。 とりあえず、false
になりました。これだけで「じゃあアロケートしていないけど」とか、そういった話に持っていくのはちょっと無理があります。とりあえず結果はこんな感じになりましたね。試しにremove
してみればいいのかもしれません。つまり、これで次に append
じゃなくて remove
を試せばいいのかと思いました。具体的には、remove(at: 1)
とかですね。これで実行してみますが、結果はあまり変わらない気がします。
false
ですね。まあ、いいでしょう。何か面白い結果が出てきたかもしれません。それにしても、A
と B
はコピーだから同じアドレスを指していないですね。そもそも、指しているのかどうかは不思議です。これがコピーオンライトの話なんでしょうね。私はまだ半信半疑な部分もありますが、コピーオンライトという機能がもっとソフトウェアの深いところで動いているのではないかという疑いが少しあります。ただ、確かにコピーオンライトだから納得できる部分もあります。
代入文についてもう少し考えてみます。昔から、自分の中では「self = self
」みたいなイメージがありますが、構造体に関しては前回話した内容と一致します。構造体の self
は、そのメモリー空間全体を指しているんですよね。それに対して、クラスの場合の self
は、ポインタの先端を指しているイメージです。この違いから、=
という代入文の挙動が値型と参照型で異なることが理解できるのです。
配列についても、以前勉強会で説明しましたが、構造体で設計されており、その中でバッファをクラスとして持っているという話です。だから、self
が構造体の全プロパティを持つメモリー空間である一方で、バッファの中身に対してコピーオンライトが働くというのも納得がいきます。そのため、4行目で B
と A
が同じ参照を指すというのは、自分の理解と少し違うと感じます。
ここで「セルフ」に関して言えば、配列(Array
)というのは定義であって実体ではないので、メモリアロケーションは buffer
の方が先に来るのでしょう。つまり、バッファのポインタが仮想的に配列のポインタと一致するということです。この観点からすると、メモリアロケーションの話の中で配列(Array)はあまり意味を持たないのかもしれません。
それでは、配列だけが特別な扱いを受けているのか、それとも他の型でも同様のことが成り立つのかについて興味が湧いてきます。実際に構造体とクラスの違いを踏まえて、mutating
メソッドを使ったりして検証してみましょう。例えば、mutating func doSomething()
というメソッドを作って、そのメソッド内で何か処理を行うだけの、それほど重要でない実装を適用してみます。
struct MyStruct {
var value: Int
mutating func doSomething() {
// 何もしない
}
}
var a = MyStruct(value: 1)
var b = a
b.doSomething()
if a === b {
print("true")
} else {
print("false")
}
このコードを実行すると結果は false
です。配列のケースでは特殊な動きがあるのではないかと感じます。おそらくコンパイラの最適化が働いているのでしょう。構造体代入時点でコピーされているので、配列(Array)の場合はその内部のバッファに関してコピーオンライトが機能しているのだと理解できました。非常に興味深い話題です。
ありがとうございます。 それでは、続いて構造体について見ていきましょう。
ありがとうございます。こちらこそありがとうございます。面白かったけど、すごく難しかったですね。Swiftの基本形が、高速に動作するように色々と配慮されていると感じました。
それでは次に進みましょう。練習問題に取り組んでみます。この練習問題は、今回扱っている列挙型と構造体、このセクションにふさわしい両方が使われた問題です。問題の内容は、カードという構造体を使って、1から13までのランクと、スペードやダイヤモンドなどのマークを組み合わせたフルデックのカードを持つ配列を返す関数を作るというものです。
フルデックとは、ランクとマークの組み合わせごとに全種類のカードを返すものです。この問題は考えどころが多そうなので、ゆっくりと見ていきたいと思います。
まず、カードの構造体があって、それでランクとマークを用意します。この要素を使ってフルデックを作っていくという問題になります。どのように進めるといいか考えてみましょう。まず関数をどこに持たせるかを考えます。適切な場所を選ぶためには、APIデザインガイドラインに沿って、所属関係を考えるのが重要です。
フルデックを返すという機能を、カードに持たせるのが良いかを考えますが、少し違う気がするので、フリーな関数として持たせることにします。これでカードの配列を返す関数を作りましょう。
func makeFullDeck() -> [Card] {
var fullDeck = [Card]()
let suits: [Suit] = [.spade, .heart, .diamond, .club]
let ranks = 1...13
for suit in suits {
for rank in ranks {
if let card = Card(rank: rank, suit: suit) {
fullDeck.append(card)
}
}
}
return fullDeck
}
この関数を使うと、フルデックのカードが得られると思います。間違っていなければ、このコードを実行することで期待通りの結果が得られるはずです。
ただ、所属関係がやっぱり気になるので、この関数をカードの外に出す場合、自動的にスタティックバーになるので、それでも良いかもしれません。
以上が、練習問題の解決方法の一つです。どのように関数やプロパティを配置するか、所属関係も考慮しながら設計することが大切です。 これがね、パッケージとしてトランプみたいなものだったら、こういう作り方もありなのかなっていう気がします。でも、何かしら適切なドメインに所属していないと、やっぱりグローバル変数っぽくなって気持ち悪いかなという印象を個人的には受けます。
そうすると、エクステンションで例えば配列のエレメントがカードだった場合、 static var
を拡張して実装するという方法もありかなと思います。こうしてあげると、フルデックは配列カードに対して存在し、フルデックが主体であるカードの配列に対して所属させることができるわけです。
この書き方が43行目のように見慣れないかもしれませんが、あらかじめ typealias
などでカードの配列ですよというふうにコードになっていたとすると、「cardsのフルデック」という感じで見た目が自然に見えるかなと思います。個人的にはこれぐらいがいいのかなという気がします。
この44行目みたいな感じが適切かどうかは周囲のイメージやコーディング規約などによって変わってくるかもしれません。カードのフルデックをもっとブラッシュアップできるかもしれませんが、時間が限られているのでいろいろ進めていきます。
強制アンラップは非常に邪魔ですね。そもそも rawValue
からランクを作るのがしんどいところで、この強制アンラップが出てきてしまう部分になります。スーツと同じようにループを回したいけど、13個リストするのはつらいところがあるので、ここで役立つのが CaseIterable
です。このプロトコルに準拠させることで、ランクが全てのケースを取れるようになります。
イントコンマ、CaseIterable
に準拠させることで、ランクが rawValue
ではなくて素直にランクで取れるようになります。これでOKですね。同様の発想で、マーク自体も4つではなく5つになる可能性を考慮すると、フルデックを作るという発想で CaseIterable
を使うことで、設計が変わってもフルデックを作ってくれるコードが書けました。
もう少し手続き言語的になりすぎてる感を感じますので、場合によっては27行目から39行目までのフルデックの中の実装をもう少しリファクタリングすると読みやすいコードになりそうです。でも、いまはだいぶいい具合にできているので、時間もそろそろなのでここで終わりにしましょう。
具体的なスーツのすべて、ランクのすべて、それを組み合わせるようなコードになっているので、悪くないでしょう。もう少し色々遊んでみたい気もしますが、時間になったのでこれぐらいにします。
これで今日の勉強会は終わりにしようと思います。ありがとうございました。お疲れ様でした。