search
LoginSignup
2

More than 1 year has passed since last update.

posted at

updated at

[Swift] Resultで例外をいい感じに処理する

注意

これはネタエントリーです

Swiftの公式見解は「例外は例外としてちゃんと処理しろ。Resultはそういうものじゃねぇ」です。
ご理解の上お読みいただけますようお願いいたします。

なにこれ?

例外処理を do - try - catchを使わずResultで処理しちゃおうという奴です。

準備

こちらのエントリにある ifSuccess / ifFailure を利用しますので確認しておいてください。

じゃあやってみようか

特に難しいことをしようというのではありませんのでコードをドーンと出しちゃいます。

以下のようなコードをResultで書き換えちゃいます。

struct HogeError: Error {}
func hoge(_ s: String) throws -> Int {
  guard let i = Int(s) else { throw HogeError() }
  return i
}

do {
  print(try hoge("123"))
}
catch {
  print(error)
}

do {
  print(try hoge("abc"))
}
catch {
  print(error)
}

とくに解説はいりませんね。

では書き換えます。

struct HogeError: Error {}
func hoge(_ s: String) throws -> Int {
  guard let i = Int(s) else { throw HogeError() }
  return i
}

Result { try hoge("123") }
  .ifSuccess { print($0) }
  .ifFailure { print($0) }

Result { try hoge("abc") }
  .ifSuccess { print($0) }
  .ifFailure { print($0) }

僕にはかなり読みやすいのですが、皆さんはどうでしょう?

エラーを複雑にしてみる 1

例外処理では複雑な例外に対処する必要が多分に発生します。
まずはenumな例外を見てみましょう。

enum HogeError: Error {
 case notNumber
 case minus
 case lessThan10(Int)
}
func hoge(_ s: String) throws -> Int {
  switch Int(s) {
  case nil: throw HogeError.notNumber
  case (...0)?: throw HogeError.minus
  case let n? where n < 10 : throw  HogeError.lessThan10(n)
  case let n?: return n
  }
}

do-try-catchでは以下のようになります。

do {
  print(try hoge("abc"))
}
catch HogeError.notNumber {
  print("not number")
}
catch HogeError.minus {
  print("minus")
}
catch let HogeError.lessThan10(n) {
  print(n, "is less than 10.")
}
catch {
  print("we caught unknown error.", error)
}

ごく普通です。

ではこれをResultで処理してみましょう。
そのために新たなstructを導入しResultのextensionに手を加えます。
長いので読み飛ばしましょう

struct ResultErrorHandler<Failure: Error> {
  let failure: Failure?
  let handled: Bool

  func ifFailure<F: Error>(as type: F.Type, _ f: (F) -> Void) -> Self {
    if let error = failure as? F {
      f(error)

      return .init(failure: failure, handled: true)
    }

    return self
  }

  func ifFailure(_ f: (Failure) -> Void) {
    if handled { return }
    guard let failure = failure else { return }

    f(failure)
  }
}

extension Result {
  @discardableResult
  func ifSuccess(_ f: (Success) -> Void)-> Self {
    if case let .success(v) = self { f(v) }
    return self
  }

  func ifFailure(_ f: (Failure) -> Void) {
    if case let .failure(error) = self { f(error) }
  }

  func ifFailure<F: Error>(as type: F.Type, _ f: (F) -> Void) -> ResultErrorHandler<Failure> {
    guard case let .failure(error) = self else {
      return .init(failure: nil, handled: false)
    }
    if let e = error as? F {
      f(e)
      return .init(failure: error, handled: true)
    }

    return .init(failure: error, handled: false)
  }
}

ここにJump

これで準備が出来ましたのでResultを使った例外処理に書き換えてみましょう。

Result<Void, Error> { print(try hoge("abc")) }
  .ifFailure(as: HogeError.self) { error in
    switch error {
    case .notNumber: print("not number")
    case .minus: print("minus")
    case let .lessThan10(n): print(n, "is less than 10.")
    }
  }
  .ifFailure { error in
    print("we caught unknown error.", error)
  }

読み飛ばしていただいたextensionで実装されたifFailure(_:)メソッドに渡された関数/クロージャは、その前に実行されたifFailure(as:)メソッドがすでにエラーハンドリングを終えている場合は実行されません。
つまりこの場合ですとFailureがHogeErrorしか起こりえませんので、ifFailure(_:)メソッドに渡した関数/クロージャが実行されることはありません。

残念ながらenumの個別のcaseごとに処理が分けられません。
この点はdo-try-catchの方がよいかもしれません。
ただし、こちらの場合はswitchによるcaseの網羅性チェックが入りますのでどちらが良いかは微妙です。
もちろんその方法もdo-try-catchで記述可能です。

