iOS
Swift

Swiftの @escaping と weak/unowned の理解

More than 1 year has passed since last update.

Swiftでクロージャを使うコードを書いていると,よく @escaping とか weak / unowned といったワードを目にすることがあります.

なんとなく「コンパイラに怒られたら付ける」とか「参考にしたコードに書いてあったから付ける」などとしてしまっていたのですが(反省),ちゃんと意味を調べてみてもなかなか理解ができませんでした.

そこで,参考にした記事と私が動作確認したコードを示し,「こう考えたら私は理解できた」というメモを残しておきます.

なお,概念の理解を優先しているため,用語や表現の正確性は無いかもしれません.

参考にした記事

環境

  • Swift 3.1
  • Xcode 8.3.3

escaping の理解

まず, @escaping の文法上の出現位置は,関数定義のパラメータリスト中におけるクロージャの型定義の前です.

class Foo {
  func foo(_ closure: @escaping () -> Void) {
  }
}

escape=退避=非同期実行 と考えてみる

この @escaping が付いているということは,

  • 渡すクロージャが退避(関数のスコープ外に保管(コピー))される
  • 渡すクロージャが関数( foo )の実行完了後に呼ばれる(関数実行後も生存し続ける)
  • 渡すクロージャが非同期実行される

と考えることができます.

escaping が必要ない場合

class Foo {
  func foo(_ closure: () -> Void) {
    closure()
  }
}

class Bar {
  func bar() {
    let foo = Foo()
    foo.foo {
      print("closure called")
    }
    print("Foo.foo() called")
  }
}

let bar = Bar()
bar.bar()

// --> closure called
// --> Foo.foo() called

Foo.foo(_ closure:) には @escaping を付けていません.
これにより渡されるクロージャ( { print("closure called") } )はescapeされません(=退避されません.関数実行完了前に呼ばれます.非同期実行されません.)

Array.forEach(_:) の定義( func forEach(_ body: (Element) throws -> Void) rethrows )を見るとわかるように,関数の実行完了前にクロージャが呼ばれ,保管されないような関数には @escaping は付いていません.

escaping が必要な場合

class Foo {
  var storedClosure: (() -> Void)?

  func foo(_ closure: @escaping () -> Void) {
    storedClosure = closure
  }

  func callClosure() {
    storedClosure?()
  }
}

class Bar {
  func bar() {
    let foo = Foo()
    foo.foo {
      print("closure called")
    }
    print("Foo.foo() called")
    foo.callClosure()
  }
}

let bar = Bar()
bar.bar()

// --> Foo.foo() called
// --> closure called

Foo.foo(_ closure:) で渡されたクロージャを関数実行後に呼び出すため storedClosure プロパティに保管しています.
このような場合 @escaping を付ける必要があります.
もし @escapingを付けなかった場合は,以下のようなコンパイルエラーが発生します( @escaping を付けなさいと言われる).

