自動参照カウント ARC(Automatic Reference Counting)

最終更新日: 2023/9/24 原文: https://docs.swift.org/swift-book/LanguageGuide/AutomaticReferenceCounting.html

オブジェクトの存続期間とオブジェクト同士の関係をモデル化する。

Swift は自動参照カウント(以下 ARC )を使用して、アプリのメモリ使用状況を追跡および管理します。ほとんどの場合、これはメモリ管理が Swift によって「ただ行われている」ことを意味し、メモリ管理について自身で考える必要はありません。ARC は、クラスインスタンスが不要になったときに、クラスインスタンスによって使用されていたメモリを自動的に解放します。

ただし、場合によっては、ARC がメモリを管理するために、各コード間の関係についてより多くの情報を必要とすることがあります。この章では、これらの状況について説明し、ARC でアプリの全てのメモリを管理できるようにする方法を示します。Swift での ARC の使用は、Transitioning to ARC Release Notesで説明されているアプローチと非常によく似ています。

参照カウントは、クラスインスタンスにのみ適用されます。構造体と列挙型は、参照型ではなく値型なので、参照が格納および渡されることはありません。

ARCはどう機能するのか(How ARC Works)

クラスの新しいインスタンスを作成するたびに、ARC はそのインスタンスに関する情報を格納するためにメモリの一部を割り当てます。このメモリには、インスタンスの型に関する情報と、そのインスタンスに関連付けられた格納プロパティの値が保持されます。

さらに、インスタンスが不要になった場合、ARC はそのインスタンスが使用していたメモリを解放し、代わりにメモリを他の目的に使用できるようにします。これにより、クラスインスタンスが不要になったときにメモリ内のスペースを占有しないようにします。

ただし、ARC がまだ使用中のインスタンスの割り当てを解除すると、そのインスタンスのプロパティにアクセスすることも、そのインスタンスのメソッドを呼び出すこともできなくなります。実際、インスタンスにアクセスしようとすると、アプリがクラッシュする可能性が高くなります。

インスタンスがまだ必要なときにインスタンスが消えないようにするために、ARC は各クラスインスタンスを現在参照しているプロパティ、定数、変数の数を追跡します。そのインスタンスへのアクティブな参照が少なくとも 1 つ存在する限り、ARC はインスタンスの割り当てを解除しません。

割り当ての解除を防ぐために、クラスインスタンスをプロパティ、定数、または変数に割り当てると、そのプロパティ、定数、または変数はインスタンスへの強参照を作成します。参照は、強参照が残っている限り割り当てを解除できないため、「強い」参照と呼ばれています。

ARCの挙動(ARC in Action)

ARC がどのように動くのかの例を次に示します。この例は、name という名前の定数格納プロパティを定義する Person というシンプルなクラスから始まります。

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) の初期化が進行中です")
    }
    deinit {
        print("\(name) のインスタンス割り当てが解除されました")
    }
}

Person クラスには、インスタンスの name プロパティを設定し、初期化が進行中だということを示すメッセージを出力するイニシャライザがあります。インスタンスの割り当てが解除されたときにメッセージを出力するデイニシャライザもあります。

次のコードスニペットでは、Person? 型の 3 つの変数を定義しています。これらの変数は、後で出てくるコードスニペットで新しい Person インスタンスへの複数の参照を設定するために使用されます。これらの変数はオプショナルの型(Person?Person ではない)であるため、nil で自動的に初期化され、現在 Person インスタンスを参照していません。

var reference1: Person?
var reference2: Person?
var reference3: Person?

新しい Person インスタンスを作成して、これら 3 つの変数のいずれかに割り当てることができます。

reference1 = Person(name: "John Appleseed")
// John Appleseed の初期化が進行中です

Person クラスのイニシャライザを呼び出した時点で、"John Appleseed の初期化が進行中です" というメッセージが出力されることに注目してください。これは、初期化が行われたことを確認します。

