はじめに
Swift6からTyped throw
がサポートされ、throwするエラーの型を指定できるようになりました。そこで今回はTyped throw
の基本的な使い方と、使うべき場面について調べたことをまとめます。
基本的な使い方
エラーを返す関数にthrows(エラーの型)
のようにすることで、スローするエラーの型を指定することができます。
enum SampleError: Error {
case invalidData
}
func errorFunction() throws(SampleError) {
throw .invalidData
}
このerrorFunction
は常にSampleError
をスローすることが保証されています。そのためdo-catch
でキャッチできるエラーはSampleErrorになります。
do {
try errorFunction()
} catch {
print(error) // SampleError
}
SampleError
をスローされるとは保障されていない関数とともに呼び出すと、キャッチされるエラーはany Error
になります。
enum SampleError: Error {
case invalidData
}
func errorFunction() throws(SampleError) {
throw .invalidData
}
func anyErrorFunction() throws {
throw SampleError.invalidData
}
do {
try errorFunction()
try anyErrorFunction()
} catch {
print(error) // any Error
}
ちなみに既存のthrowの書き方は、any Error
型のTyped throwと同じです。
// この2つは同じ
func hoge() throws {...}
func hoge() throws(any Error) {...}
またthrowしない関数は、Never
のTyped throwと同じです。
func hoge() {...}
func hoge() throws(Never) {...}
Typed throwは変更に弱い
Typed throwは一見すると便利な機能ですが、限定的な場面以外では基本的に推奨されていません。Typed throwのプロポーザル1にも以下のように記載されています。
The error values themselves are always type-erased to any Error. This approach encourages errors to be handled generically, and remains a good default for most code.(エラー値自体は、常に任意のErrorに型消去されます。 このアプローチは、エラーを一般的に扱うことを推奨しており、ほとんどのコードにとって良いデフォルトであることに変わりはない。)
その理由はTyped throwは変更に弱いからです。
例えば以下のような関数があるとします。
func performTask() throws(MyError) {
throw MyError.networkFailure
}
のちに新しいエラー型をスローする必要に迫られた場合、関数のシグネチャを変更しなければなりません。シグネチャを変更するということは、この関数の呼び出し元すべてに影響を与えるということです。
逆にTyped throwを使わなかった場合、
func performTask() throws {
throw MyError.networkFailure
}
のちに新たなエラー型をスローすることになっても、関数内部のコードを変更するだけで済みます。
このような背景があり、ほとんどのケースではTyped throwを使うべきではありません。
Typed throwを使うべき場面
ではTyped throwを使うべき場面はというと、プロポーザルに記載されています。
- エラーハンドリングが、モジュールやパッケージに閉じていること
- rethrows や Result のようにそれ自体がエラーを生み出さず、ただ通過するだけの場合
- オーバーヘッドが許容できない場合
1つ目はシンプルに、エラーがモジュール内で完結していて、外部に公開されていないパターンです。
3つ目は詳しくわかりませんが、容量的にTyped throwを使った方がいいケース?
個人的に2つ目の言っていることがわからなかったので、少し深掘りしてみました。
rethrowsをTyped throwに置き換える
rethrow
には引数として受け取ったクロージャ以外のところでエラーをスローできないという制約があります。
func countMatching(_ numbers: [Int], predicate: (Int) throws -> Bool) rethrows -> Int {
var count = 0
var caughtError: Error?
for number in numbers {
do {
if try predicate(number) {
count += 1
}
} catch {
caughtError = error
break
}
}
if let error = caughtError {
throw error // ⭐️A function declared 'rethrows' may only throw if its parameter does
}
return count
}
このコードではクロージャとは別の場所でエラーをスローしているため、ビルドエラーが発生します。
そこでTyped throwとジェネリクスをうまく使えば、この問題を解決することができます。
func countMatching<E: Error>(_ numbers: [Int], predicate: (Int) throws(E) -> Bool) throws(E) -> Int {
var count = 0
var caughtError: E?
for number in numbers {
do {
if try predicate(number) {
count += 1
}
} catch {
caughtError = error
break
}
}
if let error = caughtError {
throw error
}
return count
}
このコードは問題なく動作します。
このようにその関数自体がエラーを発生させるのではなく、ただエラーを伝播させる場合はTyped throwが適しています。
ちなみにmap
やcount(where:)
もTyped throwで実装されています。
public func map<T, E>(_ transform: (Element) throws(E) -> T) throws(E) -> [T] where E : Error
public func count<E>(where predicate: (Element) throws(E) -> Bool) throws(E) -> Int where E : Error
参考にした記事
- https://speakerdeck.com/koher/swift-6notyped-throwstoswiftniokeruerahandoringunoquan-ti-xiang-woxue-bu
- https://note.amazia.co.jp/n/n31f131be06d7
- https://speakerdeck.com/toshiyana36/swift6karanotyped-throws