このページは「はじめてのSwiftプログラミング」(清水美樹著、工学社刊、2014年8月23日発売、ISBN978-4-7775-1851-7)の著者によるサポートページです
二つのインスタンスが相互に参照しあう場合、OSのARCシステムはどちらのインスタンスも消去すべからずと判断します。
しかし、「Aはいらなくなった」「BはAを便宜上参照しているが、参照が切れても別に構わない」という場合、Aをどのように消去
すればよいでしょうか。
その方法のひとつに「weak修飾子をつけて参照する」方法があります。
ただし、相互参照を行わない方法はいくらでもありますし、アプリの途中でインスタンスを無理に消去する必要はほとんどないので、 これから示すサンプルはかなり「無理に問題をひねりだして」作ってあります。むしろ、フレームワークのテンプレートなどに「weak」と 書いてある理由を考えるのに役立ててください。
これから示すサンプルプログラムは「ずいぶん面倒だな」と思うかも知れません。
その理由は、iOSプログラムだからです。世の中の多くのサンプルプログラムは「print」のような名前のメソッドによる
「コンソール出力」で、手早く結果を確かめることができますが、iOSにはコンソールはありません。
なにかのインスタンスが持つプロパティとしての文字列にまとめて、ラベルやテキストビューなどに表示しなければならないのです。
その「なにかのインスタンス」を作るのが面倒な作業になります。
でも、考えてみれば、サーバープログラムでなければコンソールに出力する意味はなきに等しいですから、 iOSに限らず多くのアプリでは、このようなプログラムの書き方になると思います。
これから作るプログラムは、前編と同じく、 「あるギター奏者が自分のコンサートツアーをするのに、ベース奏者とドラム奏者と契約する」というシナリオで記述します。
この場合、ベース奏者及びドラム奏者との契約が切れることがあります。でも、演奏者はインスタンスとして残ります。
一方、ツアーが中止になった場合、そのツアーのインスタンスは消滅するようにします。
ツアーを企画するギター奏者は、Leaderというクラスのインスタンスにします。
この奏者のインスタンスはひとつしか作りませんが、Swiftの使用上スタティックなクラスは使いにくいので、普通のインスタンスにします。
Leaderにmessageという文字列プロパティを持たせ、ここに作業記録を全て保持させます。
ギター奏者と契約するベース奏者とドラム奏者はそれぞれクラスMusicianのインスタンスで、クラスTourのインスタンスと相互参照の 関係になります。
また、全ての作業を記録するため、MusicianとLeader、及びTourとLeaderも相互参照になります。
三者が相互に参照する中でTourのインスタンスを消滅させます。考え方のキモは、Tourを参照するインスタンスを切ることです。
丁寧に切って行くならweak修飾子は不要です。乱暴でも手早く切るときにweakを使います。
クラスTourにメソッドdeinitを定義し、その中でLeaderのプロパティmessageに記録を残します。
クラスLeaderは、クラスTourのオプショナル型のインスタンスをプロパティとして定義します。
Musicianの操作はTourを通して間接的に行います。
また、記録保持のためのプロパティmessageを持ちます。
Leaderはツアーの企画, 奏者との契約, 契約破棄, 及びツアーの中止メソッドを持ちますが、 奏者との契約と契約破棄のメソッドは、Tourの同名メソッドを通じて間接的に行います。
以上の内容を、リスト1に概略で示します。
class Leader{ var tour:Tour? var message:String func planTour(tourName:String){ self.tour = Tour(name:tourName) tour!.leader = self message += 作業記録 } func addBass(bassist:Musician){ tour!.addBass(bassist) message += 作業記録 } 同様にメソッドaddDrums, delBass, delDrumsを定義 //作業記録は残さない func cancelTour(){ if (tour != nil){ tour = nil } } //作業記録を全部出力 func reportMessage()->String{ return message } }
メソッドcancelTourは、Tourへの参照を切るメソッドです。
このメソッドではプロパティmessageに記録を残しません。
なぜなら、この作業でTourのインスタンスが消滅するはずなので、Tourのdeinitメソッドから作業記録を書き込むのです。
クラスTourは、前編と同様、ベースとドラム奏者をMusicianのオプショナル型でプロパティとして有します。
そして、奏者を直接操作するaddメソッドとdelメソッドを持ちます。これを、Leaderのaddメソッドとdelメソッドが間接的に呼び出します。
一方、前編で用いていたプロパティmessageはなくなります。Leaderのプロパティになったからです。
Tourでは自分が消滅するときのdeinitメソッドで、Leaderのプロパティmessageを用います。 そのため、自身のプロパティとしてLeaderのオプショナル型を有します。
class Tour{ let name:String var leader:Leader? var bass:Musician? var drums:Musician? func addBass(bassist:Musician){ ベース奏者を追加 } func addDrums(drummer:Musician){ ドラム奏者を追加 } func delBass(){ ベース奏者を外す } func delDrums(){ ドラム奏者を外す } deinit{ if (leader != nil){ leader!.message += このインスタンスが消滅するという報告 } } }
TourとMusicianのインスタンスを包括するクラスLeaderを作成したので、実はMusicianはTourを参照する必要がなくなります。
しかし、「相互参照」の問題を論じたいので、前編同様、Tourを参照します。特に重要な意味の参照でなくて構いません。
また、記録を保持するために、Leader(オプショナル型)のインスタンスをプロパティとして持ちます。
なお、ベース奏者・ドラム奏者の消滅は考えないので、Musicianのdeinitメソッドは特に書きません。
class Musician{ let name:String var part:String var leader:Leader? var tour:Tour? Tourを参照するようなメソッド(作業記録はleader!messageに書き込む) }
ツアーのインスタンスの作成を、クラスLeaderのメソッドplanTourにおいて行うようにします。
作成したインスタンスを、自らのプロパティtourに割り当てます。
重要なことは、インスタンスには変数名をつけずに、直接新規作成したものをプロパティに放り込むことです。
変数をつけると、「その変数が参照している」ことになるので、消滅させるのが面倒になります。
func planTour(tourName:String){ self.tour = Tour(name:tourName) //新規作成して直接投入 tour!.leader = self message += "今度、\(tour!.name)に行くよ!\n" }
クラスLeaderに、奏者を加えるためのメソッドaddBass及びaddDrumsを定義します。
主な作業は、プロパティtourの同名のメソッド(後述)を呼ぶだけですが、リーダーの立場から「彼らを加えた」という作業も記録します。
func addBass(bassist:Musician){ tour!.addBass(bassist) message += "\(bassist.name)期待してるぜ!\n" } func addDrums(drummer:Musician){ tour!.addDrums(drummer) message += "\(drummer.name)期待してるぜ!\n" }
前編でもクラスTourにaddBass, addDrumsを定義しました。
それは、「『bass及びdrumsのそれぞれプロパティ』tour」に、自らを割り当てることです。
今回のそれらのメソッドには、さらに追加の作業があります。
func addBass(bassist:Musician){ bass = bassist bass!.tour = self bass!.leader = self.leader bass!.joinTour() } func addDrums(drummer:Musician){ drums = drummer drums!.tour = self drums!.leader = self.leader drums!.joinTour() }
リスト7のようなメソッドJoinTourを書き込みます。
内容は大したことはありません。ただ、自らのプロパティmessageを用いて、「自分が加わった」という記録を保存します。
同時に、「MusicianがTourを参照」しているという事実を作ります。
なお、リスト7でプロパティleaderやtourの中身は、Leader及びTourのインスタンスが操作してくれますから、 自らはなんらかの代入作業を行う必要はありません。
func joinTour(){ if(leader != nil && tour != nil){ leader!.message += "「\(name)です。\(tour!.name)に参加できて嬉しいです。」\n" } }
さてこれから、ツアーから奏者を外す作業を書きます。参照の問題なく奏者を外すには、ツアーから奏者への参照も、 奏者からツアーへの参照もキチンと外さなければなりません。
クラスLeaderにメソッドdelBassとdelDrumsを定義します。いずれも、プロパティtourに同名のメソッドを呼ばせるだけです。
func delBass(){ tour!.delBass() message += "そいつは残念だ!\n" } func delDrums(){ tour!.delDrums() message += "そいつは残念だ!\n" }
奏者とツアーの参照を相互に消す作業は、Tourで定義するdelBass, delDrumsメソッドで行います。
まず、自身のプロパティbassとnilに、Musicianで定義する(後述)cancelTourメソッドを呼ばせて奏者からツアーへの参照を外します。
そのあと、bassとdrumsをnilにします。
func delBass(){ if(bass != nil){ bass!.cancelTour() bass = nil } } func delDrums(){ if(drums != nil){ drums!.cancelTour() drums = nil } }
クラスMusicianには、メソッドcancelTourを定義します。 このメソッドでは、奏者がツアーへの参照を切ります。すなわち、自分のプロパティtourをnilにします。
また、作業を記録します。
func cancelTour(){ if (leader != nil && tour != nil){ leader!.message += "「\(name)です。\(tour!.name)にいけなくなって残念です。」\n" tour = nil } }
クラスLeaderにメソッドcancelTourを作成します。
クラスMusicianのメソッドと同じ名前ですが、直接の依存関係はありません。
参照関係を外す作業はプロパティtourがやってくれたので、そのtourをnilにするだけです。
作業記録も残しません。tourがdeinitメソッド(後述)で記録します。
func cancelTour(){ if (tour != nil){ tour = nil } }
deinitメソッドで、「自分のプロパティleader」のプロパティmessageに作業記録を残します。
deinit{ if (leader != nil){ leader!.message += "バッドニュースだ。\(name)は中止になっちゃったんだ。この次は必ず行くよ!" } }
以上、重要な記述について説明しました。 他の記述も含めてViewController.swiftに書くと、 別ページのようになります。
実行してみてください。以下のようにメッセージが現れるでしょう。
こんにちは、ジェフです! //インスタンスjeffの作成 今度、2014アメリカ横断スーパーツアーに行くよ! //メソッドplanTourの実行 「トニーです。2014アメリカ横断スーパーツアーに参加できて嬉しいです。」 //TourのメソッドaddBassの処理 トニー期待してるぜ! //LeaderのメソッドaddBaseの処理 「ビルです。2014アメリカ横断スーパーツアーに参加できて嬉しいです。」 //TourのメソッドaddDrumsの処理 ビル期待してるぜ! //LeaderのメソッドaddDrumsの処理 「トニーです。2014アメリカ横断スーパーツアーにいけなくなって残念です。」 //MusicianのメソッドcancelTourの処理 そいつは残念だ! //LeaderのメソッドdellBassの処理 「ビルです。2014アメリカ横断スーパーツアーにいけなくなって残念です。」 //MusicianのメソッドcancelTourの処理 そいつは残念だ! //LeaderのメソッドcancelTourの処理 バッドニュースだ。2014アメリカ横断スーパーツアーは中止になっちゃったんだ。この次は必ず行くよ! //Tourのdeinitの処理
最後の出力で、Tourのインスタンスがめでたく消滅したことがわかります。
もし、リーダーのジェフさんが奏者にキャンセルするしないを通知せず一方的にツアーをキャンセルするとしたら、 プログラムはどのように書けばいいでしょうか?
それには、クラスLeadersの定義において、一方的に「プロパティtourのプロパティbassをnilにする」という操作を行ってしまうようにします。 MusicianのメソッドcanselTourも、TourのメソッドdelBassなども使いません(参照関係を残すために、そのまま書いて置きます)。
func delBass(){ message += "\(tour!.bass!.name)悪く思わんでくれ!\n" tour!.bass = nil } func delDrums(){ message += "\(tour!.drums!.name)悪く思わんでくれ!\n" tour!.drums = nil }
リスト12では、処理の順番に注意してください。プロパティbassやdrumsを用いた処理(「悪く思わんでくれ」と言う) が終わってから、nilにします。
以上のように変更して実行すると、ツアーが消滅するときの出力が表示されません。 すなわち、Tourインスタンスのdeinitメソッドが呼ばれなかったのです。
そこで、weak修飾子です。これは、「外部操作でnilになってもいいプロパティ」につけます。
今の例では、「ベースやドラムの奏者はツアーがnilになっても存在に問題を生じません。
そこで、クラスMusicianにおけるプロパティtourの定義にweak修飾子をつけます。
class Musician{ let name:String var part:String var leader:Leader? weak var tour:Tour? //他はこれまで通り
実行してみましょう。図3のように、Tourのdeinitメソッドが再び呼ばれるようになりました。
簡単なプログラムで、「weak」を使ってでもあるインスタンスを強制的にnilにしなければいけないということは、
ほとんどありません。
しかし、フレームワークのテンプレート(iOSのUIKitがまさにそれ)などで「weak」修飾子が要求されるとき、「どうしてか?」
「これはweakなのか違うのか?」と迷うことがあると思います。そのときは、「メモリの節約」と「データの重要性」を考慮してください。