新しい Person インスタンスが reference1 変数に割り当てられているため、reference1 から新しい Person インスタンスへの強参照があります。少なくとも 1 つの強参照があるため、ARC はこの Person をメモリに保持し続け、割り当てが解除されないようにします。

同じ Person インスタンスをさらに 2 つの変数に割り当てると、そのインスタンスへのさらに 2 つの強参照ができます。

reference2 = reference1
reference3 = reference1

現在、この単一の Person インスタンスへ 3 つの強参照があります。

2 つの変数に nil を代入してこれらの 2 つの強参照(元の参照を含む)を解除すると、1 つの強参照が残り、Person インスタンスの割り当てが解除されません。

reference1 = nil
reference2 = nil

ARC は、最後の 3 番目の強参照がなくなり、Person インスタンスを使用していないことが明らかになる時点まで、Person インスタンスの割り当てを解除しません。

reference3 = nil
// John Appleseed のインスタンス割り当てが解除されました

クラスインスタンス間の強循環参照(Strong Reference Cycles Between Class Instances)

上記の例では、ARC は、作成した新しい Person インスタンスへの参照の数を追跡し、不要になったらその Person インスタンスの割り当てを解除できます。

ただし、クラスインスタンスの強参照がゼロにならないコードを書いてしまう可能性があります。これは、2 つのクラスインスタンスが互いに強参照を保持している場合に発生する可能性があります。これは、強循環参照と呼ばれています。

クラス間の関係の一部を強参照ではなく、弱参照または非所有参照として定義することにより、強循環参照を解決できます。このプロセスは、Resolving Strong Reference Cycles Between Class Instances(クラスインスタンス間の強循環参照の解消)で説明されています。ただし、強循環参照を解決する方法を学ぶ前に、そのような循環がどのように発生するかを理解しておくと役に立ちます。

強循環参照が偶発的に発生する例を次に示します。この例では、PersonApartment という 2 つのクラスを定義しており、アパートのブロックとその住人をモデル化しています。

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) のインスタンス割り当てが解除されました") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    var tenant: Person?
    deinit { print("アパート \(unit) のインスタンス割り当てが解除されました") }
}

全ての Person インスタンスには、String 型の name プロパティと、最初は nil のオプショナルの apartment プロパティがあります。人は常にアパートを所有しているとは限らないため、apartment のプロパティはオプショナルです。

同様に、全ての Apartment インスタンスには String 型の unit プロパティがあり、最初は nil のオプショナルの tenant プロパティがあります。アパートには常にテナントがあるとは限らないため、tenant プロパティはオプショナルです。

これらのクラスは両方とも、そのクラスのインスタンスがメモリから割り当て解除されていることを出力するデイニシャライザ(deinit)も定義しています。これにより、PersonApartment のインスタンスが期待どおりに割り当て解除されているかどうかを確認できます。

この次のコードスニペットは、johnunit4A と呼ばれるオプショナルの型の 2 つの変数を定義してます。これらは、下記の特定の Apartment および Person インスタンスに設定されています。これらの変数は両方とも、オプショナルなため、初期値は 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 変数内に格納されているインスタンスをアンラップしてアクセスするために使用され、これらのインスタンスプロパティに値を設定できます:

john!.apartment = unit4A
unit4A!.tenant = john

2 つのインスタンスをリンクした後、強参照がどのように見えるかを次に示します。

循環参照 2 つのインスタンスをリンクした後の強参照

残念ながら、これら 2 つのインスタンスをリンクすると、それらの間に強循環参照ができます。Person インスタンスは Apartment インスタンスへの強参照を持ち、Apartment インスタンスは Person インスタンスへの強参照を持つようになりました。したがって、john 変数と unit4A 変数で保持されている強参照を解除しても、参照カウントはゼロにはならず、インスタンスは ARC によって割り当て解除されません。

john = nil
unit4A = nil