MyPlayground.playground:27:14: note: parameter 'closure' is implicitly non-escaping
  func foo(_ closure: () -> Void) {
             ^
                      @escaping 

URLSession.downloadTask(with:completionHandler:) の定義( func downloadTask(with url: URL, completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask )を見るとわかるように,クロージャが保管され,関数の実行完了後に非同期で呼ばれるような関数には @escaping が付いています.

weak/unowned の理解

まず, weak / unowned の文法上の出現位置は,クロージャ定義のパラメータリストの前です.

class Foo {
  let colsure = { [weak self] in
  }
}

循環参照が起こるパターン

次のようにメモリが開放されたら deinit でメッセージを出す Foo があるとします.

class Foo {
  deinit {
    print("foo deinit")
  }
}

var foo: Foo! = Foo()
foo = nil

// --> foo deinit

しかし,クロージャがFooのインスタンス自身を強参照している( weak / unowned を付けない)場合,クロージャのインスタンスとFooのインスタンスが相互に参照し合うのでメモリが開放されません.

class Foo {
  var storedClosure: (() -> Void)?
  let name = "foo"

  func foo() {
    storedClosure = {
        print(self.name)
    }
  }

  deinit {
    print("foo deinit")
  }
}

var foo: Foo! = Foo()
foo.foo()
foo.storedClosure!()
foo = nil

// --> foo

参照の関係性は次のようになります.

cyclic_reference.png

循環参照を切るための weak/unowned

この循環参照を断ち切るために weak / unowned を使います.

class Foo {
  var storedClosure: (() -> Void)?
  let name = "foo"

  func foo() {
    storedClosure = { [weak self] in
      if let weakSelf = self {
        print(weakSelf.name)
      }
    }
  }

  deinit {
    print("foo deinit")
  }
}

var foo: Foo! = Foo()
foo.foo()
foo.storedClosure!()
foo = nil

// --> foo
// --> foo deinit

クロージャの引数の前に [weak self] を宣言することで,クロージャからself( foo )に対する参照を弱参照にし, foo のメモリ解放が行われました.この時の参照関係は次のようになります.

weak_reference.png

weakとunowned の違い

  • weak は弱参照先がメモリ解放されている場合nilになります(つまり通常のOptionalと同様にアンラップが必要です.)
  • unowned はOptionalではないので,弱参照先がメモリ開放されていた場合クラッシュします

よって,参照先(先の例では foo )を参照する際にnilでないことが保証されるなら unowned を使います.どちらにすべきか迷ったら weak を使っておくとよいと思います.

循環参照を断ち切る例を unowned で書くと次のようになります(クロージャ内でアンラップせずに self を使えています.)

class Foo {
  var storedClosure: (() -> Void)?
  let name = "foo"

  func foo() {
    storedClosure = { [unowned self] in
      print(self.name)
    }
  }

  deinit {
    print("foo deinit")
  }
}

var foo: Foo! = Foo()
foo.foo()
foo.storedClosure!()
foo = nil

// --> foo
// --> foo deinit

unowned を使っているのに参照時にnilである場合の例は,次のようになります.

class Foo {
  var storedClosure: (() -> Void)?
  let name = "foo"

  func foo() {
    storedClosure = { [unowned self] in
      print(self.name)
    }
  }

  deinit {
    print("foo deinit")
  }
}

var foo: Foo! = Foo()
foo.foo()
let fooStoredClosure = foo.storedClosure
foo = nil
fooStoredClosure!() // fooがnilになった後でクロージャを呼ぶ

// --> foo deinit

"foo deinit" の出力の後に fooStoredClosure!() で次のような実行時エラーが発生しました.

unowned_error.png

weak/unowned の使い所

具体的な使い所としては

  • delegate の宣言(一般的にデリゲートの参照先とデリゲートを持つオブジェクトは循環参照する)
  • UIViewController における completion ブロック内での self の参照( completion を持つ関数のオブジェクトを UIViewController がプロパティで保持しているケースがある)

などが多いと思います.

delgate の例

protocol FooDelegate: class {
  func delegateFunc()
}

class Foo {
  weak var delegate: FooDelegate?

  func callDelegate() {
    delegate?.delegateFunc()
  }

  deinit {
    print("foo deinit")
  }
}

class Bar: FooDelegate {
  var foo: Foo?

  func bar() {
    foo = Foo()
    foo?.delegate = self
    foo?.callDelegate()
  }

  func delegateFunc() {
    print("bar delegate func")
  }

  deinit {
    print("bar deinit")
  }
}

var bar: Bar! = Bar()
bar.bar()
bar = nil

// --> bar delegate func
// --> bar deinit
// --> foo deinit

参照の関係は次のようになります.

delegate_example-2.png

もし delgateweak を付けなかった場合 "bar deinit" と "foo deinit" が出力されません.

UIViewController の例

ViewController とクロージャと UIAlertController のオブジェクトが3者で循環参照になってしまうため,クロージャから ViewController の参照の部分で unowned を使い弱参照にして循環を断ち切っています.

class ViewController: UIViewController {

  @IBOutlet weak var label: UILabel!

  var dlg: UIAlertController!

  override func viewDidLoad() {
    super.viewDidLoad()
    dlg = UIAlertController(title: "", message: "", preferredStyle: .alert)
    dlg.addAction(UIAlertAction(title: "ok", style: .default) { [unowned self] _ in
      self.label.text = "ok selected"
    })
  }

  override func viewDidAppear(_ animated: Bool) {
    present(dlg, animated: true)
  }

}

参照の関係は次のようになります.

uiviewcontroller_example.png

escaping と weak/unowned の関係

@escaping されたクロージャは先の UIViewController の例のように,循環参照を発生させる可能性があります.その場合に,クロージャ内で参照する変数を weak / unowned により弱参照にすることで,循環参照を断ち切りメモリ解放漏れを防ぎます.