LoginSignup
205
152

More than 5 years have passed since last update.

Swiftの @escaping と weak/unowned の理解

Last updated at Posted at 2017-09-18

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 がプロパティで保持しているケースがある)

などが多いと思います.

delegate の例

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

もし delegateweak を付けなかった場合 "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 により弱参照にすることで,循環参照を断ち切りメモリ解放漏れを防ぎます.

205
152
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
205
152