これら 2 つの変数を nil に設定したときには、どちらのデイニシャライザも呼び出されなかったことに注目してください。強循環参照により、Person インスタンスと Apartment インスタンスの割り当てが解除されることがなくなり、アプリでメモリリークが発生します。

john 変数と unit4A 変数を nil に設定した後、強参照がどのように見えるかを次に示します。

循環参照 nil 設定後の強参照

Person インスタンスと Apartment インスタンス間の強参照は残り、なくなることはありません。

クラスインスタンス間の強循環参照の解消(Resolving Strong Reference Cycles Between Class Instances)

Swift は、クラス型のプロパティを操作するときに強循環参照を解決する 2 つの方法を提供します。弱参照(weak reference)と非所有参照(unowned reference)です。

弱参照と非所有参照により、循環参照の 1 つのインスタンスは、強参照を維持することなく、他のインスタンスを参照できます。その後、インスタンスは強循環参照を作成せずに相互に参照できます。

他のインスタンスの有効期間が短い場合、つまり、他のインスタンスの割り当てを最初に解除できる場合は、弱参照を使用します。上記の Apartment の例では、アパートがその存続期間のある時点でテナントが存在しないこともあるため、この場合、弱参照が循環参照を断ち切る適切な方法です。対照的に、他のインスタンスの存続期間が同じか、それよりも長い場合は、非所有参照を使用します。

弱参照(Weak References)

弱参照は、参照するインスタンスを強く保持せず、 ARC がインスタンスを破棄することを防ぎません。この動作により、参照が強循環参照の一部になるのを防ぎます。プロパティまたは変数宣言の前に weak キーワードを配置することにより、弱参照を示します。

弱参照は参照するインスタンスを強く保持しないため、弱参照が参照している間にそのインスタンスの割り当てが解除される可能性があります。したがって、ARC は、参照するインスタンスの割り当てが解除されると、自動的に弱参照を nil に設定します。また、弱参照は実行時に値を nil に変更できるようにする必要があるため、常にオプショナル型の、定数ではなく変数として宣言されます。

他のオプショナル値と同様に、弱参照内の値の存在を確認できます。存在しない無効なインスタンスへの参照で終わることはありません。

NOTE ARC が弱参照を nil に設定した場合、プロパティオブザーバは呼び出されません。

下記の例は、上記の PersonApartment の例と同じですが、1 つの重要な違いがあります。今回は、Apartment 型の tenant プロパティが弱参照として宣言されています:

class Person {
    let name: String
    init(name: String) { self.name = name }
    var apartment: Apartment?
    deinit { print("\(name) のインスタンス割り当てが解除されました") }
}

class Apartment {
    let unit: String
    init(unit: String) { self.unit = unit }
    weak var tenant: Person?
    deinit { print("アパート \(unit) のインスタンス割り当てが解除されました") }
}

2 つの変数(johnunit4A)からの強参照と、2 つのインスタンス間のリンクは、以前と同じように作成されます。

var john: Person?
var unit4A: Apartment?

john = Person(name: "John Appleseed")
unit4A = Apartment(unit: "4A")

john!.apartment = unit4A
unit4A!.tenant = john

2 つのインスタンスをリンクしたので、参照は次のようになります:

弱参照 2 つのインスタンスをリンク

Person インスタンスは依然として Apartment インスタンスへの強参照を持っていますが、Apartment インスタンスは Person インスタンスへの弱参照を持っています。これは、john 変数を nil に設定してその強参照を解除すると、Person インスタンスへの強参照がなくなることを意味します。

john = nil
// John Appleseed のインスタンス割り当てが解除されました

Person インスタンスへの強参照がなくなったため、割り当てが解除され、tenant プロパティが nil に設定されます:

弱参照 割り当ての解除

Apartment インスタンスへの唯一の強参照は、unit4A 変数です。その強参照がなくなると、Apartment インスタンスへの強参照はなくなります。

unit4A = nil
// アパート 4A のインスタンス割り当てが解除されました

