今日は 無所有参照
を 暗黙アンラップされるオプショナル
と併用する話のサンプルコードで クラス
を 構造体
に書き換える中で起こった、値型の循環定義みたいなところを眺めていってみます。前回ではそれが問題となる理由を整理できたので、今回は問題となったサンプルコードをどう構造体に変えていくことができるかを考えてみたいと思います。できるのかそれとも別の手立てを取るものなのか、やってみないとわからなそうですけれど、よろしくお願いしますね。
——————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #248
00:00 開始 00:22 値型の循環定義についてのおさらい 03:23 よくある表現に感じる不思議 06:45 値型の循環定義はよく起こるもの? 09:45 プロパティーラッパーを用いた打開案 12:56 プロパティーラッパーを使った構造体のサイズ 14:59 循環定義はできても、共有されてしまう問題 16:27 値の共有を期待するならクラスが素直 21:08 循環参照が発生している? 22:52 メモリーリーク自体は起こらなそう 24:00 プロパティーラッパーの解放を観察 28:20 代入時に値が共有される事例 31:30 共有を期待するならクラス型 33:21 共有すること自体が妥当であるかも重要 34:41 今回の場面では、プロパティーラッパーよりはクラスが良さそう 35:19 クロージングと今後の展望 ———————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #248
では始めましょう。今日はちょっとした雑談というわけではなく、前回の話を踏まえた続きになります。前回参加していた方なら、純換参照の話を少し触れたと思うので、それを思い出してください。
現在見ているのは純換参照の解消方法のサンプルです。このサンプル内では City
と Country
という型がクラスで定義されています。都市と国があって、都市は国に所属し、国は都市を持つという関係です。このとき City
は Country
を持ち、Country
は City
を持つという純換参照の例になります。
これを解消するために、まずクラスを構造体に変更してみました。struct
として定義し、オプショナルにしておきます。例えば次のようなコードです:
struct City {
var name: String
var country: Country?
}
struct Country {
var name: String
var city: City?
}
このようにすると、 City
が Country
を持ち、 Country
が City
を持つという循環構造ができるため、エラーが発生します。このとき、構造体は参照型ではないのに、純換参照が発生している問題が生じるのです。
この問題の原因は、純換参照があるとメモリ領域がどれだけ必要かが決まらないからです。厳密にはメモリが無限に必要となってしまうため、コンパイルができないのです。この詳細については前回のアーカイブを確認していただけるとよいでしょう。
構造体では純換参照を定義することができません。この問題が設計上の制約として理解できるでしょう。都市が国に属し、国が都市を持つという構成自体は自然なものです。しかし、この構造が必要とされるケースはあまり多くありません。そのため、問題にぶつかることも少ないでしょう。
まとめると、構造体を使った設計が一般的です。構造体を使用すると、言語によるさまざまな恩恵が受けられます。例えば、以下のような利点があります:
- スレッドセーフ:マルチスレッドにおいて予期しない変化を防ぐことができる。
- 安定性:安定性が高い設計が可能になる。
- パフォーマンス:参照型よりもパフォーマンスが向上するケースがある。
- シンプルさ:継承の概念がないため、人間の理解が及びやすい。
- コピーの自動化:値コピーが自動化され、意図しない干渉を防ぐことができる。
構造体はこのように多くの利点を持ち、特に Swift では基本的なデータ型の定義に広く使われています。純換参照に関しては、設計を見直すか、他の方法で解消する必要があります。 言語によってさまざまなメリットがあるわけですが、その点を考えると、構造体を使ったほうが良さそうだと感じます。また、「国」か「市」が「国」に所属するというデータ表現もおかしくないです。ただ、それでも問題が起こっているということが謎なので、解消していきたいと思います。
さて、この値型を構造体に絞るとしましょうか。構造体の循環定義が原因で問題が起こることは、何か心当たりがある人がいますか?昔、こんなことで問題が起こったなとか、最近起こったことがあるでしょうか。
たとえば、以前にマンガ用の新規交換リスクをアプリで作っていたとき、マンガというエンティティとコレクションというエンティティがありました。マンガはマンガの情報を持ち、コレクションはそれに関連するものになっています。マンガとコレクションの関係が一対一、あるいは一対多になっており、互いに参照しあう状態です。これが問題を引き起こす可能性があります。
コレクションの具体的な役割についてはまだ把握しきれていませんが、新職関連のデータとしてのマンガを読んでいるような状況です。これにより、一部のエンティティが他のエンティティを参照する必要が出ます。たとえば、マンガが特定のコレクションに関連付けられている場合、それを参照するための仕組みが必要でしょう。こういった構造では、循環参照が起こりやすくなるので、解決策を考える必要があります。
一方で、事前にいただいたコメントや意見も参考にして、今後の設計に反映していきたいと思います。その中で、プロパティラッパーに関する興味深い例がありました。プロパティラッパーで実際に動作するプロパティを見せるという発想で、そのことで新たな可能性が見えてきます。
具体的な例としては「トラップドバリュー」という概念があります。これはプロパティラッパーで内部的に操作されるプロパティを指しています。加えて、プロジェクティブバリューも持っています。プロジェクティブバリューは表から見える方が T
型になっており、裏側でボックスに格納された T
インスタンスの情報を外部に見せる役割を果たします。
このような設計により、循環参照の問題をある程度解決できる可能性があると考えられます。構造体内でのデータの持ち方や参照の仕方について再検討し、適切な方法を導入することで、より良いプログラム設計が可能になるでしょう。 値段の安全性についてですが、今見ているものとは違うかもしれません。大型的なものを使う方法もあると教えてもらったのですね。これについてもう少し詳しく見てみてもいいでしょうか。
トラップドバリューについてですが、これはプロパティラッパーで、水面下で操作するプロパティになっています。そして、プロジェクティブバリューも持っています。プロジェクティブバリューは、表から見える方は T
なんですが、裏側のボックスで扱っている T
も見せたくて、プロジェクティブバリューを用意しているという感じです。
さて、プロジェクティブバリューが無視できるかどうかについてですが、デバフでAquaticsが使っているということになると、Foo
のインスタンスは裏側でプラスのボックスとして保管されています。これについて調べるのも面白いですね。
プロパティラッパーを使うときに、ストラクトのサイズがどうなるかについても興味深いですね。既に知っている人もいるかもしれませんが、やってみましょう。多分、8バイトになると思いますが。
例えば、プロパティラッパーでストラクトを使ってみましょう。そして、ラッパーとして バー
で int
型を持つことにしましょう。具体的には、構造体で int
型を扱い、バリューを用意する形です。ここで押し進めて、プラスプラスやストラクトを使ってみます。具体的にコードで表すと次のようになります。
struct MyStruct {
@propertyWrapper
struct IntWrapper {
var wrappedValue: Int
}
@IntWrapper var value: Int
}
上記のコードでメモリーレイアウトを調べてみると、これで MyStruct
のサイズが16バイトになるのか確認してみます。また、フォルダ構造にすると8バイトになるか確認してみます。
さらに、前回の話にも関連しますが、データのサイズが先に決まることで、データの取り扱いが簡単になります。例えば、インスタンスを以下のように作成してみましょう。
let A = MyStruct()
let B = MyStruct()
A.value = 10
print(B.value) // ここで B の値がどうなるか確認してみます。
こうして、別のインスタンスに異なる値を設定することで、参照の動作を確認できます。クラスの方が適している場合も時にはありますが、状況に応じて使い分けることが重要です。
インダイレクトプロパティラッパーを使う例も見てみましょう。例えば以下のようにしてみます。
@IndirectionWrapper var indirectionValue: Int
初期値を設定しない場合や、参照をどのように持たせるかについても工夫が必要です。例えば、IndirectionWrapper
としてT型を用意し、その初期値を設定していない場合にはどうなるか試してみることも重要です。 プロパティラップはラップを使わなければいけませんよ。それでinit
が出てこないでしょうか?init
が見つかりましたね。素晴らしいです。ここではdrive
を使いますが、これでいいですね。エラーが出るとしたら出ませんか?ここまでやってエラーが出ないなら素晴らしいですね。
これで動かしてみましょうか。こうしてあげて、これでいいでしょうか。メンバーごとのinitializer
を使うのでいいのかな?とりあえず、こんな感じでやってみます。
まず、国を作ります。国を作ってくださいましたね。素晴らしいです。ここでキャピタルシティの名前を東京にしてくれるとすごいです。CapitalCity
のインスタンスをここで作りますが、まずはnil
にしておきましょう。初期化中なので、nil
にしておいて、 let capitalCity: City?
とします。
そして、手元で試してみたところ、Copilot
がいい感じに動きましたね。こんな風に東京、大阪、名古屋を入れてみましょう。これでできました。Japan
に所属するようにして、初期化が完了しましたね。
さて、この時点で循環参照が発生しているかもしれません。多分、していないですが、循環参照はカントリーがシティを持ち、そのシティがカントリーを持つケースです。ここで循環参照していますか?これはメモリリークが起こる場合がありますが、そうでない場合もあります。
実際に解除されるときに問題が起きる可能性があります。構造体なので、解消される可能性もありますね。
例えば、Country
をオプショナルにして、init
が必要です。ジャパンが東京を持って、東京がジャパンを持っている場合に、ジャパンが解放されるとき、構造体であれば解消されやすいです。
プロパティラッパーに関しては、自分が完全に把握できていない部分もありますね。構造体であれば、ラップする値が解放されると予想されます。ただ、確実にどうなるかは予測が難しい場合もありますね。
こうしたとき、ジャパンに入れてあげたときに問題が起こるかもしれません。シティの配列の部分でエラーが出ましたね。ジャパンをオプショナルにしたので、そのボタンで動かしてあげると、またエラーが出ますね。
シティの配列の方でエラーが発生しています。ジャパン、ありがとうございます。 そうですね。今度こそいけるかな?さて、出なかったっていうのはどうなんでしょう?ここをコメントアウトして実行してみましょうか。
さて、シティのインダイレクトってところですかね。なんだかシティが回復していない感じがしますね。だから、T
を全部消して、かつ二乗してあげて…。いや、待て待て、それも怖いかもしれません。何か爆弾を仕掛けている気がします。
実行すると、日本のリムーブオールとかをやることになるわけですね。だから、やばいですよね、このコード。実行してみて、うん、ダメですね。わかりませんでした。
プリントラップ・トゥ・バリューのラップ・トゥ・バリュー
は、T
にしかしていないからですね。今回、わかりやすくするために、アンドリュー限定にしましょう。アンドリュー限定にして、ラップ・トゥ・バリューのネームを押してあげて、これで動かすと…。
ここまでで、あ、そうか。ちょっと待ってください。プロパティラッパーだから、その放送台全部にあるんですね。別々のインスタンスが。インダイレクトでね。で、その中でグラスを持ってる。超恐怖じゃないですか?
だから、これらは別々のjapan
を持っていることになります。なので、まず循環参照の面は確かに解放されるっぽい感じがするのですが、これが覚醒が作られて持たれているわけですね。必要やりますか?
これでも、肝心のこの3つまでしか出なくて。でも、この3つが開放されたということはグラスが存在しなくなっているわけだから、行動台はフリーになっているはずですね。さっき教えてもらったとおり、メモリリークは起こっていないということは間違いないです。ただ、これがどうなのかというと…。
例えば、オプショナルももういいや、こうしてあげて。こうしたときにjapan
のネームがあるわけだけど、これとisInJapan
の0番目ですね。0番目のネームと0番目のカントリーのネーム。それから、1番目のカントリーのネームとやったときに、これが全部japan
になると。このときに、japan
のネームを例えばjapan群
みたいに変えるとします。
でも、なんかレッドになってしまってるね。今回はvar
にしたいです。こうしたときに、カントリーの名前が変わって、そのときに変わるのがjapan群
みたいな感じかな?
でも、こう考えると、もうちょっと回が変わってくるな。そうですね、japan
だけ国のところだけで、他は泉のやつが入っているからデータは保護されている。このときに、japan
のネームじゃなくてカントリー1のct1
のカントリーのネームを変えてあげると、動きがすると思います。ここが変わるか。そうね。
手前でlet x =
とあらかじめこのctInJapan1
をx
としておきます。複製を取っておいて、上で取っておいて、その後カントリーのネームを1番のネームを入れ替えてあげて、これでプリント。プリントじゃなくてct
のネーム、1番カントリーのネームってやると、これも変わるんですね。動かすと。
そうそう、なるほど。ここが微妙に値型なのに、ここの国だけが連動しているのが怖いと思ったんですが、これct
なわけですよ、両方とも。ct
は構造体なわけですよ。それなのにインダイレクトが付いているから、まあ当たり前ではあるけど、この国が共有されていて連動して変化するというのがなかなか怖いですね。 これだったら、わざわざインダイレクトにする必要はないかもしれません。都市が国を参照するという発想なら、参照型と捉えて国だけをプロパティラッパーにするのではなく、素直にクラスにすることが誤解がなくて良いかもしれません。アンダースコアを付ければ将来的には変わるかもしれませんが、現状では少し気持ち悪い感じがしますね。生徒型なのにクラスにするのもスッキリしない気がします。
国は参照型で共有できるようにしておけば良いかもしれません。これによって国の名前を変えても、プロパティラッパーよりクラスの方が対応しやすくなります。国名が変わったときにも対応できますし、メンバーワイズイニシャライザーが欲しいですね。ユニットテストを実行することで確認できます。本当にコンパイルが通るかどうか確認してみましょう。国の名前が変わったときに全てが更新されるかどうかも確認できます。これでうまく動けば、クラスの方が良いという結論になります。
今回のケースではクラスがうまく機能しましたが、それがいつも良い選択とは限りません。構造体のメンバーとしてクラスを使用する場合、そのクラスが参照型であることを認識する必要があります。プロパティラッパーよりもクラスの方がわかりやすくなることもあります。ただ、どちらも一長一短なので、状況に応じて選択する必要があります。他にも、別の型を用意してうまく表現する方法や、表のアドリベーション用のテーブルを作る方法などがあります。これも含めて、もう少し考えていきたいと思います。
今回はこれで一旦終わりにしましょう。時間になりましたので、本日はここまでにします。お疲れさまでした。ありがとうございました。