Page icon

第249回

2023/05/15
Empty
番外編
Empty
Empty
Empty
Empty
Empty
Empty
12 more properties
今日も引き続き 
無所有参照
 を 
暗黙アンラップされるオプショナル
 と併用する話のサンプルコードで 
クラス
 を 
構造体
 に書き換える中で起こった、値型の循環定義みたいなところを眺めていってみます。前回に教えてもらった 
プロパティーラッパー
 で循環定義を実現する方法で興味深く思えたところを紹介しつつ、他にも別の回避する手立てで心当たりがありそうなので、そんなあたりに着目しながら見ていく回にしてみますね。よろしくお願いします。
————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #249
00:00 開始 00:14 値型の循環定義の問題 01:20 間接的列挙型に着想したプロパティーラッパーのアイデア 04:27 参照されている様子を眺める 06:46 共有されるのを把握しにくい 08:21 間接列挙型を使ったプロパティーラッパーのアイデア 10:16 間接列挙型も値型 10:51 値型として独立しながら循環定義を回避 14:02 問題解決の手法としては完璧、ただし課題感 15:41 実際問題、どう使っていけるのか 19:10 循環定義をプロパティーラッパーで回避することのおさらい 20:30 悪くないといえば、悪くない 21:02 いずれにしても設計なり扱いなりで工夫が必要 22:44 イニシャライザーで工夫してみる 25:49 循環定義ではなく片側に所属させてみる 29:56 都市自体に首都かどうかを持たせたい 34:17 構造体は一致性が明瞭 35:18 クロージング —————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #249
さて、始めていきますね。ここでは引き続き前回の内容を扱いますが、具体的には前回取り上げた「所有権のない参照」の例についてです。この中で出てきたサンプルコードがあります。このコードではデータを表現する方法として、Swift の場合、通常は構造体で表現します。しかし、前回のサンプルコードではクラスで用意した「City」と「Country」を構造体に直そうとしました。
この際、両方を構造体にしてしまうと、都市が国を持ち、国が都市を持ちといった再帰的な構造になってしまい、構造体での定義が適用できなくなります。しかし、この構造を表現できないと不自然な感じがしました。
そこで、列挙型と
indirect
キーワードを使う方法を前回紹介しました。これにより、列挙型を参照型のように扱うことができ、メモリーサイズが確定するため再帰的な定義が可能になります。構造体ではこれができませんでした。
そして、今回紹介する方法として「プロパティラッパー」を使う方法があります。プロパティラッパーを使って
indirect
をクラスで定義することにより、プロパティをクラスとして持つことができるようになります。実質的には参照型として扱うことができます。
このようにすることで、例えば次のような形で
City
Country
の間の関係を定義することができます:
@propertyWrapper class Indirect<T> { var wrappedValue: T init(wrappedValue: T) { self.wrappedValue = wrappedValue } }
これによって、全ての
City
Country
の関係をストラクトとして保ちながらも参照型のように扱うことができます。
例えば、次のように定義します:
struct City { var name: String @Indirect var country: Country } struct Country { var name: String @Indirect var capital: City }
こうすることで、参照型として取り扱うことができ、相互に依存する関係を定義できます。少々複雑な話になりますが、このような形でデータ構造を定義することが可能になります。
実際にこのコードを動かしてみると、出力結果を確認できます。例えば、次のようなコードがあります:
var japan = Country(name: "Japan", capital: City(name: "Tokyo", country: nil))
ここで
Country
City
が相互に参照し合う形で定義されています。これで
japan.capital.name
city.country.name
を表示すると、各々の都市と国の名前を取得することができます。
以上のような形で、Swift の言語仕様を利用してデータ構造の定義を進めることができます。少々難しい箇所もありますが、この方法により参照型のように扱うことができることを理解していただけたかと思います。 けれど、そういうときの値型の場合は基本的に複製されるという約束があります。そのため、ここが構造体で定義されていて、メンバーも構造体であるにも関わらず連動してしまうのが少し怖いと感じるところです。プロパティラッパーを使う方法は、名前をしっかりしていれば分かるといえば分かるのですが、構造体同士で一部共有しているところがあるのは、やはり並行処理とかをする際に怖いですよね。
もう1つの方法として教えてもらったのが、プロパティラッパーを使用する方法です。これを使うと、もう少し面白い変化が得られそうな気がするので、まずはこれを試してみましょう。利用者としては単純な値型の場合、構成する内部の状態によってメモリサイズが変わってしまうため、循環定義するとメモリサイズが確定できない都合でエラーになることがあります。ただし、インダイレクトケースを使うことで、参照型として扱うことができ、循環定義を可能にすることができます。
値型の利点として、基本的に代入時に複製されるという特性があります。その上で、インダイレクトケースを使うことで参照型のようなメモリの持ち方ができるため、独立性を保つことができます。つまり、片方を書き換えたとしても、値型を使うことで代入された方だけが変更されるようになります。
具体的には、インダイレクトケースを設定し、以下のようにすると良いです。
enum IndirectValue<Wrapped> { indirect case value(Wrapped) }
また、プロパティラッパーを使用して、以下のように内部の値を管理します。
@propertyWrapper struct Indirect<Value> { private var storage: IndirectValue<Value> var wrappedValue: Value { get { switch storage { case .value(let value): return value } } set { storage = .value(newValue) } } init(wrappedValue: Value) { self.storage = .value(wrappedValue) } }
これを利用することで、独立して変更されることを確認することができます。実際に使ってみることで、参照同士が連動しないことを確かめることができました。
この方法で、プロパティラッパーを使って循環定義を避けるという手法を取り入れると、コードがスッキリしました。このアプローチで問題を解決することができ、違和感を感じることなく動作させることができるでしょう。皆さんもぜひ試してみてください。 積極的に使っていいのかどうか、悩むところですよね。悪くはなさそうですが、何か使いたくない感じもします。インダイレクトケースとして見れば、循環でプロパティを持つ場合の設計が間違っているのではないかとも思われます。確かに、このスッキリしない感じは設計の問題から来ているかもしれません。
例えば、ヘルムではバックリンクというものがあります。これはコンピューターの直接的な話ではないのですが、国際関係のようなもので、たとえば都市が所属する国がないといった場合にバックリンクを利用することがあります。実際には、初期化の問題が気になりますね。初期化がうまくいかないと、国や都市をきちんと作れない可能性があります。
初期化のタイミングやライフサイクルをうまく調整して、一時的に不安定な状態ができても問題にならないようにすることが重要です。実践においては、値があるのだという主張をしておけば、ランタイムで落ちるリスクが多少減るでしょう。これを徹底していない状況が不正な状態だということです。
@indirect
キーワードを使うのは一時的に参照型にするためです。これは以前はクラスで対処していましたが、今回は関節列挙型を使うことで、一時的にポインターとして扱うことができます。クラスだと共有になりますが、
indirect
ケースを使うことで独立した参照が可能になります。
このアプローチにより、実際に同じ国を持つものの片方だけが変わると、その変化が期待通りに反映されるようになりました。ただ、インダイレクトケースを利用するのには少し抵抗があるかもしれません。でも、基盤を整えて一気に配置するような構造を作れば問題が解決することもあるでしょう。
最終的には国と都市の関係として、国に従属する仕様である以上、その設計を根拠に進めることで最適な解決方法が見つかると思います。 確かに、持っていっちゃってもいいかもしれないですね。やってみますか。この例だとカントリーがシティだから、それがうまくいきそうです。見ながら独立したい場合は、カントリーがシティを持つパターンにするといいかもしれません。
このステップを進める前に、この程度の定義であれば、国を定義するときにイニシャライザーで名前だけを受け取るようにしてみます。名前
name
を受け取って、それを文字列の配列として保持します。そして、例えば
capitalCity
otherCityNames
といったプロパティを設定します。
struct Country { let name: String var capitalCity: String var otherCities: [String] init(name: String, capitalCity: String, otherCities: [String]) { self.name = name self.capitalCity = capitalCity self.otherCities = otherCities } }
このようにイニシャライザーの中で適切にプロパティを初期化すれば、不安定な状態を防ぐことができます。リモートなデータであれば、どこかで非同期に取り込む必要があるかもしれませんが、配列で持つことで安定性を保つことができます。
また、シティとカントリーの関係性を解決するために、配列を活用し、セルフで解決する方法も考えられます。たとえば、配列を用いてシティを初期化し、シティのカントリーを
self
で解決します。ただし、受動させる必要があります。
struct City { let name: String weak var country: Country? init(name: String, country: Country?) { self.name = name self.country = country } }
この場合、どうしてもビックリマーク(強制アンラップ)は避けられないかもしれません。イニシャライザーで
name
country
をオプショナルでない形で定義し、プライベートイニシャライザーを提供することで、外部から不安定な状態にされることを避けます。
class Country { let name: String var capitalCity: City var otherCities: [City] init(name: String, capitalCity: City, otherCities: [City]) { self.name = name self.capitalCity = capitalCity self.otherCities = otherCities } } class City { let name: String weak var country: Country? init(name: String, country: Country) { self.name = name self.country = country } }
この設計により、イニシャライザーの中でシティとカントリーの関係が明確になり、不安定な状態を避けることができます。配列を使用して他の都市を管理する方法もあるため、シティがどのカントリーに所属しているかを知るためのアクセサリーメソッドを提供することも可能です。
extension Country { func contains(city: City) -> Bool { return city == capitalCity || otherCities.contains(where: { $0.name == city.name }) } }
このようにすることで、シティがどのカントリーに所属しているか確認することができ、シティのリストをグローバルに管理することも可能です。かように、プログラム設計時には、データの関係性をしっかりと定義し、安定した初期化を行うことが重要です。 今回は、都市のランキングやフラグ管理の話題でしたね。都市(シティ)を1位にするためのデータ管理方法について議論が行われました。以下は、具体的な方法についての説明です。
ランキングを管理するための方法の一つとして、都市にフラグを持たせることが挙げられました。これはフラグをうまく管理することで実現できます。例えば、都市が「キャピタルシティ(首都)」かどうかを示すフラグを設定します。
var isCapitalCity: Bool = false
このフラグを利用して、新しい値が追加されたときに、既にキャピタルシティが存在するかチェックする方法も示されました。例えば、次のようなコードでチェックできます。
if newCity.isCapitalCity { let count = cities.filter { $0.isCapitalCity }.count guard count == 1 else { throw Error.moreThanOneCapitalCity } }
さらに、別のアプローチとして、キャピタルシティを一つの変数で保持し、そこから判定する方法もあります。
var capitalCity: City?
これにより、都市がキャピタルシティかどうかをチェックするコードは次のようになります。
if let currentCapital = capitalCity, currentCapital == newCity { // 新しい都市がキャピタルシティと一致するかを判定 }
これらの方法について、他のメンバーと議論しながら最適な方法を探っていきました。繰り返しになりますが、重要なのは一つの都市がキャピタルシティになるように万全を期すことです。
最終的に、このようなコードを使うことによって、一貫した判定を行い、都市のランキング管理をする方法が示されました。状況によってはこの方法が適しているでしょうし、キャピタルシティとその他の都市を区別するためには悪くないアプローチだと感じました。
今日はここまでとなります。次回はさらに詳細な内容を話し合う予定です。また循環作業に戻って、再び議論を深めていきましょう。お疲れさまでした。ありがとうございました。