iOS
Swift

【Swift】weakやunownedなどの参照についてまとめてみた

これは?

タイトルの通りです。
公式のSwiftガイドブックAutomatic Reference Countingの資料をもとに書き下しています。

はじめに

SwiftのメモリはガベージコレクションではなくARC(Automatic Reference Counting)によって管理されています。

まずはこのARCについてサクッと説明した後、それに関連する強参照,弱参照(weak)そして非所有参照(unowned)について説明します。

ARCとは

新しいインスタンスを初期化する際に、ARCはそのインスタンスの型や保有するプロパティに応じたメモリを確保します。

そして、そのインスタンスが必要なくなったらARCは確保しているメモリを解放します。

しかし、まだ必要なインスタンスにもかかわらずメモリを解放してしまった場合、その後インスタンスにアクセスしようとすれば当然クラッシュしてしまいます。

これを防ぐために、ARCはそれぞれのインスタンスがいくつのプロパティや変数,定数から参照されているかをカウントし、その参照カウントがゼロにならない限りメモリは解放しないようになっています。

では実際に例を用いてARCの挙動を見てみましょう。

class Person {
    let name: String
    init(name: String) {
        self.name = name
        print("\(name) is being initialized")
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

上記のPersonクラスは、init 及び deinit が呼び出されたときにメッセージを出力します。

次のコードでは、Personクラスの新しいインスタンスに対する複数の参照を示します。

var reference1: Person? = Person(name: "John Appleseed")
// Prints "John Appleseed is being initialized
var reference2: Person? = reference1
var reference3: Person? = reference1

John Appleseedという名前を持つ新しいPersonインスタンスが初期化され、
そのインスタンスは現在reference1,2,3の3つの変数からの参照を持つことがわかります。

ここでreference2,3の参照を切ってみましょう。

reference2 = nil
reference3 = nil

インスタンスへの参照は、reference1からの参照が1つ残っているためまだメモリは解放されません。
(deinit が呼び出されていませんね)

では最後にreference1の参照も切ってみましょう。

reference1 = nil
// Prints "John Appleseed is being deinitialized"

インスタンスへの参照カウントがゼロになり、無事 deinit が呼び出されました。

強参照

さて、これまで扱ってきた参照はすべて 強参照 と呼びます。

この強参照を無意識に使いまわしていると、例えば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クラスとApartmentクラスはそれぞれお互いをプロパティとして持ちます。
optionalとして定義しているため、初期値はnilです。

次にそれぞれのインスタンスを強参照する変数を宣言します。

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

Personインスタンスの持つapartmentプロパティにApartmentインスタンスを、
Apartmentインスタンスの持つtenantプロパティにPersonインスタンスを代入します。

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

ここで、現在の参照関係は以下のようになっています。

image.png

それぞれのインスタンスがお互いに強参照していることがわかります。

なので、変数john,unit4Aの強参照を切っても参照カウントはゼロになりません。

john = nil
unit4A = nil

deinit が呼び出されないことが確認できますね。
解放されないメモリが蓄積されていくことになります。

変数からの強参照を切った後の参照関係は以下のようになります。

image.png

このインスタンス間の強参照は残り続け、メモリが解放されることはありません。

この問題を解決するためにあるのが 弱参照非所有参照 です。

弱参照 (weak)

弱参照はARCの参照カウントに加算されません。

よって、強参照と弱参照を1つずつ持つインスタンスは参照カウントが1であり、1つの強参照を切ればインスタンスは解放されます。

これを踏まえて上記の例を改善します。

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") }
}

変更点は1箇所のみ、Apartmentクラスのtenantプロパティを弱参照で宣言しています。
そして先の例と同様の参照関係を作ります。

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

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

ここで、現在の参照関係は以下のようになっています。

image.png

Personインスタンスは変数johnからの強参照とApartmentインスタンスからの弱参照を持つため、参照カウントは1となります。

変数johnにnilを代入して強参照を切ってみましょう。

john = nil
// Prints "John Appleseed is being deinitialized

Personインスタンスの参照カウントはゼロになり、 deinit が呼び出されていることがわかります。

image.png

非所有参照 (unowned)

弱参照と同様に非所有参照はARCの参照カウントに加算されません。

弱参照との使い分けは

  • 弱参照
    • 参照対象が自身よりも先にメモリが解放されるときに使う
  • 非所有参照
    • 参照対象が自身と同じかより後にメモリが解放されるときに使う

という具合に行います。

上記の例では、Apartmentの居住者tenantは必ずしもいるとは限らず、またコロコロ変わるものであるため弱参照を使用するのが適切ですね。

以下では非所有参照が適切なケース、クレジットカードとその利用者を例にとり説明します。

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") }
}

CustomerクラスとCreditCardクラスはそれぞれお互いをプロパティとして持ちます。

Customerはクレジットカードを持っていないかもしれませんが、
CreditCardは必ずその持ち主Customerがいることになります。

ゆえにCustomerクラスはcardプロパティをCreditCard?型として持ち、
CreditCardクラスはcustomerプロパティを非所有参照として宣言しています。

次にCustomerインスタンスを強参照する変数を宣言し、
そのCustomerインスタンスのプロパティcardにCreditCardインスタンスを代入します。

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

ここで、現在の参照関係は以下のようになっています。

image.png

Customerインスタンスは、変数johnからの強参照とCreditCardインスタンスからの非所有参照を持つため参照カウントは1となり、
CreditCardインスタンスは、Customerインスタンスからの強参照を1つ持つため参照カウントは1となります。

それでは、変数johnにnilを代入してCustomerインスタンスへの強参照を切ってみましょう。

john = nil
// Prints John Appleseed is being deinitialized
// Prints Card #1234567890123456 is being deinitialized

まずCustomerインスタンスの参照カウントがゼロとなりメモリが解放されます。
次にCreditCardインスタンスはCustomerインスタンスからの強参照を失い、参照カウントがゼロとなってメモリが解放されます。

非所有参照 (unowned) と暗黙的アンラップ型 (!)

PersonとApartmentの例では、それぞれをoptional型のプロパティとして持つため、弱参照が適切でした。

CustomerとCreditCardの例では、一方はoptional型のプロパティとして、もう一方はoptional型ではないプロパティとしてそれぞれを持つため、非所有参照が適切でした。

しかし、両方ともoptional型ではないプロパティとしてそれぞれを持ちたいケースも存在するはずです。
そのときに利用するのが非所有参照と暗黙的アンラップ型です。

以下ではこのケースを満たす例として、CountryクラスとCityクラスを示します。

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
    }
}

CountryクラスとCityクラスはそれぞれお互いをプロパティとして持ちます。

Countryは必ず首都である町(city)が必要であり、
Cityは必ず国(country)に所属しています。

ゆえにCountryクラスはcapitalCityプロパティをCity!型として持ち、
Cityクラスはcountryプロパティを非所有参照として宣言します。

CountryクラスのinitializerではcapitalCityプロパティを初期化する際に、selfを使用しています。
これはSwiftのinitializerのルールに反しますが(こちらの記事を参照)、capitalCityプロパティを暗黙的アンラップ型として宣言しているため初期値にはnilが入っており、これを可能にしています。

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"

上記のプログラムではCountryインスタンスを初期化し、その結果を出力しています。

capitalCityプロパティはCountryインスタンスのinitializerにて既に初期化されているため、
アクセスする際にnilであることを考慮する必要はありません。

まとめ

少々長くなりましたが、これがSwiftの参照パターンです。

本稿が、強参照,弱参照,非所有参照のどれを使うべきかの判断の一助になれば幸いです。