Apartment インスタンスへの強参照がなくなったため、この割り当ても解除されます:

弱参照 Apartment インスタンスの割り当て解除

NOTE ガベージコレクションを使用するシステムでは、メモリプレッシャによってガベージコレクションがトリガされた場合にのみ、 強参照を持たないオブジェクトの割り当てが解除されるため、シンプルなキャッシュメカニズムを実装するために弱いポインタが使用されることがあります。ただし、ARC では、最後の強参照が削除されるとすぐに値の割り当てが解除されるため、弱参照はそのような目的には適していません。

非所有参照(Unowned References)

弱参照と同様に、非所有参照は、インスタンスを強参照しません。ただし、弱参照とは異なり、非所有参照は、他のインスタンスの存続期間が同じか、存続期間がより長い場合に使用します。プロパティまたは変数の宣言の前に unowned キーワードを配置することにより、非所有参照を示します。弱参照とは異なり、非所有参照は常に値を持つことが期待されます。その結果、値を unowned としてマークしてもオプショナルにはなりません。また、ARC は非所有参照の値を nil に設定することはありません。

NOTE 割​​り当て解除されないインスタンスを参照していることが確実な場合にのみ、非所有参照を使用してください。

インスタンスの割り当てが解除された後に非所有参照の値にアクセスしようとすると、実行時エラーが発生します。

次の例では、2 つのクラス、CustomerCreditCard を定義しています。これらのクラスは、銀行の顧客と、その顧客のクレジットカードをモデル化しています。これら 2 つのクラスはそれぞれ、もう一方のクラスのインスタンスをプロパティとして保持しています。この関係は、強循環参照を生む可能性があります。

CustomerCreditCard の関係は、上記の弱参照の例に見られる ApartmentPerson の関係とは少し異なります。このデータモデルでは、顧客はクレジットカードを持っている場合と持っていない場合がありますが、クレジットカードは常に顧客に関連付けられます。CreditCard インスタンスは、それが参照する顧客より長生きすることはありません。これを表すために、Customer クラスにはオプショナルの card プロパティがありますが、CreditCard クラスには(オプショナルではない)非所有(unowned)の顧客プロパティがあります。

さらに、新しい CreditCard インスタンスは、数値と顧客インスタンスを独自の CreditCard イニシャライザに渡すことによってのみ作成できます。これにより、CreditCard インスタンスの作成時に、CreditCard インスタンスに常に顧客インスタンスが関連付けられます。

クレジットカードには常に顧客がいるため、強循環参照を避けるために、その顧客プロパティを非所有参照として定義します:

class Customer {
    let name: String
    var card: CreditCard?
    init(name: String) {
        self.name = name
    }
    deinit { print("\(name) のインスタンス割り当てが解除されました") }
}

class CreditCard {
    let number: UInt64
    unowned let customer: Customer
    init(number: UInt64, customer: Customer) {
        self.number = number
        self.customer = customer
    }
    deinit { print("カード番号 #\(number) のインスタンス割り当てが解除されました") }
}

NOTE CreditCard クラスの number プロパティは、Int ではなく UInt64 の型で定義され、number プロパティの容量は 32 ビットと 64 ビットの両方のシステムで 16 桁のカード番号を格納するのに十分な大きさです。

次のコードスニペットは、john というオプショナルの Customer 変数を定義します。この変数は、特定の顧客への参照を保存するために使用されます。この変数はオプショナルなので、初期値は nil です:

var john: Customer?

これで Customer インスタンスを作成し、それを使用して新しい CreditCard インスタンスを初期化し、その顧客の card プロパティとして割り当てることができます:

john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)

2 つのインスタンスをリンクしたので、参照は次のようになります:

非所有参照 2 つのインスタンスをリンク

Customer インスタンスには、CreditCard インスタンスへの強参照があり、CreditCard インスタンスには、Customer インスタンスへの非所有参照があります。

customer は非所有参照なので、john 変数によって保持されている強参照を解除すると、Customer インスタンスへの強参照はなくなります:

