Make non-escaping closures the default
01 何が問題だったのか
関数の引数としてクロージャを受け取るとき、そのクロージャが関数本体の外に「エスケープ」できるかどうかは重要な区別です。エスケープするクロージャは、構造体のプロパティやグローバル変数に保存したり、別のスレッドに渡してあとから呼び出したりできる一方、その分だけ実装側の制約(強参照による循環参照の可能性、キャプチャのコスト、inout 引数をキャプチャできないことなど)が増えます。エスケープしないクロージャは関数から戻るまでの間にしか呼び出されないので、こうした制約を受けません。
当時のデフォルトは「エスケープする」だった
SE-0103 以前のSwiftでは、関数のクロージャ引数は デフォルトでエスケープする 扱いでした。エスケープしないことを明示するには、引数の型に @noescape 属性を付ける必要がありました。
// SE-0103 以前
func map<T>(_ transform: @noescape (Element) -> T) -> [T] { ... }
func forEach(_ body: (Element) -> Void) { ... } // デフォルトでエスケープ可能
しかし、実際に標準ライブラリや一般的なコードで定義される関数型引数の多くは、map や filter、forEach のような「クロージャを受け取ってその場で呼び出すだけ」の、本来エスケープする必要のないものでした。デフォルトがエスケープ側にあるため、そうした関数を書くたびに @noescape を書き足すボイラープレートが発生していました。
デフォルトが逆だったことによる弊害
デフォルトが「エスケープする」側に倒れていることは、次のような問題を生んでいました。
- 純粋なSwiftで書かれた多くの関数アルゴリズムは本質的にエスケープしないクロージャを受け取るため、そのたびに
@noescapeを書かねばならず、記述量が増える。 - エスケープしないクロージャにはretain cycleの心配がない・
self.を省略できるといった利点があり、ユーザーにとっても本来こちらが「好ましいデフォルト」だった。 inout引数をエスケープするクロージャでキャプチャすることは、当時すでに禁止される方向に進んでおり、言語全体がエスケープしないクロージャを優先する流れになっていた。@autoclosure(escaping)のように、エスケープに関連する属性が他の属性の引数として表現されており、綴りが統一されていなかった。
デフォルトを逆にすれば、安全側が自然な記述になり、本当にエスケープさせたいときだけ明示する という、Swiftが一貫して採用している設計方針に揃えられます。
02 どのように解決されるのか
関数のクロージャ引数のデフォルトを反転させ、何も書かなければエスケープしない ようにします。エスケープさせたい場合だけ、引数の型に @escaping 属性を明示します。これにより、@noescape 属性は言語から削除されます。
新しいデフォルトの使い方
エスケープしないクロージャを受け取る関数は、単に何も書かずに関数型を書くだけで済みます。
// クロージャは関数本体の外にエスケープしない(新しいデフォルト)
func forEach(_ body: (Element) -> Void) {
for element in self {
body(element)
}
}
クロージャを保存したり、後から呼び出すなどしてエスケープさせたい場合は @escaping を付けます。
var handlers: [() -> Void] = []
func register(_ handler: @escaping () -> Void) {
handlers.append(handler) // エスケープするので @escaping が必要
}
エスケープするかどうかはコンパイラが静的に検出できるため、@escaping の付け忘れに対してはfix-it付きのエラーが出ます。逆に、かつての @noescape はそのままではコンパイルエラーとなり、「削除してください」というfix-itが提示されます。
@autoclosure との組み合わせ
@autoclosure(escaping) という書き方は廃止され、@autoclosure と @escaping を並べる形に統一されます。
// 旧: @autoclosure(escaping) () -> Bool
func assertLazy(_ condition: @autoclosure @escaping () -> Bool) { ... }
一時的にエスケープさせたいときの withoutActuallyEscaping
エスケープしないクロージャを、エスケープするクロージャを要求するAPI(たとえば LazySequence のメソッド群)に一度だけ渡したい、というケースのために、標準ライブラリに withoutActuallyEscaping(_:do:) というヘルパーが追加されます。クロージャを「あたかもエスケープするかのように」扱えるスコープを作り、そのスコープを抜けるまでに本当にエスケープしていないことをランタイムで検証します。もし実際にエスケープしていた場合は trap します。
func yourFunction(fn: (Int) -> Int) { // fn はエスケープしない
withoutActuallyEscaping(fn) { fn in // この fn は @escaping として扱える
somearray.lazy.map(fn) // @escaping を要求するAPIに渡せる
}
}
シグネチャは次のようになっています。
func withoutActuallyEscaping<ClosureType, ResultType>(
_ closure: ClosureType,
do: (fn: @escaping ClosureType) throws -> ResultType) rethrows -> ResultType
Objective-C / C APIのインポート
Cocoaのブロック引数はデリゲートのように エスケープするもの が多数派です。デフォルトを反転させるとCocoa APIの多くが壊れてしまうため、Clangインポータはインポート時に、Cの ((noescape)) 属性が明示的に付いていない限り、Objective-Cブロックや関数ポインタの引数に自動的に @escaping を補います。これにより、Cocoa APIの使用側ではSE-0103による書き換えはほとんど必要ありません。
既存コードへの影響
自分で @noescape を書いていた箇所は、単に属性を削除するだけで済みます。逆に、これまでデフォルトの扱いでエスケープさせていたクロージャ引数のうち、本当にエスケープしているものは @escaping を付け足す必要があり、コンパイラがfix-itで教えてくれます。エスケープするクロージャはエスケープしないクロージャより 制約が強い 側なので、呼び出し側には影響しません(エスケープする想定で渡されたクロージャが、実はエスケープしない関数に渡されるのは常に安全)。
Future Directions
SE-0073 として提案されていた「ちょうど一度だけ呼び出されるクロージャ」を表す @noescape(once) は、今後もし採用される場合には @once という独立した属性として整理される可能性があります(SE-0073 自体は Rejected となっており、現時点で実現を約束するものではありません)。