Swift Digest
SE-0012 | Swift Evolution

Add @noescape to public library API

Proposal
SE-0012
Authors
Jacob Bandes-Storch
Review Manager
Philippe Hausler
Status
Rejected

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から見えます。

提案されていた内容(却下されたもの)

  1. CoreFoundation に CF_NOESCAPE マクロを導入する。

     #if __has_attribute(noescape)
     #define CF_NOESCAPE __attribute__((noescape))
     #else
     #define CF_NOESCAPE
     #endif
    
  2. Foundation ではそのエイリアスとして NS_NOESCAPE を導入する。

     #define NS_NOESCAPE CF_NOESCAPE
    
  3. libdispatch / Foundation / CoreFoundation のAPIを監査し、クロージャ(ブロック)が関数呼び出しの外にエスケープしないものを洗い出して、そのブロック引数に CF_NOESCAPE / NS_NOESCAPE を付与する。

たとえば CFArrayApplyFunction や、NSArrayenumerateObjectsUsingBlock: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 属性付与は、この現行仕様の下でも引き続きインポート時に意味を持ちます。