Swiftにおけるインスタンスの循環参照

  • 5
    いいね
  • 0
    コメント

Swiftの循環参照によるメモリリークを理解するためにサンプルコードを書いてみました。

Xcode 8.2.1のPlaygroundで動作を確認しています。

1. 循環参照によるメモリリーク

// 書籍クラス
class Book : CustomStringConvertible {
    let name: String                    // 書籍の名前
    var bookshelf: Bookshelf?           // どの本棚に入っている
    init(name: String) { self.name = name }
    deinit { print("\(name): deinit") }
    var description: String {
        var desc : String
        if let b = bookshelf {          // 本棚に収まっている
            desc = "\(b.name)に置いてある \(name)"
        } else {                        // 本棚の外にある
            desc = "積んである\(name)"
        }
        return desc
    }
}

// 本棚クラス
class Bookshelf {
    let name: String                    // 本棚の名前
    var books = [Book]()                // 本棚にある書籍(配列)
    init(name: String) { self.name = name }
    deinit { print("\(name): deinit") }
    func add(_ book: Book) {
        books.append(book)              // 本棚から書籍への参照をセット
        book.bookshelf = self           // 書籍から本棚への参照をセット
    }
}

do {
    var officeShelf : Bookshelf! = Bookshelf(name: "職場の本棚")
    let bookA : Book = Book(name: "『アジャイルサムライ』")
    let bookB : Book = Book(name: "『時を超えた建設の道』")

    print(bookA)    // => 積んである『アジャイルサムライ』
    print(bookB)    // => 積んである『時を超えた建設の道』

    officeShelf.add(bookA)

    print(bookA)    // => 職場の本棚に置いてある『アジャイルサムライ』
    print(bookB)    // => 積んである『時を超えた建設の道』
}                   // => 『時を超えた建設の道』: deinit
// => Bookshelf『職場の本棚』とBook『アジャイルサムライ』はメモリリーク

2. 弱い参照 (weak reference) をつかった循環参照の回避

// 書籍クラス
class Book : CustomStringConvertible {
    let name: String
    weak var bookshelf: Bookshelf?      // 弱い参照 & オプショナル型

    init(name: String) { self.name = name }
    deinit { print("\(name): deinit") }
    var description: String {
        var desc : String
        if let b = bookshelf {
            desc = "\(b.name)に置いてある\(name)"
        } else {
            desc = "積んである\(name)"
        }
        return desc
    }
}

// 本棚クラス
class Bookshelf {
    let name: String
    var books = [Book]()
    init(name: String) { self.name = name }
    deinit { print("\(name): deinit") }
    func add(_ book: Book) {
        books.append(book)
        book.bookshelf = self
    }
}

do {
    var officeShelf : Bookshelf! = Bookshelf(name: "職場の本棚")
    let book : Book = Book(name: "『アジャイルサムライ』")
    officeShelf.add(book)
    print(book)         // => 職場の本棚に置いてある『アジャイルサムライ』
    officeShelf = nil   // => 職場の本棚: deinit
    print(book)         // => 積んである『アジャイルサムライ』
}                       // => 『アジャイルサムライ』: deinit

【 ポイント 】

  1. bookshelfを弱い参照(weak reference), オプショナル型で定義している。
  2. bookshelfの参照先のインスタンスが解放されたときに自動的にbookshelfにはnilがセットされる。これを「ゼロ化」と呼ぶ

3. 非所有参照(unowned reference)をつかった循環参照の回避

// 書籍クラス
class Book : CustomStringConvertible {
    let name: String
    unowned var bookshelf: Bookshelf  // 非所有参照 & 非オプショナル型
    init(name: String, bookshelf: Bookshelf) {
        self.name = name
        self.bookshelf = bookshelf
    }
    deinit { print("\(name): deinit") }
    var description: String {
        let desc : String = "\(bookshelf.name)に置いてある \(name)"
        return desc
    }
}

// 本棚クラス
class Bookshelf {
    let name: String
    var books = [Book]()
    init(name: String) { self.name = name }
    deinit { print("\(name): deinit") }
    func add(_ book: Book) {
        books.append(book)
        book.bookshelf = self
    }
}

do {
    var officeShelf : Bookshelf! = Bookshelf(name: "職場の本棚")
    let book = Book(name:"『アジャイルサムライ』", bookshelf:officeShelf)
    print(book)         // => 職場の本棚に置いてある『アジャイルサムライ』
    officeShelf = nil   // => 職場の本棚: deinit
    print(book)         // => Error Exception
}

【 ポイント 】

  1. 非所有参照(unowned)を使うことで循環参照は回避できている
  2. しかしbookshelfの参照先が解放(破棄)されたとしてもゼロ化は実行されない。bookshelfnilがセットされることはないという前提はプログラマが保証しなければならない。
  3. このサンプルでは意図的にofficeShelfnilをセットしてインスタンスを解放している。するとクラスBookshelfの中で実行時エラーが発生する。