Advanced Swift メモ - 8. Error Handling
8. Error Handling
- Swiftにはいくつものエラーハンドリングの仕組みがある。
- Optional、はシンプルだがエラー情報は返せない。
- Assertion、はバグを早期発見するために利用できる。
- 例外、はOptionalと違い詳細なエラー情報を持てる。
- CollectionTypeのfirst/lastなどはOptionalだが、失敗の理由が「配列が空」しかないので適切。
- ネットワークエラーなどは、失敗の理由を知りたいこともあるはずなのでErrorTypeが良い。
The Result Type - P.248
enum Result<A> { case Failure(ErrorType) case Success(A) }
- Swiftのエラーハンドリングは、殆どResultTypeと同じように実装されている。
throws
で宣言された関数を呼び出した時は、キャッチするか伝播させる必要がある。- try-catchは他の言語と似ているが、Swiftはランタイム上の処理コストは殆どかからない。
- これは単純なreturn処理としてコンパイラが扱うため。
- ResultTypeとの大きな違いは、エラーが型として定義されていないこと。
- 厳格な型情報が欲しければResultTypeを使うと良い。
- でも標準のtry-catchを使ったほうがシンプルではあるかも。
Errors and Objective-C Interface
NSError
ポインタを受けるようなObjective-Cの関数はthrows
に変換される。- エラーポインタの引数は除外され
- 戻り値で成否を返すようなものは
Void
になる。
- (NSString *)contentsOfFile(NSString*)fileName error:(NSError **)error;
func contentsOfFile(fileName: String) throws
Errors and Function Parameters - P.253
- すべての要素が条件を満たすか判定するメソッド
all
は以下のように実装できる。
extension SequenceType { func all(@noescape check: Generator.Element -> Bool) -> Bool { for el in self { guard check(el) else { return false } } return true } }
- しかし上記のバージョンでは
throws
宣言された関数を渡すことが出来ない。
func isEven1(x: Int) -> Bool { return x % 2 == 0 } func isEven2(x: Int) throws -> Bool { return x % 2 == 0 } (1..<10).all(isEven1) // OK (1..<10).all(isEven2) // Compile error
- そこで
map
などの標準メソッドは以下のようにrethrows
で宣言されている。
extension SequenceType { func all(@noescape check: Generator.Element throws -> Bool) rethrows -> Bool { for el in self { guard try check(el) else { return false } } return true } } try! (1..<10).all(isEven2) // OK
Cleaning Up Using defer - P.255
guard let database = openDatabase(...) else { return } defer { closeDatabase(database) } guard let connection = openConnection(database) else { return } defer { closeConnection(connection) } guard let result = runQuery(connection, ...) else { return }
defer
は他の言語のfinally
と似ているが少し異なる点もある。- try/doブロックに繋げる必要はなく、どこでも書くことが出来る。
defer
を同一ブロックで複数宣言した場合、逆順に実行される(スタックのように)- セグメンテーションフォルトあるいはfatal errorなどの場合は実行されない(プログラムはクラッシュする)
Errors and Optionals - P.256
if let contents = try? parseFile("Hello.md") { print(contents) }
- エラーがthrowされなかった場合だけ処理したいときは
try?
が使える。 - Optionalが
nil
だった場合に、代わりの例外をthrowするような関数は以下のように書ける。
func optional<A>(value: A?, onError e: ErrorType) throws -> A { guard let x = value else { throw e } return x } let int = try optional(Int("42"), onError: ReadIntError.CouldNotRead)
try?
キーワードは、エラーを無視するべきではないというSwiftの哲学とは矛盾しているようにも見える。- しかし、
try?
は明示的に書く必要がある。 - あなたがエラーの詳細な内容について興味がないときには、これは便利である。
Chaining Errors - P.257
- 例外が
throw
される可能性のあるメソッド呼び出しがチェーンしても、if
などのようにネストしたりしない。 - 例外が発生した場合は、
catch
にコントロールが移動して処理される。
func checkFilesAndFetchProcessID(filenames: [String]) -> Int { do { try filenames.all(checkFile) let contents = try contentsOfFile("Pidfile") return try optional(Int(contents), onError: ReadIntError.CouldNotRead) } catch { return 42 } }
- これを
Result
で実装することも簡単に出来る。 optional
のように成功の場合は中身を取り出して適用し、失敗の場合は何もしない。- そうしたコードはとてもエレガントになる。
func checkFilesAndFetchProcessID(filenames: [String]) -> Int { return filenames .all(checkFile) .flatMap { _ in contentsOfFile("Pidfile") } .flatMap { contents in Int(contents).map(Result.Success) ?? .Failer(ReadIntError.CouldNotRead) } }
Higher-Order Functions and Errors - P.258
- この本を執筆している段階では、Swiftのエラーはコールバック関数に向いていない。
- 単純なケースでは、コールバックの引数を
optional
にすることで解決できる。nil
が入っていた場合は失敗をみなす、というように。
func compute(callback: Int -> ()) // 結果をIntで返す func compute(callback: Int? -> ()) // 失敗した場合はnil
- 詳細なエラー情報を返すために、最初は以下のようにして
throws
宣言しようと思うかもしれない。
func compute(callback: Int throws -> ())
- しかし、この宣言は完全な誤りで、型ではなく関数に修飾する
throws
の使い方としてはNG。 - この宣言を
Result
で書きなおした場合どうなるかやってみると、間違いであることに気づく。
func compute(callback: Int -> Result<()>)
- 我々が欲しいのは、以下のようにコールバック関数の引数が
Result
であるものだ。
func compute(callback: Result<Int> -> ())
throws
を使った、現在ではあまり分かりやすくない書き方もある。
func compute(f: (() throws -> Int) -> ())
- これを利用する側はさらに複雑になる。
compute { (theResult: () throws -> Int) in do { let result = try theResult() print(result) } catch { print("An error happend: \(error)") } }
- なので
Result
は非同期エラーハンドリングの選択としては良い。 - しかし、同期関数で
throws
をすでに使用している場合、Result
を使用している箇所との食い違いで、APIが使いづらく鳴るかもしれない。 - あなたが多くの非同期関数を書いている場合、トレードオフとして
Result
を使う価値はある。 - しかし、一つのコールバックしかないようであれば、先ほどのネストした関数を利用するのも良い選択肢だ。
Conclusion - P.260
- AppleがSwift 2.0にエラーハンドリングを導入した時、多くのイケてないことが発生した。(訳間違ってるかも)
- Swift 1.xでは、殆ど
Result
型がエラー処理に使われていた。 - 実際、型付けされていない
throws
は、嬉しいような、そうでもないような気持ちがあった。(意訳) throws
の利点として、型シグネチャの宣言がシンプルであるという点がある。- 例えば、複数のエラーが発生する可能性のある関数の場合、以下のようになっていたかもしれない。
func checkFilesAndFetchProcessID(filenames: [String]) throws ReadFileError, CheckFileError, MiscellaneousError -> Int // この書き方はしんどい
- しかし、これは大きな欠点も残した。
- どのエラーが発生したか明示できず、余分なボイラープレートコードを必要とした。
- さらに、
throws
が関数のみに作用するため、不必要な複雑さを生むことになった。(非同期コールバックのように)
Swiftが80%に最適化された、実用的な言語であると考えると、この欠点はシンプルな振る舞いから外れる。
- 組み込みのエラーハンドリングを利用することで、関数を結果に包んだり、不必要な複雑さをもたらす。
- そしてあなたが思っているように、我々は曖昧なエッジケースについてここでは言及していない。(非同期コールバックのみ)
もし特別なエラー情報が欲しい場合、
Result
型(エラーをジェネリック型で表現したもの)を使用することが出来る。- しかし、これは他の複雑さをあなたのコードに持ち込むことにもなる。
あなたが作っているものによっては、この複雑さを持ち込む価値はあるかもしれない。
このようにコード中の期待しない動作をハンドリングする仕組みは多くある。
- 継続できないのであれば
fatalError
またはAssertion - エラーの種類に興味がないか、1種類に限定できるのであれば
Optional
- エラーの種類が必要か、追加情報が欲しい場合、Swift標準の例外か、
Result
型 - 関数を引数に受け取る関数を宣言するときは、
rethrows
を使うことで、throws
宣言された関数も受け取れるようになる - そして
defer
は標準のエラーを扱うときにとても便利である defer
を使ったコードはスコープを抜けるときにクリーンアップとして必ず実行される(throws
あるいはreturn
など)
- 継続できないのであれば