本日も引き続き、 強参照循環
を解消する手段のうちのもうひとつの無所有参照
まわりの挙動を具体的に眺めていきます。今回はそんな 無所有参照
を オプショナル
で扱ったときの様子を眺めていく予定。なんだかんだでこれまでの話の中でも触れた話題になりそうですけれど、復習も兼ねてじっくり眺めていきましょう。どうぞよろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #238
00:00 開始 00:35 前回のおさらい 01:10 生存範囲を意識して安全に使う 04:20 無所有参照を安全に扱うのは高度かもしれない? 05:08 無所有参照の安全性を担保するには? 06:07 イニシャライザーの隠蔽による安全性は限定的 08:56 private init と extension の併用 10:35 顧客自体がクレジットカードを作ること違和感 12:21 継承を禁止にするのも安全性向上のアイデア 14:59 private と fileprivate 15:55 C++ でいう protected は存在しない 17:03 extension における private の特例 19:54 独特なアクセスコントロール 22:16 C++ のフレンドクラスとフレンド関数 24:58 アクセス修飾子によるバイナリーの最適化 27:16 unowned と weak の違い 31:06 無所有参照は let でも扱える 32:01 クロージングと次回の展望 ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #238
今日は、循環参照に関する問題とその解決方法についてお話しします。前回からの続きなので、改めて前回の内容を復習しつつ進めます。
前回紹介した例では、循環参照の問題を避けるために、unowned
修飾子を使う方法を示しましたね。無償郵産省、つまりunowned
を使うことで、どのように安全に使うかを説明しました。
例として、クレジットカードのインスタンスを持つようなコードでした。この場合、カードをクラスのメンバー変数として直接代入しました。この方法により、カードがインスタンスの所有者よりも長い生存期間を持たないことを保証できます。具体的には、代入式の形で直接クレジットカードのインスタンスを渡すコードを示しました。
以下のようなコードを覚えているでしょうか?
class Member {
var card: CreditCard?
}
class CreditCard {
unowned let member: Member
init(member: Member) {
self.member = member
}
}
このコードによって、CreditCard
のインスタンスはMember
よりも長く存在できないようにしています。unowned
修飾子を使って、循環参照を避けることができます。
問題は、逆の場合です。CreditCard
よりもMember
が先に解放されると、CreditCard
が指しているmember
が無効な参照になってしまう可能性があります。このような状況を避けるために、正しい方法で無償郵産省(unowned
修飾子)を使用することが求められます。
例えば、次のようにコードを改良することができます:
class Member {
var card: CreditCard?
init() {
card = CreditCard(member: self)
}
}
class CreditCard {
unowned let member: Member
init(member: Member) {
self.member = member
}
}
このコードによって、Member
のインスタンスが生成されるタイミングでクレジットカードのインスタンスも生成され、それぞれが適切に相互参照するようになります。
安全にコードを書くためには、プログラマーがきちんと管理する必要があります。実際、複雑なソフトウェアでは、循環参照を避けるための工夫が求められます。
このような管理を自動化したい場合には、例えば次のようにイニシャライザをプライベートにするという方法が考えられます:
class Member {
var card: CreditCard?
private init() {
// 初期化ロジック
}
class func create() -> Member {
let member = Member()
member.card = CreditCard(member: member)
return member
}
}
class CreditCard {
unowned let member: Member
init(member: Member) {
self.member = member
}
}
こうすると、外部から勝手にMember
やCreditCard
のインスタンスを生成することができなくなり、循環参照を避けるための構造が保たれます。
つまり、上記の方法で管理することにより、安全にオブジェクト間の依存関係を構築することが可能になります。
まさにこのような工夫で、循環参照の問題を回避し、安全なコードを書くことができます。これからも、このようなテクニックを駆使して、より安全で効率的なプログラムを書いていきましょう。 こういう書き方をすると、クレジットカードのイニシャライザはファイルプライベートになるので、同じファイル内でのみ定義できて、使えるようになります。そして、その後に自分自身を割り当てることができます。これにより、イニシャライザを直接呼ぶことなく、間接的にクレジットカードを作成することが可能になります。ただし、このカードが参照できる限り、独自の変数に取られてしまうとスコープが長くなりがちです。
とはいえ、このカスタマーが持つカードをグローバル変数に送る可能性よりも、イニシャライザを公開しておく場合のリスクのほうが高いです。ローカル変数に取ることがあっても、ローカルスコープが破棄されればなくなってしまうので、安全です。
カスタマーの定義したファイルとクレジットカードを定義したファイルが別々でないと気持ちが悪いと感じる人も多いでしょう。例えば、カスタマーの定義を Customer.swift
ファイルに、クレジットカードの定義を CreditCard.swift
ファイルに書くことが考えられます。そして、クレジットカードの定義の後に、エクステンションで Customer
に assignCard
メソッドを追加するようにします。これをプレイグラウンドで検証すれば動作するはずです。
クレジットカードのイニシャライザをファイルプライベートにして、同じファイル内で assignCard
をカスタマーに追加する形で実装します。これによりイニシャライザを呼べるし、カスタマーのカードにアクセスできるし、全体として安全です。ただ、カスタマーに対してカードを割り当てる方法自体や、assign
や createCredit
のようなメソッド名に関しては解釈次第です。
もしカスタマーがクレジットカードを作るとしたら、それは単なるセッターのように見えるので問題ないかもしれませんが、このアプローチが正に適しているかどうかは意見が分かれるところでしょう。バンクなどの銀行がクレジットカードを作る方が自然だと感じる人もいるかもしれません。それでも、大きな概念上の違和感がなければ、これで良いかもしれません。
万全を期するために、より安全にするなら final class
を使うのも手です。オーバーライドが怖いという点も考慮すると、普通にクラスを作るときは final
にしておいたほうが無難です。final class
にすることで、パフォーマンスの向上も期待できます。
クラスでデータ型を表現する時、特にオーバーライドをさせる気がない場合は final class
にすることが理にかなっています。特にデータ型をクラスで作ることがあるなら、その異常な事態を防ぐためにも final class
の使用は一考に値します。
質問もいくつかいただいていますが、それはまた後で取り上げましょう。まずは、この方法が一つの解決策として考えられますし、プライベートエクステンションもよく使います。これにより、ファイル内で適切に関数やプロパティをグループ化できます。
最後に、プライベートエクステンションは fileprivate
のように使えるため、非常に便利です。そして、将来的にはタイププライベートのような機能も期待されています。 例えば、Swiftにおけるprivate
の使い方について考えてみましょう。例えば、Customer
というクラスの中でtype private
のメソッドを定義すると、別のファイルでCustomer
を拡張する際にそのメソッドを利用することができません。このように、クラス内部でローカルに閉じることができます。
一方で、例えばCreditCard
クラスにCustomer
を取り込んで、type private
のメソッドでカスタムの処理を行うことも可能です。しかし、この場合、Customer
のエクステンションであっても、そのメソッドは呼び出せなくなります。そのため、type private
とした場合、同じファイル内であればエクステンションからも呼び出せるという利点があります。
こうしたprivate
の使用には場合によっては制約がつきものです。少し前のバージョンでは、エクステンション内でprivate
を利用することができなかったこともあり、仕様の変更によって使いやすくなった部分もあります。C++では、protected
というアクセス修飾子があり、これは自分自身や派生クラスからアクセスできるけれど、外部からはアクセスできないというものです。
また、C++にはfriend
という面白い機能もあります。例えば、CreditCard
クラスから直接Customer
のプライベートメソッドにアクセスしたい場合、friend
キーワードを使って特定のクラスや関数をフレンド指定できます。これによって、通常アクセスできないプライベートメンバーにアクセスできるようになります。
class Customer {
private:
void privateMethod();
friend class CreditCard;
};
友達関係を宣言すると、CreditCard
クラスからCustomer
クラスのプライベートメンバーにアクセスできるようになります。Swiftにはこのような機能はありませんが、C++の柔軟なアクセス制御の一例として知っておくと良いでしょう。
こういった他言語の特異な機能と比較してみると、Swiftのアクセス制御がどのように設計されているのかがよく理解できます。特に、Swiftではファイル内でのプライベートアクセスに重点を置いているため、この違いを踏まえて設計を検討するのが重要です。 プライベートアクセスについては、確かに自由度が高い分、リスクも伴います。セキュリティを考慮すると少し恐ろしく感じることもありますが、慎重に管理すれば便利な機能です。
次に、各プログラミング言語によってアクセス修飾子に個性があり、たとえばSwiftの「タイププライベート」という概念に触れてみます。これは面白い発想かもしれません。プライベートバーになるとモジュールをエクスポートする必要がなくなるため、バイナリサイズの最適化につながることもあります。このように、他のファイルで同じ名前の関数を定義しても衝突しないので、安心して汎用的な名前を付けられるメリットもあります。
次に、よく寄せられる質問のひとつ、weak
とunowned
の違いについて説明します。Swiftではメモリ管理がARC(Automatic Reference Counting)により行われます。例えば、weak
オブジェクトとunowned
オブジェクトがあります。
weak
は対象オブジェクトが解放された場合、自動的にnil
が代入されます。そのため、weak
変数は必ずオプショナルでなければなりません。一方、unowned
は対象オブジェクトが解放されてもそのアドレスが保持されたままなので、アクセスするとクラッシュする可能性があります。unowned
変数はオプショナルである必要はありませんが、失敗するリスクがあるのはプログラマーの責任となります。
これが大きな違いです。具体的には、weak
バーはnil
が入る可能性があるため常にオプショナルでなければならず、unowned
バーはオプショナルでなくても構いません。しかし、解放された対象にアクセスする危険があるため、プログラマーが注意して扱う必要があります。
以上、weak
とunowned
の違いを理解して、それぞれの特性を活かして安全に使い分けてください。次回の勉強会では、さらに詳細な内容を扱う予定です。