Add Generic Result and Error Handling to autoreleasepool()
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 は受け取ったクロージャの呼び出しからしかエラーを再送出できないため、非 throws の autoreleasepool を呼び出すラッパーから 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 が確実に実行されるため、従来どおりプールの寿命管理は安全に行われます。