非所有参照 \`Customer\` インスタンスへの強参照の解除

Customer インスタンスへの強参照がなくなったため、割り当てが解除されます。これが発生すると、CreditCard インスタンスへの強参照もなくなり、割り当ても解除されます。

john = nil
// John Appleseed のインスタンス割り当てが解除されました
// カード番号 #1234567890123456 のインスタンス割り当てが解除されました

上記の最後のコードスニペットは、john 変数が nil に設定された後、Customer インスタンスと CreditCard インスタンスのデイニシャライザが両方の"deinitialized"メッセージを出力することを示しています。

NOTE 上記の例は、安全な非所有参照の使用方法を示しています。Swift は、パフォーマンス上の理由などで、ランタイムの安全性チェックを無効にする必要がある場合に、安全でない非所有参照も提供します。他の全ての安全でない操作と同様、そのコードの安全性をチェックする責任はあなたにあります。

unowned(unsafe) と書くことで、安全でない非所有参照を示します。参照しているインスタンスの割り当てが解除された後で、所有されていない安全でない参照にアクセスしようとすると、プログラムはインスタンスがかつて存在していたメモリアドレスにアクセスしようとしますが、これは安全ではありません。

オプショナル値への非所有参照(Unowned Optional References)

クラスへのオプショナルの参照を unowned とマークできます。ARC 所有権モデルの観点から言うと、オプショナルの非所有参照と弱参照は共に同じコンテキストで使用できます。弱参照との違いは、オプショナルの非所有参照を使用する場合、いつアクセスしてもクラッシュしないように、それが有効なオブジェクトを参照しているか、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 所有権モデルでは、学科がそのコースを所有します。Course には非所有参照が 2 つあります。1 つは学科へ、もう 1 つは学生が受講する必要がある次のコースです。コースはこれらのオブジェクトのいずれも所有しません。全てのコースはある学科の一部であるため、department プロパティはオプショナルではありません。ただし、一部のコースには推奨される次のコースがないため、nextCourse プロパティはオプショナルです。

これらのクラスの使用例を次に示します:

let department = Department(name: "園芸学")

let intro = Course(name: "植物調査", in: department)
let intermediate = Course(name: "一般的なハーブ栽培", in: department)
let advanced = Course(name: "熱帯植物の育て方", in: department)

intro.nextCourse = intermediate
intermediate.nextCourse = advanced
department.courses = [intro, intermediate, advanced]

上記のコードは、学科とその 3 つのコースを作成します。入門コースと中級コースの両方には、nextCourse プロパティに次のコースの提案があり、このコースを完了した後に学生が受講する必要があるコースへのオプショナルの非所有参照が保持されます。

オプショナル値への非所有参照 参照の保持

オプショナル値への非所有参照は、ラップするクラスのインスタンスを強く保持しないため、ARC によるインスタンスの割り当て解除を妨げません。これは、オプショナル値への非所有参照が nil にできることを除いて、ARC での非所有参照と同じように動作します。

オプショナル値への非所有参照と同様に、nextCourse が常に割り当てが解除されていないコースを参照するようにする必要があります。この場合、例えば、department.courses からコースを削除するときに、他のコースが持つ可能性のあるそのコースへの参照も全て削除する必要があります。

NOTE オプショナル値の基になる型は Optional です。これは、Swift 標準ライブラリの列挙型です。Optional は、値型を unowned でマークできないという規則の例外です。

クラスをラップするオプショナルは ARC を使用しないため、オプショナルへの強参照を維持する必要はありません。

非所有参照と暗黙アンラップしたオプショナルプロパティ(Unowned References and Implicitly Unwrapped Optional Properties)

上記の弱参照と非所有参照の例は、強循環参照を断ち切る必要がある、より一般的な 2 つのシナリオをカバーしています。

PersonApartment の例は、両方とも nil にすることができる 2 つのプロパティが、強循環参照を引き起こす可能性がある状況を示しています。このシナリオは、弱参照で解決するのが最善です。

CustomerCreditCard の例は、nil にできる 1 つのプロパティと nil にできない別のプロパティが、強循環参照を引き起こす可能性がある状況を示しています。このシナリオは、非所有参照で解決するのが最善です。

ただし、3 番目のシナリオでは、両方のプロパティに常に値が必要で、初期化が完了すると、どちらのプロパティも nil にはできません。このシナリオでは、一方のクラスに unowned プロパティ、もう一方のクラスに暗黙アンラップオプショナルプロパティを使用すると便利です。

これにより、初期化が完了すると、循環参照を回避しながら、両方のプロパティに直接(オプショナルのアンラップなしで)アクセスできます。このセクションでは、そのような関係を設定する方法を示します。

下記の例では、CountryCity の 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 のイニシャライザ内から呼び出されます。ただし、Two-Phase Initialization(2 段階の初期化)で説明されているように、新しい Country インスタンスが完全に初期化されるまで、Country のイニシャライザは selfCity イニシャライザに渡すことはできません。

この要件に対処するには、CountrycapitalCity プロパティを、型注釈の最後に感嘆符で示される、暗黙アンラップオプショナルプロパティ(City!)として宣言しています。これは、他のオプショナルと同様に、capitalCity プロパティのデフォルト値は nil ですが、Implicitly Unwrapped Optionals(暗黙アンラップオプショナル)で説明されているように、その値をアンラップする必要なくアクセスできることを意味します。

capitalCity にはデフォルトの nil 値があるため、新しい Country インスタンスは、イニシャライザ内で name プロパティを設定するとすぐに完全に初期化されたと見なされます。これは、name プロパティが設定されるとすぐに、Country イニシャライザが暗黙の self プロパティの参照の受け渡しを開始できることを意味します。そして、Country イニシャライザが自身の capitalCity プロパティを設定するときに、selfCity イニシャライザのパラメータの 1 つとして渡すことができます。

こうすることで、強循環参照を作成せずに単一の文で CountryCity のインスタンスを作成できることを意味します。また、オプショナル値をアンラップするために感嘆符を使用する必要もなく、capitalCity プロパティに直接アクセスできます:

var country = Country(name: "カナダ", capitalName: "オタワ")
print("\(country.name) の首都は \(country.capitalCity.name) です")
// カナダ の首都は オタワ です

上記の例では、暗黙アンラップオプショナルを使用することは、2 段階のクラスイニシャライザの要件が全て満たされることを意味します。capitalCity プロパティは、初期化が完了すると、強循環参照を回避しつつ、非オプショナル値のように使用およびアクセスできます。

クロージャの強循環参照(Strong Reference Cycles for Closures)

2 つのクラスのインスタンスプロパティが互いに強参照を保持している場合に、強循環参照がどのように作成されるかを上で説明しました。また、弱参照と非所有参照を使用して、これらの強循環参照を破る方法についても説明しました。

クラスのインスタンスプロパティにクロージャを割り当て、そのクロージャの本文がインスタンスをキャプチャした場合にも、強循環参照が発生する可能性があります。このキャプチャは、クロージャの本文がインスタンスのプロパティ(self.someProperty など) にアクセスした場合、またはクロージャがインスタンスのメソッド(self.someMethod() など)を呼び出した場合に発生する可能性があります。いずれの場合も、これらのアクセスにより、クロージャが self を「キャプチャ」し、強循環参照が作成されます。

この強循環参照が発生するのは、クロージャがクラスと同様に参照型であるためです。プロパティにクロージャを割り当てると、そのクロージャへの参照が割り当てられます。本質的に、これは上記と同じ問題です。2 つの強参照がお互いを生かし続けてしまいます。ただし、2 つのクラスインスタンスではなく、今回はクラスインスタンスとクロージャがお互いを生かし続けています。

Swift は、クロージャキャプチャリストと呼ばれるこの問題に対するエレガントなソリューションを提供します。ただし、クロージャキャプチャリストを使用して強循環参照を中断する方法を学ぶ前に、そのような循環がどのように発生するのかを理解しておくと役立ちます。

下記の例は、self を参照するクロージャを使用する場合に強循環参照を作成する方法を示しています。この例では、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) のインスタンス割り当てが解除されました")
    }

}

HTMLElement クラスは、見出し要素の場合は "h1"、段落要素の場合は "p"、改行要素の場合は "br" など、要素の名前を示す name プロパティを定義しています。HTMLElement は、オプショナルの text プロパティも定義しています。このプロパティは、その HTML 要素内でレンダリングされるテキストを表す文字列に設定できます。

これら 2 つのシンプルなプロパティに加えて、HTMLElement クラスは asHTML と呼ばれる遅延プロパティを定義しています。このプロパティは、nametext を HTML 文字列フラグメントに結合するクロージャを参照しています。asHTML プロパティの型は () -> String、または「パラメータを受け取らず、String 値を返す関数」です。

デフォルトでは、asHTML プロパティには、HTML タグの文字列表現を返すクロージャが割り当てられます。このタグには、(存在する場合)オプショナルの text 値が含まれます。text が存在しない場合、テキストコンテンツは含まれません。段落要素の場合、クロージャは text プロパティが "some text"nil かに応じて、"<p>some text</p>" または "<p />" を返します。

asHTML プロパティには名前が付けられ、インスタンスメソッドのように使用されます。ただし、asHTML はインスタンスメソッドではなくクロージャプロパティであるため、特定の HTML 要素の HTML レンダリングを変更する場合は、asHTML プロパティのデフォルト値を独自のクロージャに置き換えることができます。

例えば、表現が空の HTML タグを返さないようにするために、asHTML プロパティは、text プロパティが nil の場合にデフォルトで何らかのテキストになるクロージャを設定できます。

let heading = HTMLElement(name: "h1")
let defaultText = "デフォルトテキスト"
heading.asHTML = {
    return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
}
print(heading.asHTML())
// <h1>デフォルトテキスト</h1>

NOTE asHTML プロパティは遅延プロパティとして宣言されています。これは、要素が実際に HTML の出力ターゲットの文字列値の一部としてレンダリングされる場合にのみ必要になるためです。asHTML が遅延プロパティであるということは、デフォルトクロージャ内で self を参照できることを意味します。これは、初期化が完了し、self が存在することがわかるまで遅延プロパティにアクセスできないためです。

HTMLElement クラスは単一のイニシャライザを提供します。このイニシャライザは、新しい要素を初期化するための name 引数と(必要に応じて)text 引数を受け取ります。このクラスは、HTMLElement インスタンスの割り当てが解除されたときに表示するメッセージを出力するデイニシャライザも定義しています。

HTMLElement クラスを使用して新しいインスタンスを作成および出力する方法は次のとおりです:

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// <p>hello, world</p>

NOTE 上記の paragraph 変数はオプショナルの HTMLElement として定義されているため、下記では nil に設定して、強循環参照の存在を示すことができます。

残念ながら、上記の HTMLElement クラスは、HTMLElement インスタンスと、デフォルトの asHTML 値に使用されるクロージャとの間に強循環参照を作成します。循環は次のようになります:

クロージャの循環参照 強参照循環

インスタンスの asHTML プロパティは、そのクロージャへの強参照を保持します。ただし、クロージャは(self.name および self.text を参照する方法として)本文内の self を参照するため、クロージャは self をキャプチャします。つまり、クロージャは HTMLElement インスタンスへの強参照を保持します。2 つの間に強循環参照が作成されます。(クロージャでの値のキャプチャの詳細については、Capturing Values(値のキャプチャ)を参照してください)

NOTE クロージャは self を複数回参照していますが、HTMLElement インスタンスへの強参照は 1 つだけです。

paragraph 変数を nil に設定し、HTMLElement インスタンスへの強参照を解除しても、強循環参照は、HTMLElement インスタンスとそのクロージャの割り当てを解除の解除を妨げます。

paragraph = nil

HTMLElement のデイニシャライザのメッセージが出力されないことに注目してください。HTMLElement インスタンスが割り当て解除されていないことを示しています。

クロージャの強循環参照の解消(Resolving Strong Reference Cycles for Closures)

クロージャの定義の一部にキャプチャリストを使用することで、クロージャとクラスインスタンス間の強循環参照を解決します。キャプチャリストは、クロージャの本文内で 1 つ以上の参照型をキャプチャするときに使用するルールを定義します。2 つのクラスインスタンス間の強循環参照と同様に、キャプチャされた各参照を、強参照ではなく、弱参照または非所有参照であると宣言します。弱参照と非所有参照どちらを選択するのが適切かは、コード間の様々な関係によって変化します。

NOTE Swift では、クロージャ内で self のメンバを参照するときは常に、(someProperty または someMethod() だけでなく) self.someProperty または self.someMethod() と記述する必要があります。これは、気づかずに self をキャプチャする可能性があることを防ぐのに役立ちます。

キャプチャリストの定義(Defining a Capture List)

キャプチャリストの各アイテムは、weak または unowned キーワードと、クラスインスタンス(self など)への参照、または何らかの値で初期化された変数(delegate = self.delegate など)とのペアです。これらのペアは、カンマで区切られた 1 組の角括弧内([])に記述されます。

キャプチャリストをクロージャのパラメータリストと(提供されている場合は)戻り値の型の前に配置します。

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate]
    (index: Int, stringToProcess: String) -> String in
     // クロージャ本文がここに来ます
}

コンテキストから推論できるため、クロージャがパラメータリストまたは戻り値の型を指定しない場合は、キャプチャリストをクロージャの最初に置き、その後に in キーワードを置きます。

lazy var someClosure = {
    [unowned self, weak delegate = self.delegate] in
     // クロージャ本文がここに来ます
}

弱参照と非所有参照(Weak and Unowned References)

クロージャとキャプチャするインスタンスが常に相互に参照し、常に同時に割り当てが解除される場合は、クロージャ内のキャプチャを非所有参照として定義します。

逆に、キャプチャされた参照が将来のある時点で nil になる可能性がある場合は、キャプチャを弱参照として定義します。弱参照は常にオプショナル型で、参照するインスタンスの割り当てが解除されると、自動的に nil になります。これにより、クロージャの本文内でそれらの存在を確認できます。

NOTE キャプチャされた参照が nil にならない場合は、弱参照ではなく、常に非所有参照としてキャプチャするべきです。

非所有参照は、Strong Reference Cycles for Closures(クロージャの強循環参照)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) のインスタンス割り当てが解除されました")
    }
}

この HTMLElement の実装は、asHTML クロージャ内にキャプチャリストが追加されていることを除けば、前の実装と同じです。この場合、キャプチャリストは [unowned self] で、「強参照ではなく、非所有参照として self をキャプチャする」という意味です。

先ほどと同じように HTMLElement インスタンスを作成して出力できます。

var paragraph: HTMLElement? = HTMLElement(name: "p", text: "hello, world")
print(paragraph!.asHTML())
// <p>hello, world</p>

キャプチャリストを配置した場合の参照は次のようになります:

クロージャの循環参照 キャプチャリスト

今回は、クロージャによる self のキャプチャは非所有参照で、キャプチャした HTMLElement インスタンスを強く保持しません。paragraph 変数からの強参照を nil に設定すると、HTMLElement インスタンスの割り当てが解除されます。これは、下記の例のデイニシャライザメッセージの出力からもわかります:

paragraph = nil
// p のインスタンス割り当てが解除されました

キャプチャリストの詳細については、Capture Lists(キャプチャリスト)を参照ください。

results matching ""

    No results matching ""