今回も The Basics
の Error Handling
を見ていく中で気になった LocalizedError
まわり、エラーを表現するためのプロトコルまわりを引き続いて眺めてみます。前回でその特色的なところは紹介できたので、今日は他に見落としがないかを気にしながらその定義まわりを中心に見ていこうと思います。どうぞよろしくお願いしますね。
今回は参加者の一般参加者を募って、ゆめみの外の人たちも訪れての開催になる見通しです。
————————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #187
00:00 開始 01:03 エラー表現に関するプロトコルや型 01:38 存在するエラー型を探してみる 05:49 DecodingError と CodingError の概要 06:40 ErrorPointer 型の概要 09:16 NSError は Objective-C 由来 10:19 URLError の様子 14:16 MachError の様子も少しだけ見てみる 15:37 CocoaError と POSIXError の概略 15:53 CFError は CoreFoundation のエラー 16:36 CancellationError は Concurrency まわり 17:10 IOURLError は IOKit のエラーな様子 18:13 用意されているエラーに関する全体的な印象 18:55 Error プロトコルの定義を眺める 19:09 Error に準拠するなら Sendable 必須 19:46 Sendable への準拠の必要性 20:26 Sendable への準拠の合成について 21:34 モジュールを跨いだときの Sendable への適合性 22:35 Sendable ラッパーを使って回避する悪い事例 23:44 Error プロトコルは既定の実装を持たない様子 25:43 LocalizedError の定義 27:17 CustomNSError の定義 28:42 全体を通した所感とクロージング —————————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #187
では、始めていきましょう。今回はエラーハンドリングについてです。このスライドは3回ぐらい話しましたが、もはや使っていないですね。このセッションで気になったエラー周りの話をしていきます。前回は Result
や Error
プロトコル、LocalizedError
プロトコルなどについて話しました。今回も LocalizedError
の話が中心ですが、それだけに限らず、エラープロトコル周りの定義をもう少し詳しく見てみようと思います。
具体的には、Error
プロトコル、LocalizedError
、そしてカスタムエラープロトコルの3つです。この3つの定義を見ていこうと思いますが、ほかに何かエラー周りのプロトコルや型について知っている方がいれば教えてください。
自分が知っているのは、追加で DecodingError
や EncodingError
ですね。これらは Codable
に付随するエラーです。他にエラー周りで何があるでしょうか。NSError
もそうですね。さらに URLError
なんかもあります。あとは NSURLError
ですか。「マッチエラー」と言っていますけど、それは何でしたっけ?失礼しました、それは MachError
ですね。また、OSの話としては CoreError
や POSIXError
もあります。
さらに、特定のページには CodingError
もあるようですね。他に、AXError
というものも見かけますが、これは何でしょうね。アクセシビリティ関連のエラーのようです。また、CFError
は Foundation
の一部っぽいです。そして、CGError
も見かけますね。これは CoreGraphics
の一部です。CancellationError
もあります。これは並行処理に関連するエラーかと思います。
ここまででざっと見てみると、これらのエラーやプロトコルにはいくつかのパターンがあります。知らなかったのは gglError
や ioError
、mtlIOError
などもありますね。現在インポートしているライブラリは AppKit
までですので、ここから具体的なコードを書いていこうと思います。
例えば、DecodingError
は Codable
プロトコルに付随するエラーですが、これを使う場面では特に Codable
に限定されないエラーでもあります。ただし、CodingPath
などが明らかに Codable
の仕様に依存していますので、一緒に使うのが無難です。エンコーディングエラーも同様に Codable
で使うエラーですね。
では次に、NSErrorPointer
型についてもお話ししましょう。これは「オートリリーシングアンセーフミュータブルポインター」というものがあるようです。言い換えると、C++で言うスマートポインターに近いものです。 これについて、なかなか良さそうですね。その中でもNSエラーを扱うポインターに関してですが、なるほど、使いますね。昔ながらのCocoaライブラリーで、パラメーターでエラーを返すようなときに、NSエラーのオプショナル型をコピーする代わりに、直接ポインターで渡す方法があります。エラーは参照渡しのように、アンド(&
)をつけた書き方で渡すことが多いですが、NSエラーポインター型を用意しておけば良いのですね。
エラーに対して、例えばNSエラーのオプショナルを作り、それをアンドで渡すという形を取ることがよくありますが、確かにこの方法を知っておけば便利に使える機能ですね。この方法はエラープロトコルとは直接関係がないとも言えますが、コンテナとして持っているので間接的に関連しています。
NSエラーはObjective-C由来のエラー型で、Swiftの面白いところは、NSエラーとSwiftのエラーの両方が完全に統合ブリッジできる点です。これは、Objective-CとSwiftのブリッジが可能になっているためです。
エクステンションに関しては、特段エクステンションをしているわけではなさそうですが、暗黙のうちにエラープロトコルに準拠しているでしょう。Objective-Cのブリッジもされていると思われるため、結果的にはこの話題と関連があるということですね。
URLエラーに関しては、Foundationに含まれています。エラードメインやエラーコードなどが含まれており、ローバリューはint型で保持されているようです。URLエラーはネットワークタスクのエラー情報を細かく持つため、便利です。
これが構造体として定義されており、エラープロトコルに準拠しているか確認するには、少なくともエクステンションでは確認できませんでした。ただ、URLエラーがエラープロトコルに準拠させても良さそうですが、エラーの型の指定などで制限があるのでしょう。
イニシャライザを確認することで、URLエラーにどんなケースがあるのかを見ましたが、その都度コードやケースを渡す形になりそうです。こうすることで、エラーコードやエラードメインに合わせたエラーオブジェクトを作成できます。
確かに、イニシャライザを呼ばないと完全には理解できなかった部分もありますが、init
メソッドでコードを渡せば、目的のエラーオブジェクトを生成することができるのですね。実際に準拠して、動作することを確認しました。 別のソースファイルに書かれているのかもしれませんが、とりあえずその部分は準拠しているので問題ないです。
NSエラー系のコードを試してみたのですが、エラーを通過するためにはビックリマークが必要みたいですね。なぜエラーに明示的に準拠させていないのか、理由があるのでしょう。ココアエラーも同じような仕様かもしれません。
LogixエラーはLinuxの標準仕様です。CFエラーはコアファンデーションのエラーで、これはコアライブラリーに含まれるのかもしれません。Linuxで使用できるかどうかは分かりませんが、コアファンデーションといえば、昔のココアやカーボンの話にも関連しているのかもしれません。
また、キャンセレーションエラーはコンカレンシーのエラーで、イニシャライザーが存在します。これも標準ライブラリーに含まれるようです。トラクターやI/Oエラーに関連するエラーも見つけましたが、I/Oエラーはファンデーションに関連して出てくるのかもしれません。
I/Oエラーはデバイスドライバーを作るときに使用するライブラリー用のエラーのようで、エラープロトコルには準拠していないようでした。これも試してみたところ、エラープロトコルに準拠していなかったので、独自のエラーを作る必要がありそうです。
独自のプロトコルに準拠させたエラーを設計するうえで、プロトコルの性格を確認すると、エラーはSendable
プロトコルに準拠する必要があります。これはスレッドセーフでエラーインスタンスを投げることができるという意味です。コンカレンシーでエラーを扱う時にスレッドまたぎの可能性があるため、Sendable
であることが重要です。
しかし、このSendable
をつける必要があることを考えると面倒に感じるかもしれません。設計者がコンカレンシーに対応しているかどうかを意図して作らなければならないのは理解できますが、少し煩わしいと思うことがありますね。 構造体を作って Sendable
に準拠させる場合、全てのメンバーが Sendable
だったら自動的に Sendable
になるんでしょうか。ちょっと自信がなくなってきましたが、そういったことができるなら良い気がします。しかし、「Sendable」に関しては、何かもっと理解が必要な印象がありますね。
今、「Sendable」と書きましたが、これが何か理解できない時もあるんです。このとき、この構造体を Sendable
にしたときに初めて決まるというケースです。確かに構造体は適合されている場合がありますが、書かなくても Sendable
になっていることがあります。
「Sendable」については、理解すべきことが多いと感じます。私の理解では、同じモジュール内で確保的に Sendable
に準拠している場合は、明示的に宣言する必要はありません。しかし、モジュールが変わると、 Sendable
の安全性が暗黙的には保証されないため、明示的に宣言しないといけなくなるのです。
私もモジュールが変わると Sendable
の暗黙的な安全性ではダメで、影響が出るのかなと思います。将来的にはもっと明示的な宣言が必要になるかもしれません。モジュールを作っている人自身が Sendable
を保証できるうちは良いですが、API的に Sendable
をやめられないという覚悟の証明が必要なのです。
このように、 Sendable
じゃないために、ライブラリを作成する際にかなり苦労する場面があります。良くない方法ではありますが、 UnwrapSendable
といった型を作成し、一時的に安全だとみなすためにラッパーを作ったこともあります。ただし、これが良い方法ではないことは明らかなので、極力使いたくないし、別の方法を模索するべきだと思います。
話がそれましたが、エラーの定義を見ると、 Sendable
を要求していますが、それ以外は要求していません。規定の実装は標準ライブラリの中ではなさそうですね。前回も見ましたが、独自のエラーでも LocalizedError
のようなプロパティが使えるようになっています。これは Foundation
が機能を追加してくれた結果で、エラー自体には何もインポートされていない状態でも LocalizedError
のプロパティが使えるのです。
例えば、エラーが発生したときに localizedDescription
を出力しようとすると、これは Foundation
です。定義する際に Foundation
に行き着き、 Foundation
内でエクステンションされて localizedDescription
が追加されているわけです。このため、基本的に特殊な機能は用意されていないシンプルなプロトコルという感じになります。ジェネリクスを使えば更に柔軟に対応できるでしょう。 なので、そんなに特殊な機能としては何も用意されていないクリーンなプロトコルという感じですね。この辺りをきちんと確かめて、ジェネリクスを使えばいいということです。ファウンデーションとして何らかのエラーを取るようにしておけばいいでしょう。あそこが結局エクステンションしているから出てしまうのですが、試せなかったですね。ネイティブなものを試す上では、プレイグラウンドでは少し難しそうです。
エラーがどのようにローカライズされるのか、どのメソッドが用意されているのかを見ていくと、ファンデーションをインポートしないとたどり着けない部分があるかもしれません。ローカライズドエラーはエラーに準拠していて、エラーディスクリプションやフェーラーリーズン、リカバリーサジェスチョン、そしてヘルプアンカーを含んでいます。こんなに多くの項目が設定されているのは面白いですね。細かくレポートされるのでしょうか。
一般的に使うのはエラーディスクリプションですが、フェーラーリーズンやエラーディスクリプションも規定されています。ヘルプアンカーやヘルプテキストもあります。ローカライズドエラーのエクステンションでは、要は規定の実装がされているので、特に明示的にメソッドを追加しなくてもプロトコルに準拠できます。これは利益の利かせ方と言えるでしょう。
特に特殊なものはなさそうですが、付加情報が追加できます。カスタムNSエラーも同様で、付加情報がNSエラーを想定したものになっています。エラードメイン、エラーコード、ユーザーインフォはNSエラーに慣れている人なら理解しやすいでしょう。これらも規定の実装がされています。カスタムNSエラーも特に明示的に何かを追加しなくても、通常に準拠できます。必要であれば、エラーコードをカスタマイズできます。
カスタムNSエラーが fixed with integer low value
を low representable
で表現した場合には、エラーコードがカスタム実装されます。これ自体がエラーコードとして使えますので便利ですね。汎用的なエラーコードをSwiftらしいエラーに変換したいときに役立ちます。意外とエラー周りのプロトコルはシンプルな実装になっています。
どのプロトコルを使用するかについてですが、私は3行目の普通のエラーをよく使っていました。しかし、Swiftのプロトコルを考えると、4行目のローカライズドエラーをメインに使っていくのが良さそうです。カスタムNSエラーと両方に準拠する必要はないのかもしれません。ローカライズドエラーだけで済みそうな気がします。これは今見てきた情報よりも、なんとなくのイメージですが。
今日は時間になったので、ここで終わりにします。エラーに関して興味深い部分があれば次回も取り扱いますし、そうでなければ別のエラー関連の話題に移るかもしれません。次回の内容について準備してから決めますね。では、今日はこれで終わりにします。お疲れ様でした。ありがとうございました。