【Swift】エラーハンドリングと文字列
iOS開発でエラーハンドリング時のエラーメッセージの実装が間違えているような気がしたのでその調査メモです.
恥ずかしながら今まではCustomStringConvertibleやコンバータークラスを作成していました.
間違い等ありましたら指摘してくださると嬉しいです.
ErrorとNSError
Foundation/UIKit等を利用していると稀にNSErrorというものを見かけます.
SwiftのErrorとの違いは次の点が違います.
Error
Errorはdo-catch構文やResult型で補足することができる抽象型のプロトコルです.
Swiftで定義されているErrorプロトコルに準拠したオブジェクトを例外として投げることが可能で,クラスや共用型含む列挙型等に準拠させることができます.
NSError
Foundationで定義されたクラスで,エラーの発生とそのエラー情報を伝える使い方をします.
クラスはNSObjectを継承したクラスですが,SwiftのErrorプロトコルにも準拠しているためErrorと同様の使い方ができます.
以上がErrorとNSErrorの違いですが,一言で表すと
Errorはエラーの発生を知らせる事が可能.NSErrorはエラーの発生とその情報もセットで知らせることが可能.
という認識をしておけば大丈夫です.
NSError エラー情報
NSErrorはエラーの情報も格納できると上述しました.
エラーの情報は次のプロパティで取得することが可能です.
class NSError: NSObject { /// ローカライズされたエラーを説明する文字列 var localizedDescription: String { get } /// ローカライズされたアラートの選択肢として表示する文字列 var localizedRecoveryOptions: [String]? { get } /// ローカライズされたエラーへの対処方法を表す文字列 var localizedRecoverySuggestion: String? { get } /// ローカライズされたエラーの発生原因を表す文字列 var localizedFailureReason: String? { get } }
ローカライズされているということは,UIへの表示を前提にされたものだということが推測できます.
API呼び出しなどではなくNSErrorのインスタンスを生成したい場合はuserInfoにローカライズされた文字列を格納します.
let nsError = NSError(domain: "nserror", code: 1, userInfo: [ NSLocalizedDescriptionKey: "localized description", NSLocalizedRecoveryOptionsErrorKey: ["localized recovery options"], NSLocalizedRecoverySuggestionErrorKey: "localized recovery suggestion", NSLocalizedFailureReasonErrorKey: "localized failure reason" ]) print(nsError.localizedDescription) print(nsError.localizedRecoveryOptions) print(nsError.localizedRecoverySuggestion) print(nsError.localizedFailureReason)
macOS向けネイティブアプリ開発に用いられるCocoaでは アラートを表示するAPIの引数にNSErrorを渡すことでラベル等に適切に表示してくれるものもあります.
let nsError = ... NSAlert(error: nsError).runModal()
LocalizedError
FoundationのNSErrorのようにエラー情報を保持可能で,SwiftのプロトコルとしてErrorを利用したい場合はLocalizedErrorというFoundationで定義されたプロトコルが利用可能です.
LocalizedErrorは次のプロパティが定義された,Errorを継承したプロトコルです.
public protocol LocalizedError : Error { var errorDescription: String? { get } var failureReason: String? { get } var recoverySuggestion: String? { get } var helpAnchor: String? { get } }
先程の例として載せたNSErrorのプロパティと雰囲気が似ていると思います.
実際にプロトコルに準拠させた列挙型を定義して実際に利用してみると次のようになります.
enum MyError: LocalizedError { case a case b var errorDescription: String? { "error description" } var failureReason: String? { "failure reason" } var recoverySuggestion: String? { "recovery suggestion" } var helpAnchor: String? { nil } } do { throw MyError.b } catch { // MyError.errorDescription let myError = error as! MyError print(myError.errorDescription) // Error.localizedDescription print(error.localizedDescription) } // 両方とも"error description"が出力
定義したMyErrorのlocalizedDescription
はどのケースでも"error description"としています.
例を見ると引数の異なるprint()
が2回呼び出されていますが,その出力は両方とも同じです.
FoundationのextensionでErrorプロトコルにlocalizedDescription
というプロパティが追加されており,エラーの実装がNSErrorでもErrorでも意図した文字列(ローカライズされたエラーの説明)を返すという動作をします.
localizedDescription
がNSErrorとErrorの違いを吸収してくれるので,利用者はキャスト等の処理を挟まなくて済みます.
NSErrorと同じくCocoaAPIのAlertにMyError.bを渡してあげると,意図した形でアラートが表示されたことが確認できました.
NSAlert(error: MyError.a).runModal()
まとめ
Swiftのエラーハンドリングで型を定義する際にはLocalizedErrorを使いましょう.
エラーメッセージはError.localizedDescription
で定義しておくと,NSErrorとの違いを吸収してくれるので積極的に利用したいです.