Swift の Escaping Closure の使い方と注意点

公開:2020/09/29
更新:2020/09/29
2 min読了の目安(約2600字TECH技術記事

想定読書

  • Swift を使える。
  • Swift のクロージャーは知っている。
  • @escaping 属性をなんとなく使っている。

はじめに

Swift って英語のドキュメントが豊富にあって便利ですね。
日本語のドキュメントを見ていてピンと来ないことも丁寧に書かれていてびっくりします。

そういった有益なドキュメントから得た知見を継続的にアウトプットしたいって思っています。

escaping をいつ使うのか

関数の引数に渡されるクロージャーに付与することのできる属性です。
クロージャーの実行されるタイミングは大きく分けて下記の2つがあります。

  • 関数の実行の終了(戻り値が戻される時)前
  • 不定(非同期実行)

前者の場合は escaping 属性は特に気にしなくてもいいのですが、後者の場合は特別な考慮が必要になってきます。

関数の実行が終了したあとも関数内のスコープを参照する必要があるからです。

非同期処理を開始する多くの関数は completion handler としてクロージャーの引数を取り、非同期処理が完了したあとに実行することになります。
そのようなタイミングに escaping 属性をつけて関数に引数として渡します。

var completionHandlers = [() -> Void]()
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

escaping を使う際の注意

前述したように escaping と変数のスコープ(キャプチャ)は切っても切り離せない関係です。

escaping クロージャーは関数の実行とは別に関数のスコープを参照する必要があるため、変数をキャプチャする必要があります。

クラスのような参照型のインスタンスをキャプチャに含める際には強参照サイクルを誤って作ってしまうがちですので要注意です。

変数をキャプチャするやり方は2つあります。

  1. self を利用時に明示的に記載する
    メリット
  • 使う意思を表明できる。
  • 循環参照が存在しないことを確認できる。
  1. クロージャーのキャプチャリストに self を含める
    この場合はself の参照は暗黙的に行います。

実際に1と2のケースをソースコードに書き下すと下記のようになります。

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()
}

class SomeClass {
    var x = 10
    func doSomething() {
	// 1. 明示的に self を記載
        someFunctionWithEscapingClosure { self.x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

class SomeOtherClass {
    var x = 10
    func doSomething() {
	// 2. クロージャーのキャプチャリストに self を含める。
        someFunctionWithEscapingClosure { [self] in x = 100 }
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

ここで注意しなければいけないことがあります。

self が構造体や列挙型のインスタンスである場合、escaping のクロージャーは可変の(ミュータブルな) self への参照をキャプチャすることができないということです。
構造体や列挙型は値型であり、クロージャー内から self を変更することができないからです。

実際に下記の someFunctionWithEscapingClosure はエラーが履かれます。

Error: Closure cannot implicitly capture a mutating self parameter
struct SomeStruct {
    var x = 10
    mutating func doSomething() {
        someFunctionWithNonescapingClosure { x = 200 }  // Ok
        someFunctionWithEscapingClosure { x = 100 }     // Error
    }
}

最後に

最後までご覧いただきましてありがとうございます。
クロージャーはまだ使いなれていない分考察の浅い記事になってしまいました。
実際に使い倒して勘所がアップデートされたらまた記事を更新しようと思います。

次は Protocol-Oriented Programming in Swift を見て Swift のベースの考え方を増強して行く予定ですので自分の中で整理されたら記事にまとめてみます。

誤っている箇所や詳細な情報をお持ちでしたらぜひ共有お願いいたします。

参考