ARC
ARC(Automatic reference counting)の管理下にあるクラスでのクロージャ式プロパティの宣言や、互いにプロパティ同士で参照しあうクラスでは、Objective-Cと同様に循環参照の問題が起こります。Swiftでは参照型のオブジェクトはARCの管理下にあります。クラスとクロージャは参照型のためにARCの管理下にあり、クラスのインスタンス同士が相互に参照し合う場合や、プロパティとして宣言したクロージャの中で自身のインスタンスselfを参照している場合に、循環参照が引き起こされます。
weakキーワード
Objective-Cでのプロパティ宣言にweakを付けられたのと同様に、weakキーワードをプロパティに付けることで、インスタンスに対する弱参照を実現できます。さらに、弱参照を実現することにより、強参照によって引き起こされる循環参照の問題を解決できます[3]。
class Employee {
var employer: Employer?
deinit {
println("employee fired")
}
}
class Employer {
var employee: Employee?
deinit {
println("employer cllapsed")
}
}
var employee = Employee()
var employer = Employer()
employer.employee = employee
employee.employer = employer
この例では互いのクラスのインスタンスがプロパティを経由して強参照しあっているため、循環参照が引き起こされています。
employee、employerのインスタンスはいずれも他のインスタンスを強参照のプロパティとして保持しているために、宣言されたスコープを外れてもARCによって破棄されずにデイニシャライザも呼ばれません。
weakキーワードはこの循環参照の問題を解決します。weakキーワードを付けるプロパティは、オプショナル列挙型の変数プロパティとして宣言することが必要です。プロパティは弱参照として処理され、通常の強参照を用いた場合の循環参照の問題は解決されます。
class Employee {
var employer: Employer?
deinit {
println("employee fired")
}
}
class Employer {
weak var employee: Employee?
deinit {
println("employer cllapsed")
}
}
var employee = Employee()
var employer = Employer()
employer.employee = employee
employee.employer = employer
この場合、employeeインスタンスはemployerインスタンスの弱参照のプロパティとして参照されているため、スコープを抜けると破棄されます。破棄と同時にemployeeインスタンスからの強参照がなくなるので、employerインスタンスも破棄されます。
weakキーワードの付いたプロパティは参照があるのみで、保持によるメモリ管理を行わないため、参照先自体は他に保持するインスタンスがない場合には、いつでも解放されてしまう可能性があります。解放された場合、プロパティはnilとして扱われるので、weakプロパティを用いるときには、オプショナルチェインニングなどで値の存在を確認することになります。
[3] 強参照と弱参照の詳しい解説は、書籍『詳解 Objective-C 2.0 第3版』に書かれています。
unownedキーワード
weakキーワードと同様に、強参照によるメモリ管理の問題を解決するためにunownedキーワードが用意されています。
unownedキーワードはweakキーワードと異なり、プロパティに対して常に値が保持されていると確定している場合に使用されます。このためunowned付きのプロパティはnilとして扱われることはなく、非オプショナル列挙型で宣言します。
class Employee {
unowned var employer: Employer
init(employer: Employer) {
self.employer = employer
}
deinit {
println("employee fired")
}
}
class Employer {
var employee: Employee!
init() {
self.employee = Employee(employer: self)
}
deinit {
println("employer cllapsed")
}
}
var employer = Employer()
weakキーワードの例とは異なり、まずこの例ではEmployerのインスタンス生成時にEmployeeがプロパティとして代入されます。
Employerでは、Employeeに「!」を付けてImplicitlyUnwrappedOptional型にしています。初期化の過程での保持プロパティに関しては必ず何らかの値が入っている必要があり、Employee!とすることで保持プロパティにはデフォルト値nilが代入され、プロパティへの代入時にselfを使うことが許されるためです。
「!」がない場合には、イニシャライザでemployeeプロパティの中に何らかの値を初期化完了までに代入する必要があります。この例ではEmployeeクラスのイニシャライザの引数として、初期化の完了していないselfを呼び出しているため、エラーとなります。
また、unowenedキーワード付きプロパティが値を何も保持していなかった場合にアクセスを受けると、ランタイムエラーが起こります。
クロージャ式プロパティ内部での参照の扱い
本連載第2回で、クロージャ式が外部のインスタンスをキャプチャすることを説明しました。
クロージャ式をプロパティとして持つ次のようなクラスを考えてみましょう。
class WebService {
let urlString: String
lazy var onSuccess: String -> Void = {
responce in
println("request for " + self.urlString + " was succeeded with responce " + responce)
}
init(urlString: String) {
self.urlString = urlString
}
deinit {
println("web service removed")
}
}
var service = WebService(urlString: "http://somedomain.com")
var successHandler = service.onSuccess
こちらの例では、lazyキーワードの付いたonSuccessプロパティアクセス時にクロージャ式がselfをキャプチャしています。そしてself自身はonSuccessプロパティを保持しているために、serviceインスタンス内部での循環参照が起こっています。
スコープを抜けてもインスタンスは解放されないために、web service removedのログが出力されません。
なお、onSuccessプロパティ初期化時にデフォルト値としてselfの入ったクロージャ式を代入しようとすると、初期化を済ませていないselfがデフォルト値のクロージャ式に入っているため、エラーとなります。それを避けるためにlazyキーワードを追加しています。
さて、この事象を解消するためには、プロパティへのデフォルトクロージャ式代入時にselfのキャプチャが起こらないよう、次のように宣言を変更します。
lazy var onSuccess: String -> Void = {
[weak self] responce in
if self != nil {
println("request for " + self!.urlString + " was succeeded with responce " + responce)
}
}
この変更により自身のインスタンスのキャプチャが解消され、循環参照に伴うインスタンスが解放されない問題も解決します。
キャプチャに関わるweakとunownedキーワードの使い分けもプロパティの宣言の時と同様に、weakはクロージャ式実行時にキャプチャ解除対象がnilを取りうる場合に用います。そのためweakを付けたキャプチャ解除対象の値はオプショナル列挙型として扱われます。unownedはクロージャ式実行時にキャプチャ解除対象が存在し続ける確証がある場合に用います。
