The Swift Programming Language: Automatic Reference Countingを読みながら学んだことをメモしておきます。ざっくりと書いてあるので、細かい説明は原文を当たってご確認下さい。
注意事項
ARCのテストコードは、PlayGroundで実行しても正しく動作しません。PlayGroundに書かれたSwiftのコードを再評価するプロセス的な何かの参照が外れることはないからだと思われます。
強い参照の時の注意点
例えば、下記のSampleというクラスがあるとします。
class Sample {
let name: String
init(str :String) {
self.name = str;
println("init called")
}
deinit{
println("deinit called")
}
func callFunc() {
//なんか実行
}
}
で、こうやって初期化すると、当然initが走ってprintlnが実行されます。
var a = Sample(str: "Hello World") //init calledと表示される
その後こういうことをします。
var b = a
var c = a
a = nil
b.callFunc() //ここでCrash
が、参照先のaにnullを代入しても、bとcはずっとaの参照を持ったままです。参照先(a)がnilだけど、自分自身(b)がnilじゃないのでbは保有している参照先を見に行くけど既にnilなので糸冬っていう。定番のCrash原因ではないかと思います。
この状態でdeinitがcallされるには、すべての参照にnilを代入する必要があります。
a = nil
b = nil
c = nil //deinitが呼ばれる
循環参照問題
クラスのプロパティは基本的に強い参照となります。簡単に言っちゃえば、強い参照はお互いのインスタンスを参照しあう循環参照をやってしまうと、リファレンスカウンタが0の参照が出来てしまいずっとデイニシャライザ(deinit)が呼ばれない。リークしまくり。
classAとBがあって、AがBのインスタンスを参照し、BがAのインスタンスを参照しているようなコードを書く場合は、強参照はNGだと覚えておけば生きていける。気がする。
循環参照を解決する2つの方法
Swiftでは循環参照を解決するに当たり、「Weak Reference」と「Unowned Reference」の2つがあるよ、と書いてあります。
弱い参照(Weak Reference)
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { println("\(name) is being deinitialized") }
}
class Apartment {
let number: Int
init(number: Int) { self.number = number }
//ここね。弱参照を宣言
weak var tenant: Person?
deinit { println("Apartment #\(number) is being deinitialized") }
}
//初期化して
var john: Person?
var number73: Apartment?
john = Person(name: "John Appleseed")
number73 = Apartment(number: 73)
//互いに見つめ合って
john!.apartment = number73
number73!.tenant = john
//別れる
john = nil //Personのdeinitが呼ばれる
number73 = nil //Apartmentのdeinitが呼ばれる
参照を保持しない参照(Unowned Reference)
Unownedをどう訳すか悩む。要はリファレンスカウンタにカウントされない=オブジェクトのオーナーにならない=参照を保持しないっていうこと。
オプショナルな値を許可していないこと、参照元がnilになると自動的にnilになるのが弱参照プロパティとの違い。オプショナルな値がNGってことは何かしら値が入る前提のようだ。僕はこっちのほうが便利な気がする。
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { println("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
//これがunownedのプロパティ
//見ての通りoptionalは許可されない
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { println("Card #\(number) is being deinitialized") }
}
//CustomerはCreditCardを強参照
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
//Customerが参照を外すと、CreditCardのcustomerも参照から外れる
john = nil
// prints "John Appleseed is being deinitialized"
// prints "Card #1234567890123456 is being deinitialized"
お互いのオブジェクトを(AとBとします)保有しあう場合、AもBもオプショナルならweakを、どちらかがオプショナルならunownedを利用すると良いと書かれておりました。従属性があるんであれば、unownedの方が便利な気がする。
では、AもBも何かしらの値が入る場合はどうすればいいのか。AオブジェクトがイニシャライザでBを初期化して、AのプロパティとしてBを持つ。でBはunownedにする
class Country {
let name: String
let capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
//Cityの定義は上と同じなので割愛
var country = Country(name: "Canada", capitalName: "Ottawa")
println("\(country.name)'s capital city is called \(country.capitalCity.name)")
// prints "Canada's capital city is called Ottawa"
こうすれば、CityクラスのcapitalCityプロパティは循環参照を避けながらも他のクラスのプロパティを保有できるぞ、ということみたい。
クロージャにおける強い参照
Swiftではクロージャが書けるけど、クロージャの中でクラスのインスタンスを参照してしまう場合はクロージャとそのクラスのインスタンスの中で循環参照が起こると書いてあった。こういうコードがそれにあたる。
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
//これだと己を強参照してる
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
deinit {
println("\(name) is being deinitialized")
}
}
このクラスのオブジェクトを作ってnilを与えても、deinitは呼ばれない。生成したインスタンスとクロージャで参照しているインスタンスが循環参照を起こしてしまう。なので、今まで書いてきたようにクロージャ内で参照しているselfをunownedのように、何かしらの値が初期化時に入るけれども参照は保持しないよっていうのを実現できれば良い。
//クロージャに引数がある場合はこう
lazy var someClosure: (Int, String) -> String = {
[unowned self] (index: Int, stringToProcess: String) -> String in
// closure body goes here
}
//無いならこう
lazy var someClosure: () -> String = {
[unowned self] in
// closure body goes here
}