前回から The Basics
の Error Handling
を眺めはじめて、まずはその基礎となる特徴部分を見ています。今回もその続きで、エラーハンドリングを使うときの場面やその扱い方についておさらいしていく予定です。前回の続きにあたるオプショナルとの感覚的な違いや、前回に教えてもらった String
をエラーとしても扱えるようにする方法など、話せたらいいなと思ってますけれど、果たして時間があるかどうか。何はともあれそんな感じで Error Handling
についてじっくり見渡してみようと思ってますので、どうぞよろしくお願いしますね。
今回は参加者の一般公募もされていまして、ゆめみ社外な人の参加されての開催になります。
———————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #180
00:00 開始 01:20 エラー処理の大事なところ 03:00 標準ではエラー型がほぼ用意されていない 04:42 オプショナルを用いたエラー処理 05:58 汎用的なエラーがあると使いがちに 06:14 独自の型でエラーの詳細を通知する 08:56 予期しないエラーも想定する必要がある 09:56 検査例外と非検査例外 10:35 あらゆる可能性の中からエラーを検査 10:55 将来に追加されるかもしれないエラーにも対処 13:33 自動で宣言される error 定数 14:03 エラー処理での網羅性を検査させたいとき 15:37 そこまで精密にエラー処理する機会はないかも? 16:19 独自のエラーで元のエラーをラップする場面 17:31 エラー全体をキャッチしてから個別に判定していく? 18:50 リカバリー出来る場合に並列で記載するのは良さそう? 20:43 catch 文でのパターンマッチング 22:25 エラーハンドリングの所感 22:47 エラーハンドリングをオプショナルパインディングに変換 24:55 エラーが起こる場合を想定しないとき 25:37 try? って使ってる? 26:57 エラー情報が落ちるのが気になる 27:28 try? を使えるかもしれない場面は? 28:43 エラー情報を握りつぶす点が気になる 29:31 横着したいときに使えるくらい? 30:49 エラーを扱いやすくなったことにも起因しそう 32:07 異常系については後日 32:34 文字列をそのままエラー情報として扱う 35:20 クロージング ————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #180
はい、それでは始めていきましょう。今日は前回からの続きで、エラーハンドリングの話に入っていきます。基礎的なところを前回も見ていましたが、『The Swift Programming Language』に沿って進めていますので、今回もその延長で話をしていきます。この勉強会はだいたい1時間ぐらいですが、今日もその延長みたいな感じで話していくことになるかと思います。
今回から参加する方でも全然問題なく続いていける内容ですし、前回参加していても理解しにくいことがあるかもしれません。その場合はコメントやマイクで質問してもらえれば補足説明を行いますので、気軽に聞いてみてください。
まず前回のおさらいですが、エラーハンドリングは実行時にストップするかもしれないエラーに対応するために使うもので、Swiftは安全性を考慮した言語になっているため、いくつかのアプローチが用意されています。エラーハンドリングは失敗の原因を特定できるようにし、必要に応じて別の部分に利用可能にするという点が重要です。
例えば、前回のコードではエラーハンドリングとオプショナルの使い分けについて話しました。標準のエラーが用意されていないため、エラーを返す方法が限られていると言いましたが、これは例えばJavaScriptやC言語、C++、Javaでは標準のエラー型があり、そこからさらに拡張することが可能です。しかし、Swiftにはこのような標準のエラー型がないため、エラー処理がシンプルになり、オプショナルを使ってエラー処理が行える点が個人的に評価されています。
標準のエラー型がないことのメリットとしては、エラーハンドリングの際にエラーの原因を特定できる点です。例えば、Int
型を返す関数があり、何らかの条件でエラーを返す可能性がある場合、呼び出し側でオプショナルバインディングを用いることでエラー処理をシンプルに行えます。これにより、Do-Catch
を使わなくても、取得できた値が nil
かどうかでエラーの処理が行えます。
ただし、エラー処理をオプショナルで取り扱うことが常に良いわけではなく、複雑なエラーハンドリングが必要な場合やエラーの種類に応じて異なる処理を行う必要がある場合には、きちんとエラー型を作って処理することが大切です。このような場合には、Swiftでもエラー型を定義し、その型を用いてエラー処理を行うことが推奨されます。
例えば、以下のようにエラー型を定義することができます:
enum MyError: Error {
case invalidValue
case systemError
}
そして、関数でエラーを返す場合には以下のようにします:
func someFunction() throws -> Int {
// 条件によってエラーを投げる
throw MyError.invalidValue
}
こうすることで、呼び出し元では Do-Try-Catch
を使ったエラーハンドリングが行えます:
do {
let value = try someFunction()
// 成功した場合の処理
} catch MyError.invalidValue {
// invalidValueエラーの場合の処理
} catch MyError.systemError {
// systemErrorエラーの場合の処理
} catch {
// その他のエラーの場合の処理
}
このように、エラーハンドリングを適切に使い分けることで、Swiftのコードはより明確で安全になります。今日はこのあたりを詳しく見ていきたいと思います。 次はJavaの場合について説明しますね。Javaでは、Swiftのようにどんな例外が発生するかわからないというパターンと、この例外のみが発生しますよというパターンを記載することができます。しかし、Swiftではそういった明示的に記載する方法が取られていません。Swiftでは基本的に、何かしらのエラーが発生する可能性があるという前提で書かれています。
例えば、あるライブラリのAPIを使用しているとします。現在主流の通信手段として、有線のネットワークと無線のネットワーク(LANとWi-Fi)、そして携帯電話のセルラーネットワーク(4Gや5Gなど)があります。これを想定してライブラリが作成されているとしましょう。しかし、もし今後新しい通信手段が登場した場合、例えば衛星通信などがAPIに搭載されると、それを利用できない場合のエラーが返されるようになります。この時、コードを修正しないとエラーの判定が困難になる可能性があります。
OSのバージョンアップによってフレームワークが変わり、例えば衛星通信のエラーが返されるようになった場合、このままのコードでは対応できないことがあります。将来的にもっと想定外の通信手段、例えばテレパシーなどが登場する可能性もあります。したがって、エラーハンドリングにおいては全ての可能性を想定する必要があるのです。
以前も説明しましたが、Swiftは柔軟なエラーハンドリングが可能で、バインディングパターンを使用しなくても、自動的にエラー変数を定義して受け取ることができます。例えば、do-catch
文を使うと、全てのエラーをキャッチして処理することが可能です。また、特定のケースだけではなく全てのケースを網羅的に処理するために、未定義のケースがあればコンパイルエラーを発生させるような書き方もできます。例えば、以下のように書くことができます。
do {
// エラーハンドリングが必要なコード
} catch let error as SomeSpecificError {
// 特定のエラーの処理
} catch {
// その他のエラーの処理
}
このように書くことで、未定義のエラーケースがあるとコンパイルエラーが発生し、網羅性を確保することができます。
ただ、ここまで精密にエラーハンドリングする場面はそれほど多くないかもしれません。例えば、ディスクアクセスなどのケースでは、物理的な故障かどうかや、一時的なエラーかどうかなど、エラー判定が重要になることもあります。しかし、そういったケースはあまり多くないでしょう。
エラーハンドリングには、ラップされた独自のエラーが内部に存在する場合もあります。例えば、特定のAPIを使用する際、独自のエラーでラップされて本当のエラーがその中に含まれているケースもあります。なので、こういったエラーハンドリングの精密な知識も重要です。 要するに、ここで説明しているのはインターナルエラーに関することですね。このパターンはよく見られるケースで、リクエストのエラーやレスポンスのエラーなど、さまざまなケースで発生します。こうしたエラーにどう対応すべきかという点では、キャッチしてから適切に処理することが重要です。
多くの場合、エラーは詳細な情報を収集して修正するというより、ユーザーに通知して対処してもらう方が一般的です。例えば、アラートを表示して「何か問題が発生しました」という旨をユーザーに知らせ、その後、ユーザーが問題を修正するために何か手を打つことが多いと思います。
ソフトウェアがエラーを自動で修正するケースは少ないですが、場合によってはリカバリーを試みることもあります。たとえば、タイムアウトが発生した場合に数秒後に再試行するなどです。
私がTwitterクライアントを作成した際には、認証エラーが発生したときにユーザーに再度認証を求めることがありました。このように、ユーザーの手を借りることが多いですが、一時的なデータ取得エラーの場合は少し待ってから再試行するなどのハンドリングを行います。
このようにエラーハンドリングを適切に分けて処理することが重要です。 Swiftのコードを書いていて面白いと感じることがあります。特に、キャッチのパターンマッチングにおいては、switch
文と同じように、イナムレーションパターンやアイデンティファイアパターンを使ってエラーハンドリングができます。この方法で、例えばインターナルエラーが発生した場合に関連値を扱うコードを書いてみると、「これを使ってみたい」と思うことがあるでしょう。
例えば、特定のエラーケースに対して網羅性を確保したコードを書くことができるので、この書き方は確かに便利だと言えます。特に、どのようなエラーが発生しても対応できるように設計されたコードが、簡潔でネストされない形で書ける点は大きなメリットです。
また、高度なエラーハンドリング技術を使ってコードを書いた場合、API を使用するときに精密な情報が必要な状況にも対応できます。例えば、関数の失敗時に詳細なエラーメッセージが必要ない場合には、Optional
に変換して簡単に処理ができるようになっています。つまり、この関数が失敗したときにどうするかを考えて、「メッセージはいらない」という場合には、if let
のような形で書きたいときに try?
を使って Optional
型に変換することができます。これにより、詳細な情報を伝えながらも、使いにくくなる心配は避けられるので安心です。
API を提供する側としては、呼び出し元が柔軟に判断できるようにするために、精密なエラーハンドリングを基にした API を提供して、必要に応じて Optional
化するかどうかを決められるようにするのがベストです。例えば、API にオーバーロードをわざわざ用意する必要はなく、必要に応じてそれぞれ使い分けるという考え方です。
さらに、アプリケーションが特定の値を取得できなかった場合、終了させる必要があるような状況でも、簡単にコードを書けるようになっています。try?
を活用すれば、エラーが発生したときにそのままアプリケーションを落とすこともできます。これが try?
と !
の使い分けで、適切にエラーハンドリングすることで、柔軟かつ堅牢なコードを書くことができます。しかし、try?
は非常に便利ですが、どれくらい一般的に使われているかというと、頻度はそれほど多くないかもしれません。適切な状況で使うことが重要です。 結果を多く使う場面は多いかなと思いますが、簡単なスキルチェックなどについても簡単に見ていきましょう。エラーをスローすることには意味があります。私はすべてのエラーにはなんらかの処理をするべきだと思っています。特にRxを使っている場合は、この点が楽になります。エラーをスローすると、エラーストリームイベントが流れるため、あまり気にすることがありません。
デフォルトチェーンのようにいろんな処理を実行する中でエラーが発生すると、ストリーム自体にエラーのイベントが流れます。そのため、個別にエラーに対応しようとすると難しく感じることがあります。エラーが発生したら普通にスローさせて、ルーティングでリカバリーを考える方が中断性が高いと感じます。
エラーをスローする際にエラーハンドリングを明示的にしたい場合に使用することが多いです。少なくとも、適切な対応をしないといけない場面がよくあります。例えば、以下のようなコードがあります。
do {
try someFunction()
} catch {
print("Error: \\(error)")
}
19行目から23行目のようなコードの場合、このようにエラーハンドリングすることでエラーの情報が流れてしまうのを防ぐことができます。もしエラーが発生したら、その理由を把握しやすくなります。
スローさせる場合、詳細なエラー情報を確保しつつ、エラーハンドリングをするべきだろうという意見もあります。例えば、ネットワークフェッチの際にデータを取れなくても致命的ではない場合、それがうまくいかなかったことを検知したくなるでしょう。そのため、スローの際にはエラーの詳細を保持しておくのが良いでしょう。
Playgroundで書く場合も同様です。コンソールプログラムでエラーを確認する場合など、エラーハンドリングの重要性が理解できると思います。エラーハンドリングをきちんと行うことで、より堅牢なアプリケーションを作成することができます。 基本的にあまり良くないパターンですね。Swiftでthrows
をつけなきゃいけなくなったというか、多くの呼び出し元までエラーを全部throws
しなきゃいけなくなった場合、結構面倒くさいかもしれません。関数の中で別の関数を呼んで、その時にエラーが発生する場合にも、エラーをthrows
しなければいけないという流れです。本来はその関数がエラーをthrows
しないんだけど、エラーをthrows
する関数を呼んでいるからエラーをthrows
しなきゃいけない、という状況になってしまいます。
結局、エラーを潰してうまく制御流に戻す、といった対応が求められることもあると思いますが、これは必ずしも理想的ではありませんね。話題が尽きないので、次回また続けることにしましょう。
これまでの中で特に興味深かったのは、エラーの情報をどのように伝えるかという部分です。GetErrorLocalizedDescription
の例のように、エラーの内容を伝えさえすれば良い場面が多いです。Swiftのエラーハンドリングは確かに、エラーを文字列としてthrows
するようにすることも可能ですが、それは必ずしも良いことばかりではないですよね。特に、DM(データモデル)でのエラー判定をコード上で行う場合、文字列だけでは不十分なこともあります。
JavaScriptのように文字列を例外として伝える場合、これは問題ないことも多いです。ただし、内部的なエラー判定を正確に行うためには、文字列以外の情報も必要となることが往々にしてあります。こうした柔軟な発想も必要ですが、エラーの取り扱いについてはもう少し考慮が必要ですね。
そういう意味では、Swiftのエラーハンドリングの柔軟性や設計思想は面白いと感じます。他の言語のアプローチとも比較しながら、より理解を深めていきましょう。
今日は時間にもなったので、ここで終わりにします。お疲れ様でした。ありがとうございました。