Add @noescape to public library API
01 何が問題だったのか
Swift には、クロージャを引数に取る関数に対して「このクロージャは関数呼び出しの外にエスケープしない」ことを示す @noescape 属性があります。@noescape が付いていると、コンパイラは self のキャプチャ/保持/解放を省くなどの最適化を行えますし、クロージャ内で self. を省略できるなどの構文上の恩恵もあります。
func withLock(@noescape perform closure: () -> Void) {
myLock.lock()
closure()
myLock.unlock()
}
非エスケープなのに属性が付いていないAPIが多い
Foundation や libdispatch など、Objective-C/C 由来のAPIには、意味的にはクロージャ(ブロック)がエスケープしないにもかかわらず、Clang側の __attribute__((noescape)) が付いていないものが数多く存在しました。Clang でこの属性が付いたブロック引数はSwiftに取り込まれる際に @noescape として見えますが、属性が無ければSwiftからは「エスケープするかもしれないクロージャ」として扱われてしまいます。
その結果、本来なら得られるはずの最適化や構文ショートカット(たとえばクロージャ内での self. 省略)が、これらのAPIを使ったときに効きませんでした。
Swift側からは回避できない
純粋なSwiftコードでは、インポートされたAPIの宣言を後付けで @noescape 扱いにする手段がありません。回避策としては、C/Objective-C 側に自前のラッパー関数を用意し、そこで __attribute__((noescape)) を付けてSwiftに見せる、という面倒な手順が必要でした。
// MyProject-Bridging-Header.h
NS_INLINE void MyDispatchSyncWrapper(dispatch_queue_t queue, __attribute__((noescape)) dispatch_block_t block)
{
dispatch_sync(queue, block);
}
ライブラリ側のヘッダに直接属性を書いてしまえば、利用者がこうしたラッパーを書く必要はなくなるはずです。この提案は、CoreFoundation と Foundation のヘッダに属性付与用のマクロを導入し、クロージャがエスケープしないことが明らかな主要APIにそのマクロを適用しよう、というものでした。
02 どのように解決されるのか
この提案は Rejected(却下) となりました。Swift Evolution のプロセスを経た「提案」としては採択されなかったものの、提案されていた内容自体(CoreFoundation / Foundation のヘッダに noescape 用マクロを導入し、非エスケープなクロージャ引数に付与する作業)は、その後フレームワーク側の通常のメンテナンスの一環として実施されています。そのため、現在の Foundation などのAPIは、エスケープしないクロージャ引数には適切にこの属性が付けられた状態でSwiftから見えます。
提案されていた内容(却下されたもの)
-
CoreFoundation に
CF_NOESCAPEマクロを導入する。#if __has_attribute(noescape) #define CF_NOESCAPE __attribute__((noescape)) #else #define CF_NOESCAPE #endif -
Foundation ではそのエイリアスとして
NS_NOESCAPEを導入する。#define NS_NOESCAPE CF_NOESCAPE -
libdispatch / Foundation / CoreFoundation のAPIを監査し、クロージャ(ブロック)が関数呼び出しの外にエスケープしないものを洗い出して、そのブロック引数に
CF_NOESCAPE/NS_NOESCAPEを付与する。
たとえば CFArrayApplyFunction や、NSArray の enumerateObjectsUsingBlock:、sortedArrayUsingComparator: など、列挙系・ソート系のブロック引数がまとめて対象として挙げられていました。
void CFArrayApplyFunction(CFArrayRef theArray, CFRange range, CFArrayApplierFunction CF_NOESCAPE applier, void *context);
- (void)enumerateObjectsUsingBlock:(void (NS_NOESCAPE ^)(ObjectType obj, NSUInteger idx, BOOL *stop))block;
- (NSArray<ObjectType> *)sortedArrayUsingComparator:(NSComparator NS_NOESCAPE)cmptr;
これらのAPIに属性が付けば、Swift 側では対応する引数が @noescape として取り込まれ、クロージャ内で self. の省略や self の保持が不要になるなど、純粋なSwift APIと同じ水準の取り扱いが期待できる、という趣旨でした。
採択されなかった理由
この提案はSwift言語そのものの変更ではなく、CoreFoundation/Foundation/libdispatch といったフレームワーク側のヘッダに属性を付けていく作業に相当します。そのため、Swift Evolution で個別のAPIリストを提案として審議するよりも、フレームワーク側の通常のAPIメンテナンスとして粛々と進めたほうが適切だ、という整理で Rejected となりました。
利用者として知っておくべきこと
- 現在の Foundation などのAPIでは、エスケープしないクロージャ引数には属性が付与済みで、Swift側でも適切に非エスケープとして扱われます。利用者がSE-0012を意識する必要はありません。
- Swift のクロージャに関するエスケープの扱いは、その後 SE-0103 で「エスケープしないのがデフォルト、エスケープするなら
@escapingを付ける」という形に反転しました。現行Swiftでは@noescape属性は廃止されており、エスケープしないクロージャは単に属性なしで表現します。SE-0012 で議論されていたC/Objective-C側のnoescape属性付与は、この現行仕様の下でも引き続きインポート時に意味を持ちます。