ErrorTypeを拡張してより使いやすく

Friday, February 26, 2016

Swift2.0からErrorTypeが登場し、enum等に適応させることでエラーの種類を簡単に実装できるようになりました。

enum SomeErrors: ErrorType {
    case ErrorA
    case ErrorB
    case ErrorC(String)
}

func doSomething() throws {
    throw SomeErrors.ErrorA
}

//----- (A)
do {
    try doSomething()
} catch let error {
    print(error)
}

//------ (B)
do {
    try doSomething()
} catch SomeErrors.ErrorA {
    print("error!")
} catch SomeErrors.ErrorC(let msg) {
    print("error! \(msg)")
}

といった感じでdo~try~catchでエラーをキャッチしたときにその内容をprintできたり(A)、パターンマッチで特定のエラーをキャッチする(B)が簡単に行なえます。
また、よくあるResult型などで失敗時にErrorTypeを突っ込むことができたりします。
(B)のようにパターンマッチで条件を分けたい場合だけなら問題ないのですが、(A)のようにエラーの内容をprintしようと思うとちょっと困ったことがあります。

問題点


実際に(A)のパターンでerrorをprintすると、ErrorAと出力されます。 たったそれだけです。
ちなみに、ErrorTypeはNSErrorへのasを使ったキャストが許容されていて、

print(error as NSError)

としてあげると、
"Error Domain=SomeErrors Code=0 "(null)"
と、少しだけ出力が変わります。NSErrorと同じ形式ですね。
ただ、これも問題があって、これが仮にErrorB,ErrorCだとしても、 domaincodeuserInfo は同じものになってしまいます。

これも不便。。
なので、

  • 独自の domaincode をenumのcase毎に付けられるようにしたい/変えたい
  • userInfo を付与できるようにしたい
  • 普通にprintした時にもNSErrorと同様の出力をしたい
  • as NSErrorでキャストした場合にも独自に設定した domaincode を適応したい

といった点をprotocolを使って解決します。

実装


今回はgistのembedを試してみました。

使ってみる


先ほどのSomeErrorsEnhancedErrorTypeに準拠させて、必要な処理を追記していきます。

enum SomeErrors: EnhancedErrorType {
    case ErrorA
    case ErrorB
    case ErrorC(String)

    var domain: String {
        return "SomeErrorCustomDomain"
    }

    var code: Int {
        switch self {
        case .ErrorA: return -10000
        case .ErrorB: return -10001
        case .ErrorC( _): return -10002
        }
    }

    var userInfo: [NSObject: AnyObject]? {
        switch self {
        case .ErrorA:
            return [NSLocalizedDescriptionKey: "ErrorA occurred."]
        case .ErrorB:
            return [NSLocalizedDescriptionKey: "ErrorB occurred."]
        case .ErrorC(let msg):
            return [NSLocalizedDescriptionKey: "ErrorC occurred. message: \(msg)"]
        }
    }

}

func doSomething() throws {
    throw SomeErrors.ErrorC("wahaha")
}

do {
    try doSomething()
} catch let error {
    print(error as NSError) // 1
    print(error) // 2
    print((error as! SomeErrors).toNSError()) //3
}

結果として、

1) Error Domain=SomeErrorCustomDomain Code=-10002 "(null)"
2) Error Domain=SomeErrorCustomDomain Code=-10002 "ErrorC occurred. message: wahaha" UserInfo={NSLocalizedDescription=ErrorC occurred. message: wahaha}
3) Error Domain=SomeErrorCustomDomain Code=-10002 "ErrorC occurred. message: wahaha" UserInfo={NSLocalizedDescription=ErrorC occurred. message: wahaha}

が得られます。

解説


まず、NSErrorに必要な domaincodeuserInfo をprotocolに宣言します。

public protocol EnhancedErrorType: ErrorType {
    var domain: String { get }
    var code: Int { get }
    var userInfo: [NSObject: AnyObject]? { get }
}

そして、extensionを使ってデフォルトの挙動を与えて、使う側が全て実装しなくても動くようにしておきます。

extension EnhancedErrorType {
    var domain: String {
        return String(reflecting: self.dynamicType)
    }

    var code: Int {
        return 0
    }

    var userInfo: [NSObject: AnyObject]? {
        return nil
    }
}

加えて、NSErrorに変換するtoNSError()も定義します。

    public func toNSError() -> NSError {
        return NSError(domain: domain, code: code, userInfo: userInfo)
    }

これで使う側が独自に設定したい domaincodeuserInfo を別途宣言しなおせば、toNSError()を介してしっかりとしたNSErrorを吐き出すことができます。
ですが、まだ問題点が2つ残っていますね。

  • 普通にprintした時にもNSErrorと同様の出力をしたい

こちらは、EnhancedErrorTypeに、CustomStringConvertibleを継承させて、extensionでdescriptionを定義します。

public protocol EnhancedErrorType: ErrorType, CustomStringConvertible {
    // 省略
}

extension EnhancedErrorType {
    // 省略
    var description: String {
        return "\(toNSError())"
    }
}

これで、

do {
    try doSomething()
} catch let error {
    print(error)
}

としたときも、NSErrorと同様の出力結果が得られるようになります。

  • as NSErrorでキャストした場合にも独自に設定した domaincode を適応したい

さて、ここまで処理を追記することで、よりNSErrorに近づいたわけですが、

    print(error as NSError) // Error Domain=SomeErrors Code=0 "(null)"

とした時に、まだ出力結果が おかしいまま です。
これをどうにかするには、ErrorTypeが (隠し) 持つ _domain_code を実装し直してあげます。
現在はオープンソースでErrorTypeの実装を見ることができ、ここで、 _domain_code が実装されているのを確認することができます。

extension EnhancedErrorType {
    var _domain: String {
        return domain
    }

    var _code: Int {
        return code
    }
    // 省略
}

これで、 domaincode で宣言したものが、 _domain_code でも使われるようになるので、無事

    print(error as NSError) // Error Domain=SomeErrorCustomDomain Code=-10002 "(null)"

と表示されるようになります!
このNSErrorのキャストの場合、userInfoだけは、どうしても欠けてしまいますが。
この変換っぽいことを定義している箇所が、おそらくこの辺りかと思います。

これで、よりErrorTypeをNSErrorに近づけつつ、使えるようになるんじゃないかなと思います!
Gistはこちらです。

techSwiftTips

enumの要素を配列で取得したい

Travisでcarthageを使う場合