[Swift][Objective-C] 同期的な関数ハンドラを示す noescape ディレクティブ

2015.07.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

関数オブジェクトのライフサイクル

Objective-CのblocksでもSwiftのクロージャでも、非同期な処理は関数オブジェクトをメソッド等に渡して非同期ハンドラとすることが多いです。

// なにかのクラスの中
func someTask() {
    let url = NSURL(string: "http://google.com")!
    NSURLSession.sharedSession().dataTaskWithURL(url) { data, response, error in
        // メソッド呼び出しの時、返り値を束縛する際にはselfをつけないとエラーとなる
        let aa = self.duplicateAndDoubled(1)        
    }
}

func duplicateAndDoubled(elem: Int) -> [Int] {
    return [elem * 2, elem * 2]
}

この例では、クラスでの非同期な処理の場合には self が関数オブジェクトにキャプチャされることを明示的にするため、dataTaskWithURL メソッドのハンドラ内でコンパイラーに self をつけるように言われます。

コレクションオブジェクトのマッピング等でも関数オブジェクトは用いられますが、その場合はハンドラが同期的に処理されるために、関数オブジェクトのライフサイクルは呼び出し元のクラスを超えることが無いです。そのため、さきほどのようなコンパイラーのエラーは出したくないです。

@noescape

そんなときには Swift 1.2 で導入された @noescape ディレクティブを関数オブジェクトの直前につけ、関数オブジェクトが同期的に呼び出され、スコープ外にエスケープするタイミングで呼び出されないことを明示的にできます。

func someSyncHandler(@noescape handler: Void -> Void) {
    handler()
}

func someTask {
    someSyncHandler {
        // メソッドのスコープよりもハンドラ関数オブジェクトのライフサイクルは長くない
        // したがってインスタンスを関数オブジェクトにキャプチャしないため、
        // `self` を明示的にしなくてよい
        let arr = duplicateAndDoubled(1)
        print(arr)
    }
}

この例では someSynkHandler の関数オブジェクトハンドラに @noescape ディレクティブを付与しているために、duplicateAndDoubled メソッドの呼び出し時には呼び出し元のクラスのインスタンスが必ず存在することが保証されています。

そのため、コンパイラは self をハンドラ内のメソッド呼び出しでつけるようには言ってこないです。

このような @noescape ディレクティブを用いることで、コンパイラのエラーのお陰でクラスと用いるのに難しかった以下のような構文を定義できます。

public func unless(condition: BooleanType, @noescape procedure: Void -> Void) {
    if !condition.boolValue {
        procedure()
    }
}

unless(1 > 2) {
    // 条件式が偽であるときにこの中が実行される
    print("strange!")
    let array = duplicateAndDoubled(4)
}

この unless 構文は if 文の逆バージョンで、条件文が偽の時にブレースの中身の処理が走ります。

Swiftの標準APIでも主にコレクションを扱うAPI (flatMap等…) に @noescape ディレクティブは導入されています。

/// Return an `Array` containing the results of calling
/// `transform(x)` on each element `x` of `self` and flattening the result.
func flatMap<U>(transform: @noescape (T) -> [U]) -> [U]

__ attribute __ ((noescape))

尚、Objective-Cでも同様の機能をもったディレクティブとして、

__attribute__((noescape))

が提供されており、以下のように使用できます。

- (void)invokeBlockNow:(__attribute__((noescape)) void (^)(void))blk { // 同期的なBlock処理を保証
    blk();
}

引用