エラー処理まわりを見ていく日々が続いておりますけれど、せっかくなので今回も The Basics
の Error Handling
を見ていく中で出会った Error
と CustomNSError
と LocalizedError
と NSError
とで、どのようなテキスト出力の違いが生まれていくのかみたいなところを眺めてみます。細かな話になりますけれど、どれを使うのがいちばん良さそうかみたいな判断材料を備える機会になったらいいなと思います。どうぞよろしくお願いしますね。
今回は参加者の一般参加者を募って、ゆめみの外の人たちも訪れての開催になる見通しです。
—————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #188 00:00 開始 00:37 今回の展望 03:39 Linux 環境で NSError は使える? 05:45 LocalizedError プロトコル 07:05 CustomNSError プロトコル 09:24 NSError 型 11:02 LocalizedError を使っていけば良さそう 11:13 エラーに関するプロトコルにおける挙動の違い 12:39 各エラーに準拠させた型を準備 16:46 NSError を継承したクラスを定義 21:45 誰かの投げた NSError を Swift のエラーと区別なく扱いたい 26:07 localizedDescription で詳細な情報を得られる例 29:08 localizedDescription で詳細を伝える実装 31:01 LocalizedError が使いやすい印象 31:36 errorDescription がオプショナル型なのは疑問 32:30 これまでを通しての所感 34:11 クロージング ——————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #188
はい、始めていきますね。
今日はこれまでエラーハンドリングの話をしてきましたが、その延長線上で、新たに取り上げたい点がいくつか出てきました。これを今日は詳しく見ていきたいと思います。前回はエラー、ローカライズドエラー、カスタムNSエラー、そしてもう一つNSエラーについて話しました。この辺りの特徴をもう少し押さえると、使用方法が変わってくるのではないかと思います。
前回の話では、ローカライズドエラーが良さそうだねという話でした。しかし、それも確実な判断材料が揃っているわけではなくて、少なくともローカライズドエラーの実装が簡単だから楽だよね、という程度の評価でした。では本当にそれが最適なのか、カスタムNSエラーは駄目なのか、NSエラー型を使った方が良いのではないか、など色々と検討の余地があります。
この四択の中で、どれが一番無難か、という点をしっかり見ていきたいと思います。まずは、Mac環境を使ってNSエラーを試していきます。
NSエラーはFoundationに定義されています。カスタムNSエラーもFoundation、ローカライズドエラーもFoundationですね。Foundationの直下に定義されています。コアFoundationも含めてNSエラーとNSオブジェクトを拡張したメソッドもいくつかあります。このメソッドはあまり使うことはありませんが、とりあえず何が使えるか見てみましょうか。
例えば、Linux環境ではどうなのか確認してみます。Linux環境でもNSエラーが使えるか、近々と見てみると良いでしょう。もし使えたら、MacだけではなくLinux環境でも対応できるということになります。具体的にはSSHで繋いでいるLinux環境を使ってみますね。例えば、import Foundation
として、プリントしてみて使えるかどうか確認します。
まず、エラー型とNSエラー、ローカライズドエラーのメタタイプを取ってプリントしてみます。メタタイプが取れるかどうか、そして型が適用できるか確認するためです。カスタムNSエラーでもアソシエティブタイプを使ってもなんとかなるのか、とりあえず試してみましょう。結果として、Linux環境でも型自体は存在していることが確認できました。
以上を踏まえて、Mac環境に戻りますが、この四択のどれが適しているのか見ていきましょう。定義については前回見ていったので、今日はローカライズドエラーの詳細を見てみます。ローカライズドエラーは、エラーディスクリプションを提供するプロトコルで、適切にエラーを表示できます。純粋なプロトコルでエラーから継承され、エラーディスクリプションを持たせることで使える感じですね。
Swiftらしいという点でも、ローカライズドエラーが一番良い感じがします。目的がローカライズドメッセージを提供することなので、特に実害もなさそうです。
次にカスタムNSエラーですが、このプロトコルはNSエラーのカスタム表現を提供するものです。アプローチとしては、NSエラーを明示的に変換し、コードや設計として提供するものになります。ローカライズドエラーがエラーディスクリプションを提供することに注目したプロトコルであるのに対し、カスタムNSエラーはドメイン、コード、ユーザーインフォを提供するものです。
つまり、それぞれのエラーの特色に合わせて使い分けることが重要になります。今回は特にローカライズドエラーがSwiftらしくて使いやすそうだな、という結論を持った上で、さらに詳細に調査していきましょう。 このコメントは少しわかりにくいところもありますが、カスタムNSエラーに関するディスカッションが重要なポイントです。コメントと名前については逆になっているように見えますが、今回は名前の方に重きを置いているようですね。NSエラーをカスタムするためのプロトコルCustomNSError
があり、ブリッジ先がNSエラーしか想定していない場合には役立ちます。ただし、これだけに依存するのは限定的かもしれませんね。
カスタムNSエラー
を提供しつつ、ローカライズドエラーも提供するといった形で両方を使っていくのが良いかもしれません。中には、より詳しい定義が必要な場合もあります。一般的には、NSエラー
は安定していて、フレームワークに対して使用するのが想定されています。「Foundation」にあり、NSError
もちゃんとセンダブル(Sendable
)として使えるようになっていますが、オープンプラス(open +)なのでセンダブルかどうかを守る必要があります。リニュータブルクラスで作ることで問題を避けられるでしょう。
プロトコル上はゲットしかないので、大丈夫ですね。NSError
もLocalizedError
プロトコルを持っているので、ローカライズドディスクリプションは既に考慮されています。これを踏まえると、ローカライズドエラーのさらにリッチなバージョンとして捉えると良さそうです。NSError
はローカライズドディスクリプションに加えて、ユーザーインフォ、コード、ドメインが付加されます。
これを考慮しながら進めて、実際に動くコードを見ていきましょう。昔、自分がエラーを使っていたとき、Cocoaから返ってくるエラーと、自分が定義したSwiftのネイティブなエラーで互換性がなかったので、いろいろ調べていった結果、NSError
に準拠すると大半はうまくいくという結論に至ったという経験があります。
では、独自のエラーを作ってみましょう。ここでは、MyError
をエラープロトコルに準拠させます。以下のようなエラーを定義します。
enum MyError: Error {
case someError
case anotherError
}
extension MyError: LocalizedError {
var errorDescription: String? {
switch self {
case .someError:
return NSLocalizedString("Some error occurred.", comment: "")
case .anotherError:
return NSLocalizedString("Another error occurred.", comment: "")
}
}
}
このようにして、独自のエラーに適用することで、エラーの内容をローカライズドディスクリプション付きで表示できます。また、CocoaとSwiftのエラーハンドリングにおいても一貫性を持たせることができます。さらに詳細な動作や違いを実際に確認して整理していきましょう。バージョンに関しても注意しながら実装していくのがポイントです。 この段階では、カスタム NSError
を設定するための4つのエラータイプを定義して、それぞれのインスタンスを作成した後、標準的な NSError
クラスを継承して独自のエラーを作成する方法について説明しています。
まず、純粋なインスタンスエラーとして以下のように定義します。
Swiftエラー
LocalizedSwiftError
カスタムNSError
LocalizedカスタムSwiftエラー
これらのインスタンスは、全て型の名前で出力されるようです。次に、NSError
をカスタムして独自のエラーを作成する方法を紹介します。以下に示すコードで新しいカスタム NSError
クラスを作成しています。
class SubNSError: NSError {
// 必要なプロパティやメソッドをここに定義
}
クラス SubNSError
は NSError
を継承しており、その中で独自のエラーメッセージを定義することができます。しかし、イニシャライズの過程でクラッシュが発生しているようで、正しく初期化する必要があります。
次に、init(coder:)
イニシャライザが必要で、以下のようにします。
required init?(coder: NSCoder) {
super.init(coder: coder)
}
そして、カスタムイニシャライザも追加します。
init(domain: String, code: Int, userInfo: [String : Any]?) {
super.init(domain: domain, code: code, userInfo: userInfo)
}
これで、独自のエラーがカスタマイズされ、エラーメッセージを持つことができます。例えば、次のようにしてエラーを生成できます。
let customError = SubNSError(domain: "com.example.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "This is a custom error."])
NSError
はカスタマイズ可能でエラーメッセージを詳細に管理するために使用されます。特に、localizedDescription
プロパティを利用することで、エラーメッセージを表示することが一般的です。
これまでの説明では純粋に Swift
エラーやカスタムエラーを使っていたが、ここで独自にエラーを定義することで、単純なエラー出力から詳細なエラーメッセージ付きの出力に変わることができます。
全体のコードは以下のような感じになります。
class SubNSError: NSError {
required init?(coder: NSCoder) {
super.init(coder: coder)
}
init(domain: String, code: Int, userInfo: [String : Any]?) {
super.init(domain: domain, code: code, userInfo: userInfo)
}
}
let customError = SubNSError(domain: "com.example.error", code: 1, userInfo: [NSLocalizedDescriptionKey: "This is a custom error."])
print(customError.localizedDescription)
このようにすると、NSError
が持つ基本的な機能を拡張し、カスタマイズすることができます。また、localizedDescription
を利用することで非常に有用なエラー情報を提供することができます。
これで、NSError
のカスタマイズとエラーメッセージの管理についての基本が理解できたかと思います。次に進むときは、他のプログラミング言語やソフトウェア理論にも目を向けながら、さらに深掘りして学んでいきましょう。 エラーが返ってくる部分について、例えばファイルマネージャーのエラーを取得してみましょう。エラーコードを6に設定します。この辺でエラーを取得するかな?いや、下で取得しましょう。
レッドエラーをエラーコードとして設定し、それを使ってエラーハンドリングをします。以下のコードでファイルマネージャーのエラーをキャッチします。
let fileManager = FileManager.default
do {
try fileManager.removeItem(atPath: "somePath")
} catch {
print("エラー: \\(error.localizedDescription)")
}
このスニペットでは、エラーが発生した場合にそのエラーメッセージを print
で出力しています。エラーコードをエラー変数に設定して、エラーカラーなローカライズディスクリプションを表示します。
次に、カスタムエラーを作成する際には、標準の LocalizedError
プロトコルを利用するのが良いでしょう。以下のようにカスタムエラーにローカライズディスクリプションを持たせることができます。
struct MyCustomError: LocalizedError {
var errorDescription: String? {
return "カスタムエラーメッセージ"
}
}
このようにしておくと、カスタムエラーでもローカライズディスクリプションが利用でき、エラーメッセージを一貫して取得できます。たとえば、APIから受け取ったエラーとカスタムエラーを一緒に扱う場合には以下のようになります。
do {
throw MyCustomError()
} catch let error as LocalizedError {
print("エラー: \\(error.localizedDescription)")
}
この形にしておくと、APIのエラーとカスタムエラーが一貫して取り扱えるようになり、コードの可読性やメンテナンス性が向上します。標準ライブラリからのエラーも、カスタムエラーも同じようにハンドリングできるので、非常に便利です。
さて、ここで例をもう一度振り返ります。LocalizedError
プロトコルを利用しない場合のエラーハンドリングのコードと比較してみましょう。カスタムエラーメッセージを追加して、本来のエラー処理を続けることが重要です。
ただし、LocalizedError
プロトコルのオプショナルな特性やデフォルト実装については少し思案する点があるかもしれません。なぜオプショナルにしたのか、なぜデフォルト実装を提供しなかったのか、という疑問は残ります。それでも、このプロトコルを利用することでよりSwiftyなコードを書くことができます。
では、今日はこの辺で終了しましょう。お疲れ様でした。ありがとうございました。