今回は Automatic Reference Counting
の 循環参照
周りを見ていくことになりそうですけれど、その前に、前回の終わりに遊んだ 参照カウント
を実際に見ていく周りをもう少しだけ遊んでみたいようにも思ってます。今となってはあまり実用的な話ではないですけれど、強要というのか余興な感じで、どうぞよろしくお願いしますね。
—————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #213
00:00 開始 00:10 今回の展望 01:16 参照カウンターのおさらい 01:54 参照カウンター取得の話 02:42 今回の展望 03:54 参照カウントの様子を窺ってみる 05:28 MRC の頃の弱参照は? 06:30 Objective-C 時代の @property 10:48 弱参照は Weak テーブルで管理 13:53 改めて Swift からの参照カウント参照に着目 18:10 有意義な参照数は取得できなそう 20:20 引数に渡したときも Retain 対象 21:11 retainCount の所有先 22:05 存在型の書込可能プロパティーには書き込めない? 27:14 AnyObject は @objc な全てを呼出可能 28:53 AnyObject がクラス型かを調べるには? 32:15 AnyObject で扱う実体が所有しないメソッドを呼び出すと? 34:41 ほかのセッターが呼べない事例 35:53 プロトコルにおけるミュータブルの扱いは異様 40:46 AnyObject の範疇でプロパティーに言及する 43:02 self を存在型として扱うと? 45:15 直近しか考慮されないことに違和感 47:21 微妙ながらも見事なワークアラウンド 49:07 ワークアラウンドの最適解探し 50:58 存在型が特殊な様子 52:13 不透明型でも書き込めない 52:50 今日のおさらいとクロージング ——————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #213
では始めていきます。今日は本題である循環参照についての話に入っていこうと思いますが、前回のリファレンスカウンタ周りについてもう少し触れておくのも有意義かなと思います。最近あまり触れることのない話題ですし、それを通じて少しトリッキーなこともできたりするので、参照カウンターについても少し遊んでみようかなと思います。ですから、今日は予定的な回になるかもしれませんが、楽しんでいきましょう。
参照カウンター(ARC)は、参照カウントを使ってオブジェクトのメモリ管理をしています。参照されているオブジェクトがカウントされ、参照カウントがゼロになった時点で自動的にデアロケート(解放)される仕組みです。かつてはオートマティックリファレンスカウントではなく、プログラマーが自分で管理しなければならなかったので、参照カウンタの数を目視で確認できるという仕様が非常に便利でした。
参照カウントがいくつあるのか、どういう操作をするとカウントが増減するのかを、コードを実際に書いて確かめることができるのです。例えば、あるタイミングで参照カウントを見た時に、奇妙な値(例えば7など)が出てくることがあります。その理由が気になりますが、この記事を執筆する際に調査が足りていなかったため、これを機にもう少し掘り下げてみようと思います。
参照カウントが7になってしまった原因を探るために、いくつかの手法を試してみましょう。例えば、コードの一部分をコメントアウトした時にどう変化するかを試してみます。実行する度に参照カウントが微妙に変わることもあります。これはプログラムの裏で動いている事情に関係があるかもしれません。複数のブロックに分ければ、ローカルスコープ内の動きをある程度予測できますが、予測とは異なる動きになる可能性もあります。
ARCが登場した当初も、参照カウントが適切に得られないことがあるという話がありました。Web着参照が容易にできた以前とは事情が異なり、現在は開放された時にnil
が入るといった特徴があります。ただ、その際にはリテインやリリースの操作が必要だったため、管理が難しかったのです。
@property
という書式を使って、ゲッターやセッターのメソッド名を指定できるようになり、リードオンリーやリードライトといったオプションも指定できました。iOSのプロパティを作る際にはnonatomic
として、スレッドセーフでない設定が一般的でした。その理由は、プロパティの書き換えを保護するとパフォーマンスが低下するからです。
現在では、ウィークポインタを使って自分でメモリ管理をする方法もあります。MRC(マニュアルリファレンスカウント)で行っていたウィークポインタを用いた管理は、ARCの登場によってかなり簡素化されました。しかし、その過程で起こる細かい挙動については依然として注意が必要です。 そうなんですね、ウィークポインタというものが用意されているんですね。少しゆっくり読んでみましょうか。
クリックして要点を確認してみると、主に第3章に関係があるようです。どうやら「ウィークポインタ」があって、それが「リアルオブジェクト」というものへの参照を保持しているんですかね。動作次第でいろいろと変わるようですが、まずは登録する必要があるみたいですね。
実装としては オンレシーブデリゲートプロトコル
について触れていましたが、正確な内容はまだわかりません。「リアルオブジェクトプロトコルデリゲート」という表現があり、それに関連するメッセージがあるようですが、この時点では理解が進んでいません。
ウィークポインタの扱いに関して自分で実装する余地があるようですね。ウィークポインタとは何なのか、基本的な情報を把握することが重要です。プログラミング言語 Swift で言う「弱参照」の機能を指しています。これはメモリ管理のための機構で、オブジェクトが解放されたときに自動的に参照を無効化するために「ウィークテーブル」という仕組みが存在します。
ウィークテーブルでは参照を管理し、オブジェクトが解放された場合、その参照を自動で無効化します。これは、参照カウントがゼロになったオブジェクトを正確に解放するために重要です。
ARC(自動参照カウント)では、参照カウントの変化を追跡してメモリを管理しますが、ウィークポインタを使う場合、このウィークテーブルが非常に重要な役割を果たします。例えば、ARCドキュメントを参照してみると、リファレンスカウンタがどのように動作しているか理解するのに役立ちます。
ウィークテーブルに関する情報が出てくるタイミングについては、少し具体的な名前や状況を見てみましょう。iOS開発においても同様の機能がありますし、特定のライブラリやフレームワークがうまく取り扱っている場合もあります。
前回も少し触れたように、任意のオブジェクトを管理しつつリテインカウントを追跡する際、ARC以前では手動で管理する必要がありました。現在では、ARCがこのプロセスを自動化しているため、手動でリテインカウントを操作する必要が少なくなりました。
例えば、CFGetRetainCount
というAPIもありますので、これを利用してオブジェクトの参照カウントを確認できます。他にも、アンダースコアゲットアンオンドリテインカウント
という関数があるので、それを使うことで詳細な参照カウントを取得することも可能です。これらの関数を活用することで、メモリ管理の詳細な動作を確認することができます。 リテインカウントっていう名前自体はこのコンテキストからは見えていません。そういうわけで、リテインカウントなのか、ウィークリテインカウントとアンオンリテインカウントがあるらしいです。これらも全部ひっくるめてカウントされているのでしょうか。これを直接呼ぶにはどうすればよいのか考えてみます。
定義自体はSwiftのライブラリに書いてあるので、ビルドをかけると例えばインスタンス指定してエラーが出る場合があります。これは単純に形が違うだけかもしれません。意味を覚えていくとキャッチしてビルドを通すことができます。プリントしてみますか。たとえば、「print(retainCount)
」のようにしてプリントしてみましょう。
ゲット
の普通のリテインカウントと、ゲット
のアンオンリテインカウントがあります。これらを例えばAとBとCに分けて目印をつけてみます。ビルドしてランしてみると、「8 1 1」と表示されます。ちょっと豪快ですが、いろいろ出てきますね。今のこのコードは横参照していませんが1とか出てくるんです。また、アンオンリテインカウントも1とか出ています。
これはキャストの問題かもしれません。例えば、すべてのオブジェクトをオプショナルではなくて、エニーにしたと仮定して試してみます。これで開放しない前提でオプショナルにしないようにして、再度実行してみます。コメントアウトした部分があるので少し変わるかもしれません。結果として「6 1 1」になりました。ここでキャストしてしまっていますので、素直にエニーオブジェクトにして3回代入して表示させてみます。
このコードでは要らない部分も出てきますので、除外してもう一度ビルドすると、「6 1 1」で変わりません。少し意図が見えづらいかもしれませんが、この画面ではこんな感じです。後ろのコードをコメントアウトするとどうなるのか試してみると、「4」になりました。2個分減ったわけです。これは関係ないかもしれませんが、理解の助けになるでしょう。1個カウントリテインするとして、パラメーターに渡した時点でもカウントが変わります。
例えばこの方法ではなく、他のメソッドに呼び出した場合でもカウントが変わってくる場合があります。ほかのシナリオでも試してみると、多少の違いが見られます。例えば復活させると「5」と表示されます。このような検証を通してリテインカウントについて理解を深められます。
ところで、リテインカウントメソッドがどこにあるのかというと、NSオブジェクトに含まれています。NSオブジェクトにはリテインカウントがあると思いますが、それを使用するときには注意が必要です。
例えば、リテインカウントを操作しようとしたとき、NSオブジェクトのリテインカウントがあったとしても、それがイミュータブル(不変)であるため代入できない場合があります。リファレンスはイミュータブルなので、たとえバーにしてもリテインカウントがセッターを持っていても、直接的には操作できないことがあるのです。
このように、Swiftのリテインカウントについて学習するときは、詳細や例外的なケースに気を付けながら検証することが必要です。 リファレンスの使い方について話しているようですね。パーソンは通常のクラスで、これをNSObject
互換にしてイニシャライザーでスーパーのユニットを呼びます。そして、書き換え可能なプロパティx
を用意します。しかし、以下のようにリファレンスクラスのx
が書き換えられない場合があります。
class Person: NSObject {
var x: Int
init(x: Int) {
self.x = x
super.init()
}
}
この場合、書き換えできないとエラーが出ることがあります。なぜなら、ゲッターオンリープロパティとして扱われるからです。それを防ぐために、@objc
マークをつける必要がある場合もありますが、それでもビルドがうまくいかないことがあります。
また、NSArray
のようなオブジェクトではアペンドが可能ですが、使い方を間違えるとエラーが発生することがあります。例えば、オブジェクティブCのレベルでマークした場合、それが禁止されることがあります。これはSwiftで呼び出すときの問題ではなく、実装の段階でセットできないようにしている可能性もあるでしょう。
メソッドの振る舞いが異なる場合があります。例えば、参照型を表すためにオプショナル型が使われることがあります。Optional<Person>
のように、これは基本的にはミュータブルでないものとして扱われます。
Objective-CのメッセージパッシングとSwiftのメソッド呼び出しの仕組みが異なるため、インターフェース探しで問題が生じることがあります。特に、アクセシビリティの面で、オプショナルなインターフェースに変わることがあり、それが見失われることもあります。Objective-Cでは、このような状況によりクラッシュすることもありました。
セッターが制約されていることもあります。セッターを呼べる構文がなければ、特別なケースを除き、完全なミュータブルとはみなされません。
例えば、以下のようにオプショナルを使う場合もあります。
var person: Person?
person?.x = 10
この場合、person
が存在しないとクラッシュする可能性がありますが、オプショナルを使うことでそれを防ぐことができます。
以上のように、SwiftとObjective-Cの互換性を考慮しながらコーディングすることが重要です。 なるほど、特殊なセッターが呼べないことについて議論していますね。セッターを呼ぼうとしても、オプショナルのセッターは呼びにくいことがあります。これは非常にマイナーな現象ですが、面白いテーマですね。アクターも似た動きをし、自分の範囲内でしかプロパティを書き込めない点で関連があるかもしれませんが、具体的な関連性は見つかっていないようです。
一応、これは余談の中でも特に余談になるので、興味がある人は個人的に調べるといいでしょう。話を戻すと、初年にMWK(ミューティングの代わりに使用される?)したアプローチの中で似たようなケースが見られています。プロトコルのセッターがデフォルトでミューティングを使えないという話ですね。
具体的なケースでは、MyProtocol
というプロトコルがあり、それのプロパティコールセッターにミューティングを付けて試みたが、予想通りにはいかないという現象が見られています。プロトコルのセッターにミューティングをつけられない場合も考えられます。
プロトコルエクステンションのメンバーにはミューティングが使えない場合があるので、エクステンションのメンバーにはミューティングを使えない特性があるようです。一方で、実際にコードで試してみる価値はありますね。
プロトコルのセッター、特にプロトコルエクステンションのセッターではミューティングがうまく使えないという問題があります。この現象は、コードの実装とプロトコルの仕様に起因している可能性があります。
したがって、AnyObject
を使った場合には、プロトコルのセッターにミューティングを含める試みを行う価値があります。例えば、プロトコルではAnyObject
をつけても、実装部分で具体的にエラーが出る場合があります。
protocol MyProtocol: AnyObject {
var property: Int { get set }
}
このように、プロトコルにAnyObject
を付けることでエラーを回避できる場合もあります。ただし、コード全体の状況やミューティングの設定が正しく反映されない場合があるので注意が必要です。
これらの話題は実装において重要なポイントとなりますが、調査を進めることで新たな発見があるかもしれません。 例えば、次のようなコードがあります。"x = self as MyProtocol" これは "MyProtocol" にキャストされるので、どうなるか試してみたいと思います。参照型であるため、面白い結果が期待できます。
参照型である場合、この手法は有効です。しかし、"MyProtocol" のプロパティは明示的に指定しなければなりません。例えば、プロパティをアクセスする際に、"MyProtocol" のプロパティであることを示す必要があります。それによって、正しいプロパティが参照されることを確認することができるのです。
さて、"MyProtocol" を親として継承している場合も考慮する必要があります。しかし、それがミュータブルであれば問題ありません。この関数の定義として、"MyProtocol" を参照していますので問題ありませんが、実際にプロパティを読み出す際には、本当に "MyProtocol" のプロパティだけが参照されることを確認する必要があります。
このように考えると、コンパイラの動きに対して正しい挙動であるか疑問が出てきます。しかし実際には、コンパイラは正しく動作しているようです。あるイシューが報告されていて、まだクローズされていないようですので、最新のバージョンで確認する価値があります。
昨日は参照型で問題が発生することを確認しましたが、今のところ回避策を見つけました。セルフキャストで試してみると、問題なく動作しました。素晴らしいワークアラウンドですので、実際に試してみてもいいと思います。
ある場合に "self" とプロパティの間に関係が変わるとき、特に "AnyMyProtocol" のような形でそれを扱う必要がある場合も発生します。"AnyMyProtocol" とは、特定のプロトコル型をラップしているもので、プロパティに直接書き込みができなくなる問題があるのです。これは、プロトコル型の扱い方に特有の問題です。
このような問題に対して、一時変数を使うのが有効な回避策となります。次回の勉強会では、循環参照の問題について再度議論し、より深掘りしていきます。
以上で今回の勉強会を終わります。ご参加ありがとうございました。