Swift には、クラス型のプロパティを扱うときに、強い参照の循環を解決する方法が 2 つあります。弱い参照と非所有の参照です。
弱い参照と非所有の参照は、あるインスタンスが、別のインスタンスの参照を強く保持することなく、そのインスタンスを参照できるようにする参照循環です。従って、強い参照の循環を生成することなく、インスタンスが互いに参照することができます。
生存期間のどこかで参照が nil
になることが妥当な場合には、弱い参照を利用します。反対に、初期化の間に設定されると、その後に参照が nil
になることがないとわかっている場合には、非所有の参照を利用します。
弱い参照
弱い参照は、参照するインスタンスを強く保持し続けない参照で、参照されていたインスタンスを ARC が処理することを妨げません。この振る舞いは、参照が強い参照の循環になることを防ぎます。弱い参照とするには、プロパティや変数の宣言の前に weak
キーワードを置きます。
生存中のどこかで参照が値を失う可能性がある場合、参照の循環を避けるために弱い参照を利用します。参照に常に値がある場合には、Unowned Reference で説明されているように、非所有の参照を利用します。前の Apartment
の例では、アパートの生存期間のどこかでテナントがいないことはありえるので、このケースでは参照の循環を切れる弱い参照が適切な方法です。
弱い参照は参照するインスタンスを強く保持し続けないため、弱い参照がまだ参照している間に、そのインスタンスが割り当て解除されることがあります。従って、参照するインスタンスが割り当て解除されたときに、ARC は自動的に弱い参照に nil
を設定します。弱い参照の値を nil
にできるようにするため、弱い参照は常にオプショナル型とします。他のオプショナル型の値と同じようにして、弱い参照の値が存在しているかを確認することができ、存在しなくなった無効なインスタンスを参照したままになることはありません。
次の例は、1 点の重要な違いを除いて、前の Person
と Apartment
の例とまったく同じです。今回は、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
// "John Appleseed is being deinitialized" と出力
Person
インスタンスに対する強い参照が無くなったため、割り当て解除されて tenant
プロパティに nil
が設定されます。
残っている Apartment
インスタンスに対する強い参照は、変数 unit4A
からの参照です。その強い参照を切ると、Apartment
インスタンスに対する強い参照は無くなります。
unit4A = nil
// "Apartment 4A is being deinitialized" と出力
Apartment
インスタンスに対する強い参照が無くなったため、割り当て解除されます。
非所有の参照
弱い参照と同様、非所有の参照は、参照するインスタンスを強く保持し続けることはありません。一方で、弱い参照と異なり、非所有の参照には常に値があるものと想定されます。これにより、非所有の参照は常にオプショナルでない型で定義されます。非所有の参照とするには、プロパティや変数の宣言の前に unowned
キーワードを置きます。
非所有の参照はオプショナルでないため、非所有の参照を使用するたびにアンラップする必要はありません。非所有の参照は、常に直接参照することができます。しかし、オプショナルでない型の変数に nil
を設定することはできないため、参照するインスタンスが割り当て解除されたときに、ARC は参照に nil
を設定することができません。
参照するインスタンスが割り当て解除された後に、非所有の参照にアクセスしようとした場合、実行時エラーになります。参照が常にインスタンスを参照するときにのみ、非所有の参照を利用します。
参照するインスタンスが割り当て解除された後に、非所有の参照にアクセスしようとした場合、Swift は確実にアプリをクラッシュさせることにも注目してください。この状況において、予期しない振る舞いになることはありません。そうなることを防ぐべきですが、アプリは常に期待どおりクラッシュします。
以降の例では、銀行の顧客とその顧客のクレジットカードをモデル化する、2 つのクラス Customer
と CreditCard
を定義しています。各クラスはプロパティとして他方のクラスのインスタンスを保持します。この関係は、強い参照の循環を生成する可能性があります。
Customer
と CreditCard
間の関係は、弱い参照の例で見た Apartment
と Person
間の関係とは少し異なります。このデータモデルでは、顧客はクレジットカードを持っているかもしれませんし、持っていないかもしれませんが、クレジットカードは常に顧客に結びついています。これを表現するために、Customer
クラスは card
プロパティをオプショナルとしていますが、CreditCard
クラスはオプショナルでない customer
プロパティとしています。
さらに、新しい CreditCard
インスタンスは、CreditCard
イニシャライザに number
値と customer
インスタンスを渡すことでしか生成できません。これにより、CreditCard
インスタンスが生成されるときに、CreditCard
インスタンスに結びつけられる customer
インスタンスが常にあるようにしています。
クレジットカードには常に顧客があるため、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
ではなく、32 ビットおよび 64 ビットの両システムで 16 桁のカード番号を保管するのに十分な UInt64
の型で定義されています。
次のコードスニペットは、特定の顧客に対する参照を保管するために使用する、Customer
のオプショナル変数 john
を定義しています。
var john: Customer?
次に、Customer
インスタンスを生成し、新しい CreditCard
インスタンスを初期化するためと、その顧客の card
プロパティに代入するために利用します。
john = Customer(name: "John Appleseed")
john!.card = CreditCard(number: 1234_5678_9012_3456, customer: john!)
次は、2 つのインスタンスをつなげた結果、参照がどのようになっているかを示しています。
Customer
インスタンスは CreditCard
インスタンスに対する強い参照を持ち、CreditCard
インスタンスは Customer
インスタンスに対する非所有の参照を持っています。
customer
の参照は非所有なため、変数 john
による強い参照を切ると、Customer
インスタンスに対する強い参照は無くなります。
Customer
インスタンスに対する強い参照が無くなったため、割り当て解除されます。この結果、CreditCard
インスタンスに対する強い参照も無くなるため、割り当て解除されます。
john = nil
// "John Appleseed is being deinitialized" と出力
// "Card #1234567890123456 is being deinitialized" と出力
最後のコードスニペットは、変数 john
に nil
が設定された後、Customer
インスタンスと CreditCard
インスタンスそれぞれのデイニシャライザが、デイニシャライズされたメッセージを出力していることを示しています。
非所有の参照と無条件にアンラップされるオプショナルプロパティ
上で見た弱い参照と非所有の参照の例は、強い参照の循環を切る必要がある一般的なシナリオをカバーしています。
Person
と Apartment
の例は、2 つのプロパティが共に nil
になることがあり、強い参照の循環になる可能性がある状況を示していました。このシナリオの場合には、弱い参照で解決することがベストです。
Customer
と CreditCard
の例は、一方のプロパティは nil
になることがあり、他方は nil
になることがない、強い参照の循環になる可能性がある状況を示していました。このシナリオの場合には、非所有の参照で解決することがベストです。
しかしながら、両方のプロパティに常に値があり、初期化完了後にはプロパティが nil
になることが無いという 3 つ目のシナリオがあります。このシナリオの場合には、一方のクラスを非所有の参照とし、他方のクラスを無条件にアンラップされるオプショナルプロパティとして組み合わせることが効果的です。
こうすることで、参照の循環を回避しつつ、初期化の完了後は直接(オプショナルをアンラップすることなく)それぞれのプロパティにアクセスできるようになります。そのような関係をセットアップする方法について見ていきます。
次の例では、2 つのクラス Country
と City
を定義していて、プロパティに他方のクラスのインスタンスを保持しています。このデータモデルでは、すべての国が常に首都を持つ必要があり、すべての都市が常に国に属する必要があります。これを表現するために、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 で説明されているように、新しい Country
インスタンスが完全に初期化されるまで、Country
のイニシャライザは City
のイニシャライザに self
を渡すことはできません。
この要求に対処するために、Country
クラスの capitalCity
プロパティを、タイプアノテーションの最後にエクスクラメーションマークを付ける (City!
) ことで、無条件にアンラップされるオプショナルプロパティとして宣言します。つまり、capitalCity
プロパティは、他のオプショナルと同じように nil
がデフォルト値ですが、Implicitly Unwrapped Optionals で説明されているように、値をアンラップする必要なくアクセスすることができます。
capitalCity
のデフォルト値が nil
であるため、Country
インスタンスがイニシャライザ内で name
プロパティを設定するとすぐに、新しい Country
インスタンスは完全に初期化されているとみなされます。つまり、name
プロパティが設定されるとすぐに、self
プロパティを参照し、渡すことができるようになります。従って、Country
イニシャライザが capitalCity
プロパティを設定するとき、Country
イニシャライザは City
イニシャライザのパラメータの 1 つとして self
を渡すことができます。
まとめると、強い参照の循環を生成することなく、Country
と City
のインスタンスを 1 文で生成することができ、オプショナル値をアンラップするためにエクスクラメーションマークを使う必要なく、capitalCity
プロパティに直接アクセスすることができます。
var country = Country(name: "Canada", capitalName: "Ottawa")
print("\(country.name)'s capital city is called \(country.capitalCity.name)")
// "Canada's capital city is called Ottawa" と出力
この例では、無条件にアンラップされるオプショナルを使用することで、2 段階のクラスイニシャライザによるの必要条件のすべてが満たされています。強い参照の循環を回避しつつ、初期化の完了後はオプショナルでない値のように capitalCity
プロパティにアクセスすることができます。
Portions of this page are translations based on work created and shared by Apple and used according to terms described in the Creative Commons Attribution 4.0 International License.