Swift
Swift4.0

Swiftのクロージャにおける循環参照

Swiftのクロージャが循環参照をつくりメモリリークする例を理解するためにサンプルコードを書いてみました。

Xcode 9.2 (Playgrounds), Swift 4.0.3 で動作を確認しています。

1. クロージャが強参照(strong reference)することによる循環参照

class CalorieCalculator {
    let name: String
    let weight: Float

    init(name: String, weight: Float) {
        self.name = name
        self.weight = weight
        print("\(self.name) is initialized")
    }

    deinit {
        print("\(self.name) is deinitialized")
    }

    // 遅延格納型プロパティ(lazy)
    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)

    func calorie(mets: Float) -> (Float) -> Float {
        return {
            // クロージャからselfに対する強参照(strong reference)がある
            (hour:Float)->Float in return (mets*hour*self.weight)
        }
    }
}

do {
    let calcOfAnna = CalorieCalculator(name: "Anna", weight: 52.3)
    let calcOfElsa = CalorieCalculator(name: "Elsa", weight: 61.5)

    print("- [Anna] swim : \(calcOfAnna.swim(1.0)) kcal")
    print("- [Anna] golf : \(calcOfAnna.golf(8.0)) kcal")
    print("- [Anna] walk : \(calcOfAnna.walk(8.0)) kcal")
}

実行結果

Anna is initialized
Elsa is initialized
- [Anna] swim : 418.4 kcal
- [Anna] golf : 1464.4 kcal
- [Anna] walk : 1673.6 kcal
Elsa is deinitialized

【 ポイント 】

  1. クロージャの内部でselfを強参照でキャプチャすることで循環参照が発生している。するとインスタンスが解放されずメモリリークする。

2. 明示的なキャプチャリストにより循環参照の回避

class CalorieCalculator {
    let name: String
    let weight: Float

    init(name: String, weight: Float) {
        self.name = name
        self.weight = weight
        print("\(self.name) is initialized")
    }

    deinit {
        print("\(self.name) is deinitialized")
    }

    // 遅延格納型プロパティ(lazy)
    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)

    func calorie(mets: Float) -> (Float) -> Float {
        return {
            // 値型をキャプチャリストに明示的に指定
            [weight](hour:Float)->Float in return (mets*hour*weight)
        }
    }
}

do {
    let calcOfAnna = CalorieCalculator(name: "Anna", weight: 52.3)
    let calcOfElsa = CalorieCalculator(name: "Elsa", weight: 61.5)

    print("- [Anna] swim : \(calcOfAnna.swim(1.0)) kcal")
    print("- [Anna] golf : \(calcOfAnna.golf(8.0)) kcal")
    print("- [Anna] walk : \(calcOfAnna.walk(8.0)) kcal")
}

実行結果

Anna is initialized
Elsa is initialized
- [Anna] swim : 418.4 kcal
- [Anna] golf : 1464.4 kcal
- [Anna] walk : 1673.6 kcal
Elsa is deinitialized
Anna is deinitialized

【 ポイント 】

  1. CalarieCaluculatorクラスのインスタンス self をクロージャの内部から参照していたことが諸悪(循環参照)の根源
  2. だから値型(Float)のweightを明示的にキャプチャしてselfへの参照は削除
  3. 循環参照がなくなりメモリリークが解消!

3. 非所有参照 (unowned reference) による循環参照の回避

class CalorieCalculator {
    let name: String
    let weight: Float

    init(name: String, weight: Float) {
        self.name = name
        self.weight = weight
        print("\(self.name) is initialized")
    }

    deinit {
        print("\(self.name) is deinitialized")
    }

    // 遅延格納型プロパティ(lazy)
    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)

    func calorie(mets: Float) -> (Float) -> Float {
        return {
            // selfを非所有参照(unowned)でキャプチャ
            [unowned self](hour:Float)->Float in return (mets*hour*self.weight)
        }
    }
}

do {
    let calcOfAnna = CalorieCalculator(name: "Anna", weight: 52.3)
    let calcOfElsa = CalorieCalculator(name: "Elsa", weight: 61.5)

    print("- [Anna] swim : \(calcOfAnna.swim(1.0)) kcal")
    print("- [Anna] golf : \(calcOfAnna.golf(8.0)) kcal")
    print("- [Anna] walk : \(calcOfAnna.walk(8.0)) kcal")
}

実行結果

Anna is initialized
Elsa is initialized
- [Anna] swim : 418.4 kcal
- [Anna] golf : 1464.4 kcal
- [Anna] walk : 1673.6 kcal
Elsa is deinitialized
Anna is deinitialized

【 ポイント 】

  1. self を [unowned self] と明示的に非所有参照でキャプチャすることで循環参照を回避している
  2. この例ではクロージャが実行されるときにselfが解放されていることはないため実行時エラーは発生しない
  3. もしクロージャの実行時にselfが解放されていれば実行時エラーが発生する

4. 弱参照(weak reference)による循環参照の回避

class CalorieCalculator {
    let name: String
    let weight: Float

    init(name: String, weight: Float) {
        self.name = name
        self.weight = weight
        print("\(self.name) is initialized")
    }

    deinit {
        print("\(self.name) is deinitialized")
    }

    // 遅延格納型プロパティ(lazy)
    lazy var golf: (Float) -> Float = self.calorie(mets: 3.5)
    lazy var walk: (Float) -> Float = self.calorie(mets: 4.0)
    lazy var swim: (Float) -> Float = self.calorie(mets: 8.0)

    func calorie(mets: Float) -> (Float) -> Float {
        return {
            // selfを弱参照(weak)でキャプチャ
            [weak self](hour: Float) -> Float in
            if let weakSelf = self {
                return mets * hour * weakSelf.weight
            } else {
                return 0
            }
        }
    }
}

do {
    let calcOfAnna = CalorieCalculator(name: "Anna", weight: 52.3)
    let calcOfElsa = CalorieCalculator(name: "Elsa", weight: 61.5)

    print("- [Anna] swim : \(calcOfAnna.swim(1.0)) kcal")
    print("- [Anna] golf : \(calcOfAnna.golf(8.0)) kcal")
    print("- [Anna] walk : \(calcOfAnna.walk(8.0)) kcal")
}

実行結果

Anna is initialized
Elsa is initialized
- [Anna] swim : 418.4 kcal
- [Anna] golf : 1464.4 kcal
- [Anna] walk : 1673.6 kcal
Elsa is deinitialized
Anna is deinitialized

【 ポイント 】

  1. 弱参照(weak reference)をつかうことで循環参照を回避している
  2. もしselfが解放されたとしてもゼロ化される(nilがセットされる)ため安全なコードである
  3. ゼロ化の実行コストとnilのときのコードの分岐が冗長である(安全のためのコスト)
  4. selfがnilのときに関数calorie()が0を返すことが妥当かは残課題