Swift Digest
SE-0061 | Swift Evolution

Add Generic Result and Error Handling to autoreleasepool()

Proposal
SE-0061
Authors
Timothy J. Wood
Review Manager
Dave Abrahams
Status
Implemented (Swift 3.0)

01 何が問題だったのか

Swift 標準ライブラリの autoreleasepool 関数は、Objective-C ランタイムの autorelease pool を Swift から使うための API です。当初のシグネチャは戻り値もエラー送出もサポートしておらず、次のような形をしていました。

public func autoreleasepool(code: () -> Void)

この形だと、プール内部で計算した結果を外側に渡したり、内部で発生したエラーを呼び出し側に投げ直したりするために、一時変数を使って値を受け渡す定型コードが必要でした。

func doWork() throws -> Result {
    var result: Result? = nil
    var error: ErrorProtocol? = nil
    autoreleasepool {
        do {
            // 実際の計算。result に代入できないパスもあるかもしれない
        } catch let e {
            error = e
        }
    }
    guard let result = result else {
        throw error!
    }
    return result!
}

このパターンには次のような問題がありました。

  • ボイラープレートが多い: 結果とエラーを受け取るためだけに Optional の一時変数を 2 つ用意し、try/catch で手動ブリッジする必要がありました。
  • コンパイラによる安全性チェックが効かない: 「結果が入るはず」「エラーが入るはず」という状態を実行時の guard と強制アンラップに頼って担保しており、条件の取り違えで nil を強制アンラップしてしまう危険がありました。
  • 意図が読み取りにくい: autoreleasepool で何を計算してどう返したいのか、という本来の意図が周辺の受け渡しコードに埋もれてしまいます。

さらに、ユーザー側で autoreleasepool をラップして汎用版を自作する方法も試されましたが、rethrows は受け取ったクロージャの呼び出しからしかエラーを再送出できないため、非 throwsautoreleasepool を呼び出すラッパーから rethrows 付きで throw し直すことができず、標準ライブラリ側で対応する必要がありました。

02 どのように解決されるのか

autoreleasepool のシグネチャがジェネリックな戻り値と rethrows に対応する形に変更されました。Swift 3.0 で実装されています。

新しいシグネチャ

public func autoreleasepool<Result>(body: () throws -> Result) rethrows -> Result

ポイントは次の 3 つです。

  • 戻り値型 Result がジェネリックになり、クロージャが返した値をそのまま呼び出し側に返せます。
  • クロージャが throws になり、プール内部で投げたエラーをそのまま伝播できます。
  • 外側の関数は rethrows なので、クロージャが throw しない場合は try なしで呼び出せます。

あわせて、引数ラベルが code から body に変更されました。標準ライブラリの他の同種 API で使われている命名に揃えるための変更です。

使い方

先ほどの例は、一時変数もガードも不要になり、意図がそのまま書ける形になります。

func doWork() throws -> Result {
    return try autoreleasepool {
        // 値を返すか、エラーを投げる
    }
}

クロージャが値を返すだけで throw しない場合は try も不要です。

let count = autoreleasepool {
    expensiveComputation()
}

rethrows のおかげで、クロージャが throw するかどうかに応じて呼び出し側に try が必要かどうかがコンパイラによって正しく判定されます。「結果が入っているはず」「エラーが入っているはず」という状態を手動で管理する必要がなくなり、強制アンラップも不要です。

実装

標準ライブラリ内部の実装は、プールの push / pop を defer で挟んでクロージャの戻り値をそのまま返すだけのシンプルなものです。

public func autoreleasepool<Result>(body: () throws -> Result) rethrows -> Result {
    let pool = __pushAutoreleasePool()
    defer {
        __popAutoreleasePool(pool)
    }
    return try body()
}

defer により、クロージャが正常終了した場合でも途中で throw した場合でも pool の pop が確実に実行されるため、従来どおりプールの寿命管理は安全に行われます。