エラーを複雑にしてみる 2

複数のtypeのエラーが投げられる場合。

例外が投げられる可能性のあるAPI呼び出しを含むメソッドが独自の例外を投げる可能性もある。という場合などです。
メソッド内でAPI側の例外を処理しろって話ですが、今回はそういうのはなしで。

enum HogeError: Error {
 case notNumber
 case minus
 case lessThan10(Int)
}

struct FugaError: Error {}

func getString() throws -> String {
  throw FugaError()
}

func hoge() throws -> Int {
  switch Int(try getString()) {
  case nil: throw HogeError.notNumber
  case (...0)?: throw HogeError.minus
  case let n? where n < 10 : throw  HogeError.lessThan10(n)
  case let n?: return n
  }
}

HogeErrorに加えてFugaErrorを定義しました。
また文字列を取得するthrowableなgetString()関数を定義し、関数hoge()はそれを利用して文字列を取得するように変更しました。

ではこれをdo - try - catchを使って書いてみます。

do {
  print(try hoge())
}
catch HogeError.notNumber {
  print("not number")
}
catch HogeError.minus {
  print("minus")
}
catch let HogeError.lessThan10(n) {
  print(n, "is less than 10.")
}
catch is FugaError {
  print("Can not get string.")
}
catch {
  print("we caught unknown error.", error)
}

catch is FugaErrorが追加されただけで他は変わっていません。

ではこれをResultを使って書き換えます。

Result<Void, Error> { print(try hoge()) }
  .ifFailure(as: HogeError.self) { error in
    switch error {
    case .notNumber: print("not number")
    case .minus: print("minus")
    case let .lessThan10(n): print(n, "is less than 10.")
    }
  }
  .ifFailure(as: FugaError.self) {
    print("Can not get string.")
  }
  .ifFailure { error in
    print("we caught unknown error.", error)
  }

こちらも.ifFailure(as: FugaError.self)が追加されただけで他は変わっていません。

エラーを複雑にしてみる 3

Errorに継承関係がある場合
ちょっと特殊だとは思いますがErrorに継承関連がある場合を考えます。

class E0: Error {}
class E1: E0 {}

func fuga(_ e1Flag: Bool) throws -> String {
  if e1Flag {
    throw E1()
  }

  throw E0()
}

これをdo - try - catchで書いてみます。

do {
  print(try fuga(true))
}
catch let error as E0 {
  print("E0", error)
}
catch let error as E1 {
  print("E1", error)
}
catch {
  print("Other", error)
}

特に問題はありませんね。
と思いますがこれは問題ありです。
出力結果はこうなります。

E0 E1

投げられた例外はE1です。しかしcatchしたのはE0としてです。
do - try - catchではswitch-caseと同様に記述された順番に検査され最初に合致したcatchのみが実行されそれ以降は無視されます。
E1はE0でもあるためこのような結果を得ることになります。

ただしい書き方はこうなります。

do {
  print(try fuga(true))
}
catch let error as E1 {
  print("E1", error)
}
catch let error as E0 {
  print("E0", error)
}
catch {
  print("Other", error)
}

E1(継承先、子孫)の場合をE0(継承元、親、先祖)の前に記述します。

ではこれをResultで書き換えます。

Result<Void, Error> { print(try fuga(true)) }
  .ifFailure(as: E0.self) { error in
    print("E1", error)
  }
  .ifFailure(as: E1.self) { error in
    print("E1", error)
  }
  .ifFailure { error in
    print("Other", error)
  }

出力結果

E0 E1

先に提示したextensionで実装されたifFailureメソッドのシリーズはメソッドチェーンによる記述となるため記述した順に実行されるのは自明です。
また、一度処理を実行した場合はそれ以降の処理を行わない設計となっていますので、出力結果はdo - try - catchと同様になります。
こちらも以下のように書き換えることで対応します。

Result<Void, Error> { print(try fuga(true)) }
  .ifFailure(as: E1.self) { error in
    print("E1", error)
  }
  .ifFailure(as: E0.self) { error in
    print("E1", error)
  }
  .ifFailure { error in
    print("Other", error)
  }

これで意図した結果が得られます。

まとめ

いかがでしたでしょうか?

適切なextensionを導入することでdo - try - catchを使用するのと遜色なくResultを例外処理機構として利用することができることがわかりました。

結論

だからといってやっちゃだめ。

冒頭に書いたとおり、Swift開発陣は例外は例外として扱いdo - try - catchを使用することを推奨しています。
また、Resultを例外処理機構の一部として利用することを想定していません。

あくまでもネタエントリーであることをご理解いただけますようお願いします。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
2