Swiftでクロージャを使うコードを書いていると,よく @escaping
とか weak
/ unowned
といったワードを目にすることがあります.
なんとなく「コンパイラに怒られたら付ける」とか「参考にしたコードに書いてあったから付ける」などとしてしまっていたのですが(反省),ちゃんと意味を調べてみてもなかなか理解ができませんでした.
そこで,参考にした記事と私が動作確認したコードを示し,「こう考えたら私は理解できた」というメモを残しておきます.
なお,概念の理解を優先しているため,用語や表現の正確性は無いかもしれません.
参考にした記事
- Swift 3 の @escaping とは何か - Qiita
- "Weak, Strong, Unowned, Oh My!" - A Guide to References in Swift — KrakenDev
環境
- 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
参照の関係性は次のようになります.
循環参照を切るための 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と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!()
で次のような実行時エラーが発生しました.
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
に weak
を付けなかった場合 "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)
}
}
参照の関係は次のようになります.
escaping と weak/unowned の関係
@escaping
されたクロージャは先の UIViewController
の例のように,循環参照を発生させる可能性があります.その場合に,クロージャ内で参照する変数を weak
/ unowned
により弱参照にすることで,循環参照を断ち切りメモリ解放漏れを防ぎます.