Swiftでプログラミング-Automatic Reference Counting
Swiftは、自動参照カウント(ARC)を使用して、アプリのメモリ使用量を追跡および管理します。 ほとんどの場合、これはSwiftでメモリ管理が「正しく機能する」ことを意味し、メモリ管理について自分で考える必要はありません。 ARCは、クラスインスタンスが不要になると、クラスインスタンスが使用していたメモリを自動的に解放します。
ただし、場合によっては、ARCでメモリを管理するために、コードの各部分間の関係に関する詳細情報が必要になります。 この章では、これらの状況について説明し、ARCでアプリのすべてのメモリを管理できるようにする方法を示します。 SwiftでのARCの使用は、Objective-CでARCを使用するためのARCリリースノートへの移行で説明されているアプローチと非常によく似ています。
参照カウントは、クラスのインスタンスにのみ適用されます。 構造体と列挙型は値型であり、参照型ではなく、保存されたり、参照によって渡されたりすることはありません。
How ARC Works
クラスの新しいインスタンスを作成するたびに、ARCはそのインスタンスに関する情報を格納するためにメモリの一定量を割り当てます。このメモリは、インスタンスのタイプに関する情報を、そのインスタンスに関連付けられている保存プロパティの値とともに保持します。
さらに、インスタンスが不要になると、ARCはそのインスタンスが使用していたメモリを解放し、代わりにそのメモリを他の目的に使用できるようにします。これにより、クラスインスタンスが不要になったときに、メモリ内のスペースを占有することがなくなります。
ただし、ARCがまだ使用中のインスタンスの割り当てを解除すると、そのインスタンスのプロパティにアクセスしたり、そのインスタンスのメソッドを呼び出したりすることができなくなります。実際、インスタンスにアクセスしようとすると、アプリがクラッシュする可能性が高くなります。
インスタンスがまだ必要なときに消えないようにするために、ARCは、各クラスインスタンスを現在参照しているプロパティ、定数、変数の数を追跡します。 ARCは、インスタンスへのアクティブな参照が少なくとも1つ存在する限り、インスタンスの割り当てを解除しません。
これを可能にするために、クラスインスタンスをプロパティ、定数、または変数に割り当てると、そのプロパティ、定数、または変数はインスタンスを強力に参照します。参照は「強力な」参照と呼ばれます。これは、そのインスタンスをしっかりと保持し、その強力な参照が残っている限り、割り当てを解除できないためです。
ARC in Action
自動参照カウントの仕組みの例を次に示します。 この例は、Personという単純なクラスから始まります。このクラスは、nameという名前の格納された定数プロパティを定義します。
class Person {
let name: String
init(name: String) {
self.name = name
print("\(name) is being initialized")
}
deinit {
print("\(name) is being deinitialized")
}
}
Personクラスには、インスタンスのnameプロパティを設定し、初期化が進行中であることを示すメッセージを出力する初期化子があります。 Personクラスには、クラスのインスタンスの割り当てが解除されたときにメッセージを出力するデイニシャライザーもあります。
次のコードスニペットは、タイプPerson?の3つの変数を定義します。これらの変数は、後続のコードスニペットで新しいPersonインスタンスへの複数の参照を設定するために使用されます。 これらの変数はoptional型(PersonではなくPerson?)であるため、値nilで自動的に初期化され、現在、Personインスタンスを参照していません。
var reference1: Person?
var reference2: Person?
var reference3: Person?
これで、新しいPersonインスタンスを作成し、次の3つの変数のいずれかに割り当てることができます。
reference1 = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized"
Personクラスのイニシャライザを呼び出した時点で、「JohnAppleseedが初期化されています」というメッセージが出力されることに注意してください。 これにより、初期化が行われたことが確認されます。
新しいPersonインスタンスがreference1変数に割り当てられたため、reference1から新しいPersonインスタンスへの強力な参照があります。 少なくとも1つの強力な参照があるため、ARCは、この個人がメモリに保持され、割り当てが解除されないようにします。
同じPersonインスタンスをさらに2つの変数に割り当てると、そのインスタンスへのさらに2つの強力な参照が確立されます。
reference2 = reference1
reference3 = reference1
現在、この単一のPersonインスタンスへの3つの強力な参照があります。
2つの変数にnilを割り当てて、これらの強力な参照の2つ(元の参照を含む)を解除すると、1つの強力な参照が残り、Personインスタンスの割り当てが解除されません。
reference1 = nil
reference2 = nil
ARCは、3番目の最後の強力な参照が解除されるまでPersonインスタンスの割り当てを解除しません。その時点で、Personインスタンスを使用しなくなったことは明らかです。
reference3 = nil
// Prints "John Appleseed is being deinitialized"
Strong Reference Cycles Between Class Instances
上記の例では、ARCは、作成した新しいPersonインスタンスへの参照の数を追跡し、不要になったときにそのPersonインスタンスの割り当てを解除することができます。
ただし、クラスのインスタンスが強力な参照がゼロになるまで到達しないコードを作成することは可能です。これは、2つのクラスインスタンスが相互に強い参照を保持し、各インスタンスが他方を存続させる場合に発生する可能性があります。これは、強力な参照サイクルとして知られています。
クラス間の関係の一部を、強い参照ではなく弱い参照または所有されていない参照として定義することにより、強い参照サイクルを解決します。このプロセスについては、クラスインスタンス間の強力な参照サイクルの解決で説明しています。ただし、強力な参照サイクルを解決する方法を学ぶ前に、そのようなサイクルがどのように発生するかを理解しておくと役立ちます。
これは、誤って強力な参照サイクルを作成する方法の例です。この例では、PersonとApartmentという2つのクラスを定義します。これらは、アパートのブロックとその居住者をモデル化します。
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
すべてのPersonインスタンスには、String型のnameプロパティと、最初はnilであるoptionalのapartmentプロパティがあります。 人が常にアパートを持っているとは限らないのでそのapartmentプロパティはoptionalです。
同様に、すべてのApartmentインスタンスには、String型のunitプロパティがあり、最初はnilであるoptionalのtenantプロパティがあります。 アパートには必ずしもテナントがいるとは限らないため、tenantプロパティはoptionalです。
これらのクラスは両方とも、そのクラスのインスタンスが非初期化されているという事実を出力する非初期化子 deinitも定義します。 これにより、PersonとApartmentのインスタンスが期待どおりに割り当て解除されているかどうかを確認できます。
この次のコードスニペットは、johnとunit4Aと呼ばれるoptionalの2つの変数を定義します。これらは、以下の特定のApartment andPersonインスタンスに設定されます。 これらの変数は両方とも、optionalであるため、初期値はnilです。
var john: Person?
var unit4A: Apartment?
これで、特定のPersonインスタンスとApartmentインスタンスを作成し、これらの新しいインスタンスをjohn変数とunit4A変数に割り当てることができます。
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
これら2つのインスタンスを作成して割り当てた後、強力な参照がどのように表示されるかを次に示します。 john変数には新しいPersonインスタンスへの強い参照があり、unit4A変数には新しいApartmentインスタンスへの強い参照があります。
これで、2つのインスタンスをリンクして、その人にアパートがあり、アパートにテナントがあるようにすることができます。 感嘆符(!)は、johnおよびunit4A optional変数内に格納されているインスタンスをアンラップしてアクセスするために使用されるため、これらのインスタンスのプロパティを設定できることに注意してください。
john!.apartment = unit4A
unit4A!.tenant = john
2つのインスタンスをリンクした後、強力な参照がどのように表示されるかを次に示します。
残念ながら、これら2つのインスタンスをリンクすると、それらの間に強力な参照サイクルが作成されます。 これで、PersonインスタンスはApartmentインスタンスへの強い参照を持ち、ApartmentインスタンスはPersonインスタンスへの強い参照を持ちます。 したがって、john変数とunit4A変数によって保持されている強力な参照を解除しても、参照カウントはゼロに低下せず、インスタンスはARCによって割り当て解除されません。
john = nil
unit4A = nil
これらの2つの変数をnilに設定した場合、どちらのデイニシャライザーも呼び出されなかったことに注意してください。 強力な参照サイクルにより、PersonインスタンスとApartmentインスタンスの割り当てが解除され、アプリでメモリリークが発生するのを防ぎます。
john変数とunit4A変数をnilに設定した後、強力な参照がどのように表示されるかを次に示します。
PersonインスタンスとApartmentインスタンスの間の強力な参照は残り、壊すことはできません。
Resolving Strong Reference Cycles Between Class Instances
Swiftは、クラス型のプロパティを操作するときに、強参照サイクルを解決する2つの方法を提供します。弱参照と所有されていない参照です。
弱い参照と所有されていない参照により、参照サイクル内の1つのインスタンスが、他のインスタンスを強く保持することなく、他のインスタンスを参照できるようになります。 その後、インスタンスは、強力な参照サイクルを作成せずに相互に参照できます。
他のインスタンスの有効期間が短い場合、つまり、他のインスタンスの割り当てを最初に解除できる場合は、弱参照を使用します。 上記のアパートの例では、アパートがその存続期間のある時点でテナントを持たないことが適切であるため、この場合、弱い参照が参照サイクルを中断する適切な方法です。 対照的に、他のインスタンスの存続期間が同じかそれより長い場合は、所有されていない参照を使用します。
Weak References
弱参照は、参照するインスタンスを強力に保持しない参照であるため、ARCが参照されるインスタンスを破棄するのを停止しません。この動作により、参照が強力な参照サイクルの一部になるのを防ぎます。プロパティまたは変数宣言の前にweakキーワードを配置することにより、弱参照を示します。
弱参照は、それが参照するインスタンスを強力に保持しないため、弱参照がまだ参照している間に、そのインスタンスの割り当てが解除される可能性があります。したがって、ARCは、参照するインスタンスの割り当てが解除されると、弱参照を自動的にnilに設定します。また、弱参照では実行時に値をnilに変更できるようにする必要があるため、optional型の定数ではなく変数として常に宣言されます。
他のoptional valueと同じように、弱参照に値が存在するかどうかを確認できます。また、存在しなくなった無効なインスタンスへの参照になってしまうことはありません。
ARCがnilへの弱参照を設定する場合、プロパティオブザーバーは呼び出されません。
以下の例は、上記のPerson and Apartmentの例と同じですが、重要な違いが1つあります。今回は、Apartment型のtenantプロパティが弱参照として宣言されています。
class Person {
let name: String
init(name: String) { self.name = name }
var apartment: Apartment?
deinit { print("\(name) is being deinitialized") }
}
class Apartment {
let unit: String
init(unit: String) { self.unit = unit }
weak var tenant: Person?
deinit { print("Apartment \(unit) is being deinitialized") }
}
2つの変数(johnとunit4A)からの強力な参照と、2つのインスタンス間のリンクは、以前と同じように作成されます。
var john: Person?
var unit4A: Apartment?
john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")
john!.apartment = unit4A
unit4A!.tenant = john
2つのインスタンスをリンクした後の参照の外観は次のとおりです。
Personインスタンスには引き続きApartmentインスタンスへの強い参照がありますが、ApartmentインスタンスにはPersonインスタンスへの弱い参照があります。 これは、john変数が保持している強力な参照をnilに設定して解除すると、Personインスタンスへの強力な参照がなくなることを意味します。
john = nil
// Prints "John Appleseed is being deinitialized"
Personインスタンスへの強い参照がなくなったため、割り当てが解除され、tenantプロパティはnilに設定されます。
Apartmentインスタンスへの唯一の強い参照は、unit4A変数からのものです。 その強い参照を破ると、Apartmentインスタンスへの強い参照はなくなります。
unit4A = nil
// Prints "Apartment 4A is being deinitialized"
Apartmentインスタンスへの強い参照がなくなったため、割り当ても解除されます。
ガベージコレクションを使用するシステムでは、メモリ不足によってガベージコレクションがトリガーされた場合にのみ、強参照のないオブジェクトの割り当てが解除されるため、単純なキャッシュメカニズムを実装するために弱いポインタが使用されることがあります。 ただし、ARCでは、最後の強参照が削除されるとすぐに値の割り当てが解除されるため、弱参照はそのような目的には不適切になります。
Unowned References
弱参照と同様に、所有されていない参照は、それが参照するインスタンスを強力に保持しません。ただし、弱参照とは異なり、他のインスタンスの存続期間が同じか長い場合は、所有されていない参照が使用されます。プロパティまたは変数の宣言の前にunownedキーワードを配置することにより、所有されていない参照を示します。
弱参照とは異なり、所有されていない参照には常に値があります。その結果、値を所有されていないものとしてマークしてもoptionalにはならず、ARCは所有されていない参照の値をnilに設定することはありません。
重要
所有されていない参照は、参照が常に割り当て解除されていないインスタンスを参照していることが確実な場合にのみ使用してください。
インスタンスの割り当てが解除された後で、所有されていない参照の値にアクセスしようとすると、ランタイムエラーが発生します。
次の例では、CustomerとCreditCardの2つのクラスを定義します。これらのクラスは、銀行の顧客とその顧客の可能なクレジットカードをモデル化します。これらの2つのクラスはそれぞれ、他のクラスのインスタンスをプロパティとして格納します。この関係は、強力な参照サイクルを作成する可能性があります。
CustomerとCreditCardの関係は、上記の弱い参照の例で見られるApartmentとPersonの関係とは少し異なります。このデータモデルでは、顧客はクレジットカードを持っている場合と持っていない場合がありますが、クレジットカードは常に顧客に関連付けられます。 CreditCardインスタンスは、それが参照する顧客よりも長生きすることはありません。これを表すために、Customerクラスにはoptionalのcardプロパティがありますが、CreditCardクラスには所有されていない(optionalではない)customerプロパティがあります。
さらに、新しいCreditCardインスタンスは、数値と顧客インスタンスをカスタムCreditCardイニシャライザーに渡すことによってのみ作成できます。これにより、CreditCardインスタンスの作成時に、CreditCardインスタンスに常に顧客インスタンスが関連付けられます。
CreditCardには常に顧客がいるため、強い参照サイクルを回避するために、そのcustomerプロパティを所有されていない参照として定義します。
class Customer {
let name: String
var card: CreditCard?
init(name: String) {
self.name = name
}
deinit { print("\(name) is being deinitialized") }
}
class CreditCard {
let number: UInt64
unowned let customer: Customer
init(number: UInt64, customer: Customer) {
self.number = number
self.customer = customer
}
deinit { print("Card #\(number) is being deinitialized") }
}
CreditCardクラスのnumberプロパティは、IntではなくUInt64の型で定義され、numberプロパティの容量が32ビットシステムと64ビットシステムの両方で16桁のカード番号を格納するのに十分な大きさであることを保証します。
この次のコードスニペットは、特定の顧客への参照を格納するために使用されるjohnと呼ばれるoptionalのCustomer変数を定義します。 この変数は、optionalであるため、初期値はnilです。
var john: Customer?
これで、Customerインスタンスを作成し、それを使用して、新しいCreditCardインスタンスを初期化し、その顧客のカードプロパティとして割り当てることができます。
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
2つのインスタンスをリンクしたので、参照は次のようになります。
CustomerインスタンスにはCreditCardインスタンスへの強い参照があり、CreditCardインスタンスにはCustomerインスタンスへの所有されていない参照があります。
unowned customer referenceは、john変数によって保持されている強力な参照を解除すると、Customerインスタンスへの強力な参照はなくなります。
Customerインスタンスへの強い参照がなくなったため、割り当てが解除されました。 これが発生した後、CreditCardインスタンスへの強い参照はなくなり、割り当ても解除されます。
john = nil
// Prints "John Appleseed is being deinitialized"
// Prints "Card #1234567890123456 is being deinitialized"
上記の最後のコードスニペットは、john変数がnilに設定された後、CustomerインスタンスとCreditCardインスタンスの両方の非初期化子が「deinitialized」メッセージを出力することを示しています。
上記の例は、安全な所有されていない参照を使用する方法を示しています。 Swiftは、パフォーマンス上の理由など、ランタイムの安全性チェックを無効にする必要がある場合に備えて、安全でない所有されていない参照も提供します。 すべての安全でない操作と同様に、あなたはそのコードの安全性をチェックする責任を負います。
unowned(unsafe)と書くことにより、安全でないunowned参照を示します。 参照しているインスタンスの割り当てが解除された後で、安全でない所有されていない参照にアクセスしようとすると、プログラムはインスタンスがあったメモリ位置にアクセスしようとします。これは安全でない操作です。
Unowned Optional References
クラスへのoptionalの参照を所有されていないものとしてマークできます。 ARC ownership model,に関しては、所有されていないoptionalの参照と弱参照の両方を同じコンテキストで使用できます。 違いは、所有されていないオoptionalの参照を使用する場合、それが常に有効なオブジェクトを参照するか、nilに設定されていることを確認する必要があることです。
学校の特定の学部が提供するコースを追跡する例を次に示します。
class Department {
var name: String
var courses: [Course]
init(name: String) {
self.name = name
self.courses = []
}
}
class Course {
var name: String
unowned var department: Department
unowned var nextCourse: Course?
init(name: String, in department: Department) {
self.name = name
self.department = department
self.nextCourse = nil
}
}
Departmentは、部門が提供する各コースへの強い参照を維持します。 ARC ownership modelでは、DepartmentがCourseを所有します。 コースには2つの所有されていない参照があります。1つは学部への参照で、もう1つは学生が受講する必要のある次のコースへの参照です。 コースはこれらのオブジェクトのいずれも所有していません。 すべてのコースは一部の部門の一部であるため、departmentのプロパティはoptionalではありません。 ただし、一部のコースには推奨される後続コースがないため、nextCourseプロパティはoptionalです。
これらのクラスの使用例を次に示します。
let department = Department(name: "Horticulture")
let intro = Course(name: "Survey of Plants", in: department)
let intermediate = Course(name: "Growing Common Herbs", in: department)
let advanced = Course(name: "Caring for Tropical Plants", in: department)
intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]
上記のコードは、部門とその3つのコースを作成します。 イントロコースと中間コースの両方で、nextCourseプロパティに保存されている推奨次のコースがあります。これは、学生がこのコースを完了した後に受講する必要のあるコースへの所有されていないoptionalの参照を維持します。
所有されていないoptionalの参照は、ラップするクラスのインスタンスを強力に保持しないため、ARCによるインスタンスの割り当て解除を妨げることはありません。 所有されていないoptionalの参照がnilになる可能性があることを除いて、ARCでの所有されていない参照と同じように動作します。
optionalではない所有されていない参照と同様に、nextCourseが常に割り当て解除されていないコースを参照するようにする責任があります。 この場合、たとえば、department.coursesからコースを削除するときは、他のコースが持つ可能性のあるそのコースへの参照もすべて削除する必要があります。
optional値の基になる型はOptionalです。これは、Swift標準ライブラリの列挙型です。 ただし、optionalは、値の型を所有されていないものとしてマークできないという規則の例外です。
クラスをラップするoptionalは参照カウントを使用しないため、optionalへの強力な参照を維持する必要はありません。
Unowned References and Implicitly Unwrapped Optional Properties
上記の弱い参照と所有されていない参照の例は、強い参照サイクルを中断する必要がある、より一般的な2つのシナリオをカバーしています。
Person and Apartmentの例は、両方ともnilであることが許可されている2つのプロパティが、強力な参照サイクルを引き起こす可能性がある状況を示しています。このシナリオは、弱参照を使用して解決するのが最適です。
CustomerとCreditCardの例は、ゼロにすることが許可されている1つのプロパティと、ゼロにすることができない別のプロパティが、強力な参照サイクルを引き起こす可能性がある状況を示しています。このシナリオは、所有されていない参照を使用して解決するのが最適です。
ただし、3番目のシナリオがあります。このシナリオでは、両方のプロパティに常に値が必要であり、初期化が完了すると、どちらのプロパティもnilになることはありません。このシナリオでは、一方のクラスの所有されていないプロパティを、もう一方のクラスの暗黙的にラップされていないoptionalのプロパティと組み合わせると便利です。
これにより、初期化が完了すると、参照サイクルを回避しながら、両方のプロパティに直接(optionalのアンラップなしで)アクセスできます。このセクションでは、そのような関係を設定する方法を示します。
以下の例では、CountryとCityの2つのクラスを定義し、それぞれが他のクラスのインスタンスをプロパティとして格納します。このデータモデルでは、すべての国に常に首都が必要であり、すべての都市は常に国に属している必要があります。これを表すために、CountryクラスにはcapitalCityプロパティがあり、Cityクラスにはcountryプロパティがあります。
class Country {
let name: String
var capitalCity: City!
init(name: String, capitalName: String) {
self.name = name
self.capitalCity = City(name: capitalName, country: self)
}
}
class City {
let name: String
unowned let country: Country
init(name: String, country: Country) {
self.name = name
self.country = country
}
}
2つのクラス間の相互依存関係を設定するために、CityのイニシャライザーはCountryインスタンスを取得し、このインスタンスをそのcountryプロパティに格納します。
Cityのイニシャライザーは、Countryのイニシャライザー内から呼び出されます。ただし、Countryのイニシャライザは、Two-Phase Initializationで説明されているように、新しいCountryインスタンスが完全に初期化されるまで、Cityイニシャライザに自分自身を渡すことはできません。
この要件に対処するには、CountryのcapitalCityプロパティを、型注釈(City!)の最後にある感嘆符で示される暗黙的にアンラップされたoptionalのプロパティとして宣言します。つまり、capitalCityプロパティのデフォルト値は他のoptionalと同様にnilですが、「暗黙的にラップされていないoptional」で説明されているように、値をアンラップすることなくアクセスできます。
CapitalCityにはデフォルトのnil値があるため、Countryインスタンスが初期化子内でnameプロパティを設定するとすぐに、新しいCountryインスタンスは完全に初期化されたと見なされます。これは、Countryイニシャライザが、nameプロパティが設定されるとすぐに、暗黙のselfプロパティの参照と受け渡しを開始できることを意味します。したがって、Countryイニシャライザーは、Countryイニシャライザーが独自のcapitalCityプロパティを設定しているときに、Cityイニシャライザーのパラメーターの1つとしてselfを渡すことができます。
つまり、強力な参照サイクルを作成せずに、1つのステートメントでCountryインスタンスとCityインスタンスを作成でき、感嘆符を使用してoptionalの値をアンラップすることなく、capitalCityプロパティに直接アクセスできます。
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// Prints "Canada's capital city is called Ottawa"
上記の例では、暗黙的にアンラップされたoptionalを使用することは、2フェーズクラスの初期化子の要件がすべて満たされていることを意味します。 CapitalCityプロパティは、強力な参照サイクルを回避しながら、初期化が完了すると、optionalではない値のように使用およびアクセスできます。
Strong Reference Cycles for Closures
上記で、2つのクラスインスタンスプロパティが相互に強い参照を保持している場合に、強い参照サイクルを作成する方法を説明しました。また、弱い参照と所有されていない参照を使用して、これらの強い参照サイクルを中断する方法も確認しました。
クラスインスタンスのプロパティにクロージャを割り当て、そのクロージャの本体がインスタンスをキャプチャする場合にも、強力な参照サイクルが発生する可能性があります。このキャプチャは、クロージャーの本体がself.somePropertyなどのインスタンスのプロパティにアクセスするため、またはクロージャーがself.someMethod()などのインスタンスのメソッドを呼び出すために発生する可能性があります。いずれの場合も、これらのアクセスにより、クロージャーは自己を「キャプチャ」し、強力な参照サイクルを作成します。
この強力な参照サイクルは、クラスと同様にクロージャが参照型であるために発生します。プロパティにクロージャを割り当てると、そのクロージャへの参照が割り当てられます。本質的に、それは上記と同じ問題です。2つの強力な参照がお互いを生かし続けています。ただし、2つのクラスインスタンスではなく、今回はクラスインスタンスとクロージャが相互に存続しています。
Swiftは、クロージャキャプチャリストと呼ばれる、この問題に対する洗練されたソリューションを提供します。ただし、クロージャキャプチャリストを使用して強力な参照サイクルを中断する方法を学習する前に、そのようなサイクルがどのように発生するかを理解しておくと役立ちます。
以下の例は、自己を参照するクロージャを使用するときに強力な参照サイクルを作成する方法を示しています。この例では、HTMLElementと呼ばれるクラスを定義します。これは、HTMLドキュメント内の個々の要素の単純なモデルを提供します。
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) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
HTMLElementクラスは、nameプロパティを定義します。これは、見出し要素の場合は「h1」、段落要素の場合は「p」、改行要素の場合は「br」など、要素の名前を示します。 HTMLElementは、optionalのtextプロパティも定義します。これは、そのHTML要素内でレンダリングされるテキストを表す文字列に設定できます。
これらの2つの単純なプロパティに加えて、HTMLElementクラスはasHTMLと呼ばれる遅延プロパティを定義します。このプロパティは、名前とテキストをHTML文字列フラグメントに結合するクロージャを参照します。 asHTMLプロパティは、タイプ()-> String、または「パラメーターを受け取らず、String値を返す関数」です。
デフォルトでは、asHTMLプロパティには、HTMLタグの文字列表現を返すクロージャが割り当てられています。このタグには、optionalのテキスト値が存在する場合はそれが含まれ、テキストが存在しない場合はテキストコンテンツが含まれません。段落要素の場合、クロージャは、textプロパティが「sometext」またはnilのどちらに等しいかに応じて、「<p> some text </ p>」または「<p />」を返します。
asHTMLプロパティは、インスタンスメソッドのように名前が付けられ、使用されます。ただし、asHTMLはインスタンスメソッドではなくクロージャプロパティであるため、特定のHTML要素のHTMLレンダリングを変更する場合は、asHTMLプロパティのデフォルト値をカスタムクロージャに置き換えることができます。
たとえば、表現が空のHTMLタグを返さないようにするために、asHTMLプロパティをクロージャに設定して、textプロパティがnilの場合、デフォルトで一部のテキストを設定できます。
let heading = HTMLElement(name: "h1")
let defaultText = "some default text"
heading.asHTML = {
return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// Prints "<h1>some default text</h1>"
asHTMLプロパティは、要素が実際にHTML出力ターゲットの文字列値としてレンダリングされる必要がある場合にのみ必要になるため、遅延プロパティとして宣言されます。 asHTMLがレイジープロパティであるという事実は、初期化が完了してセルフが存在することが判明するまでレイジープロパティにアクセスされないため、デフォルトのクロージャー内でセルフを参照できることを意味します。
HTMLElementクラスは、名前引数と(必要に応じて)テキスト引数を取り、新しい要素を初期化する単一の初期化子を提供します。 このクラスは、HTMLElementインスタンスの割り当てが解除されたときに表示するメッセージを出力するデイニシャライザーも定義します。
HTMLElementクラスを使用して新しいインスタンスを作成およびprint出力する方法は次のとおりです。
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
上記の段落変数はoptionalのHTMLElementとして定義されているため、以下ではnilに設定して、強力な参照サイクルの存在を示すことができます。
残念ながら、上記のHTMLElementクラスは、HTMLElementインスタンスと、デフォルトのasHTML値に使用されるクロージャーとの間に強力な参照サイクルを作成します。 サイクルは次のようになります。
インスタンスのasHTMLプロパティは、そのクロージャへの強い参照を保持しています。 ただし、クロージャは(self.nameとself.textを参照する方法として)本体内のselfを参照するため、クロージャはselfをキャプチャします。これは、HTMLElementインスタンスへの強力な参照を保持することを意味します。 2つの間に強力な参照サイクルが作成されます。
クロージャーは自己を複数回参照しますが、HTMLElementインスタンスへの1つの強力な参照のみをキャプチャします。
段落変数をnilに設定し、HTMLElementインスタンスへの強い参照を解除すると、強い参照サイクルのために、HTMLElementインスタンスもそのクロージャーも割り当てが解除されません。
paragraph = nil
HTMLElementデイニシャライザーのメッセージは出力されないことに注意してください。これは、HTMLElementインスタンスの割り当てが解除されていないことを示しています。
Resolving Strong Reference Cycles for Closures
クロージャの定義の一部としてキャプチャリストを定義することにより、クロージャとクラスインスタンス間の強力な参照サイクルを解決します。 キャプチャリストは、クロージャの本体内で1つ以上の参照型をキャプチャするときに使用するルールを定義します。 2つのクラスインスタンス間の強い参照サイクルと同様に、キャプチャされた各参照は、強い参照ではなく、弱い参照または所有されていない参照として宣言します。 弱いか所有されていないかの適切な選択は、コードのさまざまな部分間の関係によって異なります。
Swiftでは、クロージャ内でselfのメンバーを参照する場合は常に、self.somePropertyまたはself.someMethod()(somePropertyまたはsomeMethod()だけでなく)を記述する必要があります。 これは、偶然に自分を捕まえる可能性があることを思い出すのに役立ちます。
Defining a Capture List
キャプチャリストの各項目は、弱いキーワードまたは所有されていないキーワードと、クラスインスタンス(selfなど)または何らかの値で初期化された変数(delegate = self.delegateなど)への参照とのペアです。 これらのペアは、コンマで区切られた1対の四角い中括弧内に記述されます。
クロージャのパラメータリストの前にキャプチャリストを配置し、提供されている場合は型を返します。
lazy var someClosure = {
[unowned self, weak delegate = self.delegate]
(index: Int, stringToProcess: String) -> String in
// closure body goes here
}
コンテキストから推測できるため、クロージャでパラメータリストまたはリターン型が指定されていない場合は、クロージャの最初にキャプチャリストを配置し、その後にinキーワードを続けます。
lazy var someClosure = {
[unowned self, weak delegate = self.delegate] in
// closure body goes here
}
Weak and Unowned References
クロージャとそれがキャプチャするインスタンスが常に相互に参照し、常に同時に割り当てが解除される場合、クロージャ内のキャプチャを所有されていない参照として定義します。
逆に、キャプチャされた参照が将来のある時点でゼロになる可能性がある場合は、キャプチャを弱い参照として定義します。 弱参照は常にoptionalの型であり、参照するインスタンスの割り当てが解除されると自動的にnilになります。 これにより、クロージャーの本体内にそれらが存在するかどうかを確認できます。
キャプチャされた参照がnilにならない場合は、弱い参照ではなく、常に所有されていない参照としてキャプチャする必要があります。
所有されていない参照は、上記のクロージャの強力な参照サイクルからのHTMLElementの例の強力な参照サイクルを解決するために使用する適切なキャプチャ方法です。 サイクルを回避するためにHTMLElementクラスを作成する方法は次のとおりです。
class HTMLElement {
let name: String
let text: String?
lazy var asHTML: () -> String = {
[unowned self] in
if let text = self.text {
return "<\(self.name)>\(text)</\(self.name)>"
} else {
return "<\(self.name) />"
}
}
init(name: String, text: String? = nil) {
self.name = name
self.text = text
}
deinit {
print("\(name) is being deinitialized")
}
}
このHTMLElementの実装は、asHTMLクロージャー内にキャプチャリストが追加されていることを除けば、前の実装と同じです。 この場合、キャプチャリストは[unowned self]です。これは、「強い参照ではなく、所有されていない参照として自己をキャプチャする」ことを意味します。
以前と同じように、HTMLElementインスタンスを作成して印刷できます。
var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// Prints "<p>hello, world</p>"
キャプチャリストを配置した場合の参照の外観は次のとおりです。
今回は、クロージャーによる自己のキャプチャは所有されていない参照であり、キャプチャしたHTMLElementインスタンスを強力に保持していません。 段落変数からの強い参照をnilに設定すると、以下の例の非初期化メッセージの出力からわかるように、HTMLElementインスタンスの割り当てが解除されます。
paragraph = nil
// Prints "p is being deinitialized"
この記事が気に入ったらサポートをしてみませんか?