さて本日は前回に 無所有参照
を 暗黙アンラップされるオプショナル
と併用する話の中で出てきたサンプルコードを見ていて気になった、循環参照を伴うインスタンスの生成まわりについてを細かく眺めてみる回にしますね。サンプルコードみたいな感じで問題ないのか、それともより適切な方法があるのか、そんな観点で眺めてみます。よろしくお願いしますね。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #245
00:00 開始 00:28 今回の展望 00:53 これまでのおさらい 04:29 同じ都市を扱いたいとき 05:21 所属先を保証する初期化 06:04 設計者の示す初期化意図 08:09 同じ値を表すことの困難さ 10:28 首都の東京と、都市の東京 11:56 クラスにおける一致判定 13:00 switch による厳密透過性判定は行われない 13:48 クラスで値表現をしようとするなら? 17:08 どちらが先に立つかを考えてみる 21:32 現実に即する必要はない 22:12 Equatable と Hashable との関係性 23:47 ハッシュ値が欲しいだけで、同値性が不要な場合は? 26:44 考え方で作りも大きく変わってくる 30:10 クロージングと次回の展望 ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #245
では、続きを進めていきますね。
今日は循環参照を解消するための話をしていきます。これまでも無意識参照や循環参照について話してきましたが、今回はその解消のための具体的な方法を掘り下げていきます。前回の勉強会で出てきたサンプルコードを振り返りながら、皆さんの知識を借りて、本当にこのコードでいいのかを確認していきたいと思います。
まずおさらいします。今回のテーマは循環参照です。具体例として、2つのクラス「Country」と「City」があり、以下の条件を満たすように定義しています。
- 全ての国(Country)が必ず都市(City)を持たなければならない。
- 全ての都市(City)が必ず国(Country)に所属しなければならない。
この2つの要件を満たすためのクラス設計について説明します。
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalCityName: String) {
self.name = name
self.capitalCity = City(name: capitalCityName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
「Country」クラスは都市を必ず持たなければなりません。ここで初期化の都合上、一時的にnil
を許す必要があり、変数capitalCity
をオプショナルにしています。ただし、このオプショナルは初期化中に一時的にnil
になるだけで、実際にはnil
でないことを保証するためにアンラップしています。
一方、「City」クラスは国に必ず所属している必要があるため、オプショナルではなく、初期化の時点で必ず国の情報が入るようになっています。
この設計では循環参照が発生する可能性があります。そこで、循環参照を断ち切るためにunowned
キーワードを使っています。アンオウンド参照を使うことで、循環参照を避け、メモリリークを防ぐことができます。
このように、各クラスが持つプロパティの初期化を適切に行うことで、循環参照を防ぎ、安定したクラス設計が実現できます。
次に、これが本当に適切な初期化方法なのかを確認しましょう。今紹介したコードでは、明示的にnil
代入をしていますが、Swiftのオプショナル型の初期値はnil
になるため、ここで書かなくても問題ありません。ただし、安全のために記述しました。
また、この設計のメリットとして、国を定義する時に都市が必ず国に所属していることを約束できる初期化になっています。ただし、このイニシャライザー以外では初期化方法がなくなり、クラスの設計者が初期化を完璧に行うためにこのイニシャライザーを使用するよう強制する形になっています。
このような設計であれば、初期化の手段が限定されることでクラスの安定性が高まります。ただし、プロパティがオープンになっている部分など、まだ検討の余地があるかもしれません。クラス内部でインスタンスを生成することで、これが本当に最善なのか再検討も必要です。
以上の設計を踏まえつつも、実際に運用していく中で、何か問題が出た場合はその都度修正を施していくことが重要です。今日はこの辺りで一旦区切りとし、次回はさらに具体的な実例や応用についても見ていきたいと思います。 イミュータブルクラスについてですね。これには様々な状況がありますが、一旦それについては置いておきましょう。では、実際に使ってみましょう。まず、Country
クラスを使用してみます。
let country = Country("日本")
let capitalCityName = "東京"
これで初期化できましたね。ここまではオッケーです。次に、国が一つ出来上がりましたね。さて、次は首都ではなくて、例えば都市(City
)を初期化してみましょう。
let city = City("横浜", country: country)
こうすると日本の都市が初期化できます。このとき、Country
インスタンスが必要となるので、日本をセットする必要があります。
投票機能について考えると、次のようなコードになります。
var citiesInJapan = [City]()
if city.name == "横浜" {
citiesInJapan.append(city)
}
これは横浜が見つかったときの処理です。ループは1回しか入らないので、if
文の方が適切ですね。例えば:
if city.name == "横浜" {
// 何かの処理
}
クラスを使うとき、インスタンスの一致性判定が必要な場合があります。クラスを使う場合、以下のようなコードで一致性を確認できます。
if city === anotherCity {
// 何かの処理
}
City
クラスのname
プロパティで判定する場合もありますが、スイッチ文での判定はやや複雑になります。例えば:
switch city.name {
case "東京":
// 処理
case "横浜":
// 処理
default:
break
}
クラスのインスタンスが異なる場合もありますが、状態を表現するために同じ型の異なるインスタンスを使うことは一般的ではないかもしれません。ただし、初期化について考える場合、例えばCountry
の初期化フェーズではcapitalCity
を含めて考えますが、インスタンスのライフサイクルの中で自由にセットする必要がある場合もあります。
ですから、初めに都市を作り、後ほどその都市を追加することもあります。こういう状況では、暗黙的にアンラップされるオプショナル型を使うことで、初期化フェーズ中に値をセットする必要がなくなります。ライフサイクルが広い場合でも、以下のように初期化できるかもしれません。
class Country {
var capitalCity: City! // 暗黙のアンラップオプショナル
init(name: String) {
// 国の初期化
}
}
もちろん、これを使うかどうかはケースによりますが、このような方法も考えられます。クラストの一致性判定や初期化のフェーズに関しては、プロジェクトの要求や開発の方針に従うことが重要です。 このインターフェースだと、コードが汚くなりますね。例えば、Tokyo
を初期化してから、Japan
の首都にTokyo
を設定するなどの方法では。こういうコードも気持ち悪いので、あまりお勧めできません。しかし、雰囲気だけ掴んでもらえればと思います。国と都市を揃えてから首都を設定する方法もあるわけです。このようにすると、名前で比較するのではなく、ちゃんとインスタンスとして独立した都市(City
)として比較が可能になります。この方法だと無駄なメモリも消費せず、効率的です。
ただし、このCountry
を少し改良する必要があります。こういった方法で初期化するのはどうなのでしょうか。もしかしたら、もっと良いやり方があるのではないかとも感じますし、繰り返し使うかもしれない方法です。極端な話、国が滅んでも都市はまだ存在するのではないでしょうか。そもそも、Country
をオプショナルにして、CapitalCity
は必須にすると、国は必ずしも存在しないが都市は存在するという状況を再現しやすくなります。
ただし、両方をオプショナルにするのは少しやりづらくなるかもしれません。その場合、City
をどこに固定するかがポイントになります。日本が滅んだとしても、その瞬間には東京はまだ存在します。この考え方も一つの意見ですね。実際に、日本が大日本帝国から現代の日本国になったとき、二つの日本は同じ日本だとみなすことができます。
シティというものは、もともと人が集まってできるものです。政府が滅んでも、シティは人が存在する限り存続します。仮に日本が滅んでも、東京という都市に住んでいる人々はまだ存在しますので、東京としても生き残るというわけです。
また、都市が独立した場合には、そのインスタンスを振り返るためのコストがかかっても問題ないと思います。ただ、API設計において、何でもできることが必ずしも良いとは限りません。そのため、この辺りはバランスを取っていく必要があります。
個人的に好みの話ですが、Equatable
とHashable
についても触れておきます。ハッシュが取れれば一致比較できるわけではありません。例えば、ある文字列のハッシュと別の文字列のハッシュが一致するからといって、その文字列自体が一致するわけではないというのは有名な話ですね。 なので、Hashable
は一貫性とは関係ないはずなんですけど、このHashable
の定義をたどると、なぜかEquatable
を求めているんですよね。個人的にはこれが好きなんです。つまり、Hashable
の定義をたどると、Equatable
が必要になってくるんですよね。進めていく上で、Hashable
がEquatable
に所属しているというのは、正しいと言っていいのか分かりませんが、そういう解釈もあるということです。
プログラム的には、ハッシュと一貫性を両方求めることが多いんですよね。Hashable
だけどEquatable
じゃなかったとき、その値が役立たないわけではないですが、考えると難しい話です。要するに、ハッシュ値が欲しい場合がありますが、比較はしないといけないという時に、わざわざEquatable
を載せるのは辛いこともあると思います。
ハッシュ値が取れると、そのハッシュ値を例えば改ざん防止に使うことがあります。メールの本文が書き換えられていないことを証明するデジタル証明では、メール本文からハッシュを取って、そのハッシュが同じであれば、メールが改ざんされていないとみなされます。メール本文がHashable
だったとしても、Equatable
である必要は、ハッシュ値が同じならEquatable
とも言えるかもしれませんね。
ただ、メールの宛先内容の全てが一致するプロパティを作ると、添付ファイルがたくさんあるときに一致判定が重くなることがあります。そのような場合はメールのIDで一致判定をすれば良いかもしれません。このように、Hashable
がEquatable
を要求する状況は難しいですね。使う状況によって適切な判断が求められます。
また、オプショナルな値を使う場合もあります。その場合、インターフェースが役立たなくなることもあります。具体例として、キャピタルネームの初期化がありますが、キャピタルシティとしてシティを受け取るイニシャライザーを増やす方法もあります。例えば、以下のように書けます:
self.name = name
self.capitalCity = city
これをすることで、ダミーの値を用意する必要がなくなります。しかし、これもまた一つの厄介な問題を引き起こすことがあります。 capitalCity
として東京を入れたり、他のシティを入れたりする場合の問題も考慮する必要があります。このようなインターフェースは破綻する可能性もあります。
次回はこのようなクラスを構造体(struct)に変更して全体を整える試みに挑戦しようと思います。今日は時間になりましたので、これで終わりにします。お疲れさまでした。ありがとうございました。