https://youtu.be/Duwh0OQDG-k
今回からは A Swift Tour
の 構造体
に入っていこうと思うのですけれど、その前に前回の 列挙型
の話の中で出てきた indirect
についてですとか、そこから派生して値型と構造体の扱いみたいなところについて簡単にながら最初に見ていこうかなと思ってます。どうぞよろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #59
00:00 開始 00:47 列挙型のおさらい 01:50 列挙型を再帰的に使う 03:00 indirect enum の利用例 04:35 indrect enum って実践で使う? 05:49 途中の値を変えられる? 09:53 indirect enum は参照として扱われる 10:42 談笑タイム 12:13 表向きには値型として振る舞う 14:02 indirect enum を途中から繋ぎ直すのは難しい 15:12 配列とリストの特徴 17:55 内部に参照を持つ値型 19:50 値型と参照型のメモリーサイズ 22:42 ポインターのサイズ 28:20 コンパイル時にサイズが決まる 31:19 値型は再帰的には使えない 35:56 値型を再帰的に扱う方法 37:37 存在型 40:49 間接的に再帰的な使用になる場面 44:28 オプショナル型のメモリーサイズ 49:21 ポインター表現を含む列挙型のサイズ 53:21 次回の展望 ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #59
今回はセクションとしては列挙型の話が一応前回で終わって、今回は構造体の話に入っていこうと思いましたが、前回最後の最後に「インダイレクト」というキーワードが出てきたので、せっかくなのでその辺りを補足しようかなと思います。若干脇にそれる感じで、今日は進めてみようかなと思います。
普通の列挙型は enum
で定義します。例えば enum SomeEnum { case a, case b }
のような列挙型を定義するとします。これが基本ですよね。この後、付属値(関連値)についても見ていきますが、例えば Int
型の値を持たせる場合、ここまではOKですよね。プレイグラウンドが動いているか確認しましょう。例えば、let value = SomeEnum.a(10)
のようにして値を入れてあげると、ちゃんと動きます。こうやっていろいろ使っていくわけですが、例えばこの中で自分自身の型を使いたいとき、普通の列挙型では表現できません。エラーで「インダイレクトを付けてください」と丁寧に指示があります。
インダイレクトをつけると、エラーに従って indirect
をつけてあげることで、コンパイルが通るようになります。たとえば indirect enum SomeEnum { case a(Int), case b(SomeEnum) }
のようにすると、自分自身の型を使って再帰的に列挙型を生成できます。これがインダイレクトの基本的な機能です。
インダイレクトが役立つ例としては、リストを作成する場合などがあります。例えば、enum List { case value(Int, List), case end }
のように定義し、let list = List.value(1, .value(2, .end))
のようにチェーンで繋いでいく表現です。このような再帰的な構造を持つ列挙型を作ることができます。
しかし、この indirect enum
を実際に使うことはあまりないです。自分は主に遊び目的で使ったりしますが、最終的にはコード全体のほうが楽だと感じることが多いです。昔使った記憶はあるのですが、ほとんど忘れてしまいましたね。
コメントでも「フリーコードを作った時に使った記憶がある」と寄せられていますが、自分もフリーする時に使ったことがあります。最終的にはコード全体になってしまうことが多いんですよね。
このインダイレクトを使った列挙型の欠点として、例えば AとBの間にCを入れるのが大変です。ポインターで持っておくとか多分できないですよね。階層を変更するのも結構大変です。頑張って取り出して置き換える必要がありますが、そのためにはパターンマッチングが必要です。パターンマッチングは条件文でしか使えないのがネックですね。 コードを書き換える際には、リスト型の要素に対して変なことをしていないか注意してチェックしてください。例えば if case
を使った場合、それ自体がエラーになることもあるかもしれません。まず、冗長な部分や意味のないコードは削除して、正確に let
や var
を使い分けることが必要です。
例えば、次のようなシンプルなコードを構成してみましょう:
if case let .red(value) = someEnum {
// この部分でvalueに対する処理を行います
print(value)
}
先ほどのコード例でも、変数 V
(今は v
としておきます) を使う際に、どこで初期化されてどのように使用されるかを明示的にすることが大切です。
以下のコード例では、列挙型が参照型として振る舞うかどうかを確認しながら、indirect
を使った場合のシンプルな動作確認が可能です。
enum LinkedList<Element> {
case end
indirect case node(Element, LinkedList<Element>)
}
let tail = LinkedList.node(2, .end)
let head = LinkedList.node(1, tail)
このコードの構造を理解するために、具体的な動作や変更を行う際は、以下のように変数 x
や y
の扱いを明確にし、参照型としての効果を観察します。
var list = LinkedList.node(1, .node(2, .end))
print(list)
// 値を書き換えようとする場合
switch list {
case let .node(value, next):
print("Value: \\(value)")
// 例:次のノードを書き換えるなど
list = .node(value, next)
case .end:
print("End of list")
}
また、インダイレクトの使用により参照型として振る舞うことがあるため、参照の変更が正しく行われるかどうかを確認することが重要です。これにより、列挙型内部のデータ構造をどのように保つかを理解しやすくなります。
実際の開発では、基礎を固めたうえでこれらの考え方を適用し、効率的に実装できるよう意識すると良いでしょう。特に、練習の場を利用して体に刷り込むように基礎を繰り返すことが重要です。このような基礎力を積み重ねていくことが、いずれ直感的にコーディングできる力を養うことにつながります。
最後に、コードが正しく動かない場合は、必ずエラーメッセージを確認し、何が問題なのかを詳細に検討することも大切です。万全を期すために、こまめにテストを行い、動作を確かめながら進めていきましょう。 なので、より素早く値を取るためには、値を標準または降順で並べておいて、2分探索でより早く取るなど、工夫が必要です。配列も同様で、内部の値に注目して目的のものを探す場合は、似たようなことをします。例えば、5番目を取得する場合、リストは5個分たどらないといけないですが、配列のようなランダムアクセスができるものについては、インデックス(例えば4番目)で直接アクセスできます。
リスト自体は現在定義していますね。リストは値型で、内部は参照を持っているという話をしていました。配列も同じですね。配列も構造体で定義されていますが、内部でバッファを持っています。昔は ArrayBuffer
という型が表で見えるようになっていましたが、今は見えなくなっています。しかし、内部には存在しており、これはクラス型で定義されています。Array
型はこのように作られています。
インダイレクトな列挙型も同じような感覚です。そうすると、配列は良いとして、次は Playground が動いたか確認します。さて、サイズの話をしましょう。前回最後にお話したサイズの計算についてです。
例えば、構造体があって、何らかのデータを持っているとしましょう。例えば、var value1: Int
と var value2: Int
のように定義します。そして、このサイズを取得するために、次のように書きます。
struct MyData {
var value1: Int
var value2: Int
}
let myData = MyData(value1: 1, value2: 2)
print(MemoryLayout.size(ofValue: myData))
このコードを実行すると、16というサイズが出力されます。構造体の場合、その変数自体がメモリ空間を丸ごと取ります。つまり、インスタンス自体がメモリを占有します。
一方、クラスにすると次のようになります。
class MyDataClass {
var value1: Int
var value2: Int
init(value1: Int, value2: Int) {
self.value1 = value1
self.value2 = value2
}
}
let myDataClass = MyDataClass(value1: 1, value2: 2)
print(MemoryLayout.size(ofValue: myDataClass))
このコードを実行すると、8というサイズが出力されます。この8バイトはポインタのサイズです。
例えば、UnsafeRawPointer
のサイズも8バイトです。
print(MemoryLayout<UnsafeRawPointer>.size)
これが64ビットCPUの場合のため、8バイトになります。32ビットCPUでは、ポインタのサイズは4バイトでした。
このように、構造体の場合は直接そのサイズ、クラスの場合はポインタのサイズが関係します。クラスのインスタンス変数のサイズは、ポインタが指す先で管理されます。
列挙型も同様な特性があります。普通の列挙型は、すべての列挙子の付属値を表現できるメモリサイズが確保されますが、インダイレクトな列挙型 (indirect enum
) ではサイズが8バイト(ポインタのサイズ)になります。
indirect enum LinkedList {
case value(Int, LinkedList)
case end
}
print(MemoryLayout<LinkedList>.size) // 8
インダイレクトをやめると、サイズが16になります。
enum LinkedList {
case value(Int, LinkedList)
case end
}
print(MemoryLayout<LinkedList>.size) // 16
以上のように、値型と参照型、構造体とクラス、普通の列挙型とインダイレクトな列挙型のメモリ使用の違いについて説明しました。 とりあえず、普通のenum
だと値型としてself
自体が内部の値を表現可能なものにしますが、indirect
にするとself
はポインターになるような動きになります。この動きが、indirect
の指定により少し変わってきます。しかし、普通は値型として使って何の問題もなく動くように作られているので、使う際に特に気にする必要はありません。
では、なぜこのindirect enum
と普通のenum
を区別する必要があるのかについて、少しお話ししたいと思います。プレイグラウンドが動けばいいのですが、動かない場合は仕方ありませんね。
indirect enum
の背景には、コンパイル時の都合が関係しています。構造体を定義するときには、構造体全体のサイズが決まらないといけません。例えば、以下のようなコードがあります。
struct MyData {
var v: Int
var w: String
var x: Double
}
このとき、コンパイル時にはこのMyData
の型のサイズを決める必要があります。Int
は8バイト、String
は16バイト、Double
は8バイトです。これにより、最終的にサイズが32バイトに決まってコンパイルができるようになります。
しかし、以下のように自分自身を含む場合はどうでしょうか。
struct MyData {
var v: Int
var w: String
var x: Double
var y: MyData
}
このとき、MyData
のサイズを決めるためにMyData
を使う必要があり、循環参照が発生してサイズが決められなくなってしまいます。エラーとしては「リカーシブに最終的に参照する」というものになります。この問題を解決するためには、class
を使う必要があります。
クラスの場合、例えば以下のようにすると、
class MyData {
var v: Int = 0
var w: String = ""
var x: Double = 0.0
var y: MyData?
}
クラスは参照型のため、ポインタのサイズ(通常8バイト)で済みます。しかし、イニシャライザがない場合、無限ループが発生することがあります。self
の初期化時に自分自身を含めると、無限ループに陥る可能性があるのです。例えば、
let i = MyData()
i.y = MyData()
このようにすると、初期化時に無限ループが発生します。結果的に、コンパイルは通りますが、実行時に無限ループになります。
こういったことから、自分自身を最適的に使う場合には、値型ではなく参照型を使用するのが適切です。そのため、普通のenum
とindirect enum
が区別されます。Swiftでは明示的にindirect
をつける仕様を採用しています。
構造体を再帰的に使う方法は基本的にないため、再帰的な構造を作成する際にはクラスを使用するのが一般的です。このような問題に直面することはあまりないかもしれませんが、知っておくと便利な知識です。 クラスの場合ですが、ビューコントローラーなどが典型的な例ですね。ただ、自分自身を規定しないか、デリゲートでself
を代入するといったことがよくあります。しかし、構造体ではあまり見られません。
ただし、ラップしてしまえば特に問題はありません。例えば、CustomStringConvertible
を指定して、description
プロパティを持たせればいいのです。このプロパティがCustomStringConvertible
型だということがコンパイルで通るのです。このようにラップすることでCustomStringConvertible
型というプロトコルの「存在型」として扱えるのです。存在型のサイズが決まっているため、それを扱うためのメモリ空間も確保できるのです。これで問題なくコンパイルが通ります。
存在型という話が出ましたが、存在型は40バイトのサイズを持ちます。ですので、迂闊にプロトコルの存在型を多用すると、メモリを多く消費する可能性があります。ただ、現代のiOSアプリなどでは大きな問題にはなりませんが、それでも知っておくべきことです。
型を指定する際には、その型のサイズが事前に決まっている必要があります。参照型では、型自体のサイズはポインタのサイズとしてシンプルに決まります。しかし、存在型やボクシングといった概念の場合は40バイトなどのサイズで決まっているため、問題ないのです。
待機的な型でなければサイズは自動的に決まりますが、そうでない場合はエラーとなることがあります。例えば、構造体の中で変数myValue
を規定していて、その中でバーを使ってmyData
を使うとします。この時、待機的にmyValue
が決まらない場合、エラーとなります。これは、循環的に値が定義され、サイズを事前に決められないからです。
さらに、インダイレクトイーナムの例を挙げて説明します。例えば、enum MyEnum
があって、その中にケースとしてmyA
があり、myData
を持っているとします。この場合、待機的な定義でエラーが出ることがありますが、indirect
を付けることでコンパイルが通ります。これは、サイズが固定されるからです。このように、リカーシブな定義に対してもindirect
を付けることで解決できるのです。
他にもコメントで「オプショナルにしないと無限になる予感」という指摘がありますが、確かにオプショナル型にすることで問題を回避できることがあります。ただ、オプショナル型も特有の動きをするので注意が必要です。 サイズ感がね、なるほどね。せっかくだからやってみます。マイデータのサイズがあるでしょ?オプショナルってね、ちょっと単なるラップじゃないんですよね。これのサイズをまずちょっと見てみますけど......40もあったっけ?えーと、16、40、えーと......こんなにあるか。86、86、あーあるか。32、あー8、あったあった。もう40バイト使ってるのね。あの、マイイーナム、これがインダイレクトイーナムだから8ね。で、これで40だ。
で、あって、これをオプショナルにすると41になるのかな。あ、40のまま。なんか記憶と違った。40でインダイレクト?なんか勘違いをしているかな。サイズが変わらなかったらオプショナル表現できないですよね。んーと、サイズが86?マイイーナムは、マイイーナムのサイズが違うのかな。んーと、マイイーナムのサイズも見てみましょう。8だよね。算数間違ってるのかな?えーと、ストリング16、8、16、30、40、多分合ってる。ちょっと算数苦手なので時々間違うんですけど、多分合ってる。24、30、40、基本合ってるよね。
えーと、そうなんだ。ちょっとこれ分かんない。後で調べてみよう。分かんないってね、なんかどうにもならないな。イント型、イント型とイントオプショナルだと8と9でしょ。そうですよね、うん。そう、こうやってね、オプショナルかどうかっていうのは要はね、列挙型の.some
と.none
ですね。で、これのね、サムの場合はイントっていうことでね、列挙型が組まれるわけなんですけど......あれ?なんでだろう?分かる人います?8ケースサムでマイ......あ、間違えてるね、マイイーナム。これ、たぶん間違えたね、あれ、間違えた。なんか自信がなくなってきた。もう一回やってみよう。
えーと、マイイーナム、マイデータ、まあいいや、マイデータ。あ、でもやっぱ40だ。後で書き換えたっけ?まあいいや、どうでもいい。うん、なんで40なのこれ?マイデータでしょ。で、これでケース.noneと持たせたときのメモリレイアウトは......えーと、メモリレイアウト、メモリレイアウト、Eのサイズ、これ40になるの?えーと、あ、ケース、構文間違えた。えーと、こうか。あ、40になるんだ。あ、とか、あ、なるか、なるね、うん。うんうんうん、あ、とか。あ、じゃあなんでイントは8と9になるんだ?
えーと、コメントは......えーと、ニルケース、0ポインター?ポインターか。ポインターとして扱うのニルの場合?たぶんコンパイラーがゼロ指定するんで、ゼロ指定されたときがニルという証言になるから、メモリレイアウトがこの場合変わらない。イントはゼロ値があるので、あ、なるほど。たぶん最適化の結果ですね。あ、なるほど。一応ね、今マイデータも構造体で作ってるから、全部ゼロがありそうだけど、ここがポインターだからか。なるほど。ここが仮にイント型に変えると40と41になるんですかね?あ、なかった、あれ。
このタイプは好きですね。ただニルの場合はゼロポインターが与えられるんで、ポイントサイズとしては変わらないです。メモリサイズは。あ、ほんと?こうだと、変わってくるんだ。あ、なった、なった、だいたい分かった。ストリング型もゼロポインター的な表現がきっとあるんですね。最適化の都合で、内部でポインター管理してて。あ、なるほど。よく分かりました、ありがとうございます。なるほどね、そう、ポインターというか、Swiftって時折こういう最適化してくれるの面白いですね。なんかSwiftに任せておけば安心みたいな、そんな感じが感じられるところ。
なるほどね、だから構造体でも一箇所もポインター表現がない場合は、ポインター表現は言い過ぎですね、ゼロ表現が存在しない時にはオプショナルにしてもサイズが変わらないんだ。これがたとえば、アンセイフローポインターでも、これだけだとたぶん40と41でしょ。48、48、48か。ローポインターはニル取っちゃうのかな。アンセイフポインターはイント型の......アンセイフポインターだと48と?あ、両方48か。まだ理解しきれてないや。ニルポインター、こうすると変わるのかな。48と49か、あ、そっか、そっか、そっか。ああ、はいはい、分かった、分かった。
オプショナルがない時にはニルポインター表現がないから、全部がゼロの時にはニル扱いができるよ。でもニルを取れるポインターになっちゃうと、ゼロポインター表現を中に含められるようになっちゃうから、それをオプショナルにした時には追加でニルっていう表現が必要になるんだ。なるほど、よく分かった。ありがとうございます、なるほど、なるほど。
他にもこの前出てきたお話として、Eのサイズ、列挙型の列挙子が1個だけのときにはゼロバイトになるとか、すごく面白い最適化ね。はい、じゃあいい時間になりましたので、今日は参照型と値型の内部的な動きみたいなお話をする感じで終わりましたけれど、次回はいよいよ構造体の特徴みたいなところについて話していこうと思います。はい、それでは今日はこれで終わりにしますね。お疲れ様でした、ありがとうございました。