https://youtu.be/j6JTL1fP8p8
今回も引き続き A Swift Tour
の Error Handling
について見ていきます。前回に do-catch
に窺える特徴みたいなところを眺めましたけれど、今回はそれも含めて The Swift Programming Language に記載されていた内容に沿ってエラー対応のしかたを最終確認していくみたいな回になりそうです。どうぞよろしくお願いしますね。
————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #73
00:00 開始 02:02 do-catch の基本 03:12 エラーを送出するかもしれない機能を定義するとき 04:14 エラーを送出するかもしれない機能を呼び出すとき 05:06 練習問題 05:34 エラー型の定義 07:28 エラーを送出する可能性のある関数定義 07:51 余談:プリンターでの情報の受け方 10:55 正常系と異常系 12:16 エラーを捕獲する場面 13:59 catch キーワードだけで捕捉するとき 14:22 エラーが捕獲されるときの動作 15:52 余談 16:35 catch ブロックを複数用意する 17:40 波括弧を置く位置について 18:29 コードスニペット 19:56 余談 21:15 エラーをキャストしつつ変数で受ける 22:50 複数の catch がある場合の動作 24:51 switch 文との類似点と fallthrough 26:16 文字列リテラルの中で二重引用符を使いたいとき 26:42 エラーを捕獲する順番 29:08 エラー対応の網羅性 30:03 型キャストパターンと値束縛パターン 31:18 個別に関連値を持つ場合のエラー対応 31:53 列挙子パターン 33:55 全てのエラーを網羅するには 35:28 非検査例外 38:12 エラーの種類が増える可能性 40:26 想定しないエラーへの対処 41:22 Playground で catch を網羅しなくても良い理由 41:52 フェイルセーフ 43:21 全てのエラーを捕獲できれば良い 45:07 クロージング —————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #73
では始めますね。今日はエラーハンドリングに関する話をします。具体的には、「The Swift Programming Language」の「A Swift Tour」にあるエラーハンドリングの部分になります。もう少し大きな括りで言うと、「Welcome to Swift」に含まれますので、Swiftの初歩としてエラーハンドリングを見ていこうというセクションになっています。
ただ初歩とは言っても、この勉強会の性格上、その初歩を広い視野で、広く深く扱う感じで見ていく会になっているので、細かな点についてさまざまな脱線もしながら話を進めていきます。今日は前回にもお話しした通り、エラー対応の特に do-catch
について、スライドに従って説明を進めていこうと思います。
おさらいになる部分もありますが、本に沿って進めると体系立てて整理された形で話を進めることができるかと思います。前回のような雑多な感じよりも、今回の方が分かりやすいかもしれません。そんな感じで一緒に本を眺めながら、勉強を進めていきましょう。
ではまず、do-catch
によるエラー対応についてです。このスライドを元に前回話しましたが、本になぞって進めるともっと明瞭に見えてくると思います。早速進めてみましょう。
おさらいになりますが、do
ブロック内でエラーを発生させる可能性のあるコードに try
を付けます。それに対して catch
ブロックを用意し、エラーが発生した場合にそれを補足します。補足したエラーは、エラー変数で取得できます。この変数名も変更することができます。このスライドを元に前回の参加者の方は頭に思い描きながら、これから話す内容を理解しやすくなるかと思います。
スライドに従って注目すべきところとして、「エラーを発生させる可能性のあるコードに try
を付ける」とありますが、この「エラーを発生させる可能性のあるコード」とは、あらかじめAPIを用意した人が「エラーを発生させるかもしれない」という形で throws
キーワードを使ってマークしてくれたAPIのことを指します。自分でAPIを作るときも同様です。したがって、エラーを発生させる可能性のあるコードについては try
を付ける形になります。
一方で、そもそもエラーを発生させる可能性がないコードには try
は付ける必要はありません。また、エラーを発生させる可能性があるコードAPIには必ず try
などを付けなければなりません。そのため、自分でエラーの可能性を持たせるAPIを作成する際も、必ず try
などを付ける必要があります。トライを付けなくても良い場合については前回お話ししましたので、そのあたりが気になる方はアーカイブを見ていただければ分かるかと思います。今日はその部分は踏み込まず、先へ行きます。
具体的にプレイグラウンドでコードを書いたほうが分かりやすいですが、次にちょうど練習問題があるので、それを踏まえて実際のコードを書いていく形にしましょう。この練習問題は前々回に例に上がったプリンターに関するエラーで、実際にエラーを発生させるコードを扱う練習問題になっています。まず最初にプリンターエラー型を用意するところからおさらいも含めて書いてみましょう。
ではまず、エラー型をどういったプリンターエラーにするかというと、あらかじめ自分で列挙型を作成します。例えば enum PrinterError: Error
として、Swift標準のエラープロトコルに準拠させます。次に、紙がないというエラーをcase outOfPaper
、トナーがないというエラーを case noToner
という形で用意します。
enum PrinterError: Error {
case outOfPaper
case noToner
}
このように、プリンターのエラー型を定義しました。具体的にどのようにエラーを発生させ、対応するかについて次に見ていきましょう。 それでは、前回の復習も兼ねて、エラー処理についてもう一度確認していきます。今回は、エラーを発生させるかもしれないAPIを用意し、それを使ってエラーを捕捉する過程を説明します。
まず、エラーを発生させる可能性のある関数を定義するために、send
関数を用意します。この関数は、例えばプロセスのID(イント型)とプリンターの名前(ストリング型)を引数として受け取り、それを処理するようなものです。以下は、その一例です。
func send(jobID: Int, to printerName: String) throws -> String {
if printerName == "Neverhazard" {
throw PrinterError.printerNotFound
}
return "Job sent"
}
ここで、printerName
が Neverhazard
だった場合に、printerNotFound
というエラーを投げるようになっています。この PrinterError
は、エラープロトコルに準拠した型として定義されています。
次に、この send
関数を使ってエラー処理を行う例を示します。この場合、do
ブロックの中で try
キーワードを用いて関数を呼び出し、エラーが発生した場合は catch
ブロックで捕捉します。
do {
let response = try send(jobID: 101, to: "HP_Printer")
print(response)
} catch {
print(error)
}
このコードでは、send
関数を試み、成功した場合はレスポンスを表示し、エラーが発生した場合はそのエラーの内容を表示します。また、エラーの具体的な捕捉も可能で、その場合は catch
ブロックを次のように書き換えることができます。
catch PrinterError.printerNotFound {
print("Printer not found")
} catch {
print("An unexpected error occurred: \\(error)")
}
上記の例で send
関数に渡すプリンター名を Neverhazard
に設定することで、実際にエラーを発生させることができます。
do {
let response = try send(jobID: 102, to: "Neverhazard")
print(response)
} catch PrinterError.printerNotFound {
print("Printer not found")
}
このコードを実行すると、PrinterError.printerNotFound
が発生し、「Printer not found」と表示されます。これでエラーの発生とその捕捉の流れが一通り確認できますね。 「なので、こう書き換えるだけですね。そうすると、あらかじめ組み込まれていた8行目の条件に合致して、プリンターエラーが送出されます。この時の動きを実際に見てみましょう。要は、18行目がエラーを捕獲して、エラーメッセージを出してくれるので、そこを実際に確かめてみましょう。それだけです。
今回の例を動かすと、19行目の処理が走って、そのまんまエラー型がテキストとして表示されます。整理すると、今回は正常系に対して戻ってこなかったので、正常系である16行目は実行されていません。逆に異常系で戻ってきたので、異常系をキャッチして19行目の処理が動いたという感じです。シンプルな例題ですね。
ここまでで質問はありますか?普段、あまり質問を受けていませんでしたけど、何かあれば言ってもらって大丈夫です。この勉強会の醍醐味として、話が脱線しても全然オッケーです。面白い部分があれば、いつでも割り込んでもらっていいので。
とりあえず、こんな感じで練習問題はオッケーです。この練習問題を解くだけでも、一通りエラーの送出とキャッチの雰囲気は掴めるので、とても有意義です。慣れるのにもすごくいい気がします。
では続いて、前回も話した複数のキャッチ文でエラーを捕獲する方法について説明します。今回の例題をもう少し細かくエラーハンドリングしてみましょう。なかなか良い練習問題の流れですね。練習問題という括りにはなってないですけどね。
やることとしては、実際にコードを書いていった方が分かりやすいと思います。大枠としては、今までは全てのエラーを一つのキャッチで捕まえていましたが、ここに新たに条件を加えて、特定の条件に基づいてどう対処するかを考えます。
具体的には、先ほどの例題のコードに新しい条件を加えていきます。まず、onFireError
というエラーが発生したときについてです。別の勉強会でも指摘されたことがありますが、このキャッチを書く位置について、標準的な書き方に従いましょう。Appleの公式ガイドラインではなく、コードの補完やスニペットなどを参考に、キャッチを一行上に書く形にします。
具体的にはこうです:
do {
// 処理
} catch PrinterError.onFire {
// エラー処理
} catch {
// それ以外のエラー処理
}
このように、キャッチ文を追加していきます。例えば PrinterError.onFire
のときには、通常のエラー処理とは異なる処理を行うようにします。Xcodeのプレイグラウンドではスニペットが表示されないこともありますが、標準的なXcodeでは表示される場合があります。
それでは、onFire
の時には、スライドにあった通りのテキストを出力するようにします。」
以上のように書き換えました。ここまで理解していただけたでしょうか?質問があれば、何でも気軽に聞いてくださいね。 とりあえず、これらのエラーメッセージを出力しています。あ、エラーメッセージじゃないですね。コンソールに出力しているだけです。とりあえずエラーを出力するという形にして、さらに別のエラー、例えばプリンターエラーが想定される場合のコードを書きます。
catch let printerError as PrinterError
という書き方をします。要は、投げられたエラーがプリンターエラー型の場合には、変数printerError
にエラーを受け取って、それを表示するようにするコードです。この場合、「こんなエラーが起こった」と表示します。
前回お話しした内容で、「変数名を変えても意味はないけれど、こんな感じに使える」という紹介がありました。それが今のコードの例で出てきました。エラーをただerror
と受けるのではなく、printerError
のように具体的な名前で受けることで、コードが読みやすくなるという話でした。
この時、実際どう動くかというと、neverHappensToMe
のエラーが投げられた場合、プリンターエラーがキャッチされて20行目のコードが動くはずです。実際に20行目が動きました。onFire
ではないため、18行目をスキップして20行目が実行されます。
次に、onFire
の場合を想定してみます。例えば、プリンター名がburning
という場合、PrinterError.onFire
エラーを投げると、この名前がburning
の時にはエラーが21行目ではなく、23行目が実行されるというようなコードになります。22行目ではプリンターエラー全体を捕捉しつつ、特定のエラー(onFire
)に限定した処理を実行します。
具体例として、他のエラーではユーザーに通知するだけですが、onFire
の場合には、プリンターがある部屋のスプリンクラーを作動させるなど、特別な処理が必要になることがあります。このように特定エラーをキャッチして対応する場合に役立ちます。
ふと思いましたが、例えばcatch
ブロックでスプリンクラーを作動させつつ、ユーザーに通知する場合、23行目でユーザーに通知します。スイッチ文を使う場合、特別な処理の後に一般処理をするためにフォールスルーを使用できますが、catch
文ではフォールスルーが使えないことを試してみました。やはりフォールスルーは使えません。エラー処理ではスイッチ文でフォールスルーを使うしかないようです。
今回のような場合で、もしユーザーにも通知する必要があるなら、単にプリントするだけではなく、スプリンクラーを作動させたり、もう少し具体的な処理が必要になります。 とりあえず同じことを通知したいんだったら、この プリンターエラー
ってここでも同じように書いていかなければいけないという冗長さがあります。ただ、これは適切な方法だと思いますね。例えば、プリンターエラー
ここで オンファイヤー
を、ダブルコートで複数書いてしまった場合です。こういうときには、両サイドにシャープを入れて、中でダブルコートも使えるようにします。
こんな風に書いていくことになります。次に、コメントにある良い感じのテストをやってみましょう。
この23行目を最初に書いてしまうとどうなるかを確認します。これを先に持ってくると、オンファイヤー
かどうかのチェックよりも前に20行目で捕獲して、プリンターエラー
というメッセージになってしまいます。この21行目が表示されて終わってしまうという動きをします。ここはかなり重要なポイントですね。注意しないといけないところです。
スイッチ文も同じ動きです。エラーハンドリングも同じ動きです。とにかく全体を見て適切なものを選ぶのではなくて、上から順にマッチしていくものを探していく仕様になっています。そのため、このスイッチ文でもキャッチ文でも順番が非常に重要です。上から順番に必要なものを書いていくことで、整然と並んでいる必要があります。整然と並んでいる代わりに、プログラマーが意図した順番になっていきます。
フォールスルーの話に戻りますが、スイッチ文のフォールスルーは何かと危険なので使わない方がいいという話になりがちです。しかし、順番を意図して書いているのであれば、この時だけ次のケース文も実行するということも可能です。順番を考慮した上で成り立つ話ですので、フォールスルーもあまり特殊なことをしているわけではないという感覚になります。
そのため、フォールスルーを見直してみると新しいコードが書けるようになるかもしれません。スイッチ文についての話です。コメントでもいただきましたが、ここで警告が出るのは非常にありがたいですね。要は、プリンターエラー
をすべて捕獲している20行目があるため、特定の プリンターエラー
を捕獲しようとしても、その部分は絶対に実行されないということがわかります。この警告を見逃さなければ、そんなに危険なコードにはならないということです。
今回の例では、コードを元に戻しましたが、順番をちゃんと意識したコードを書いていかないといけません。ここは重要なポイントです。
20行目と23行目の特徴の違いとして、23行目はアイデンティファイアーパターンとバリューバインディングパターンを使って、どんなエラーがあったのかをタイプキャスティングパターンでキャストしつつ受け取っています。パターンマッチングに忠実に話すと非常に長くなりますが、とにかくエラーを プリンターエラー
変数に受け取っているということです。この違いは、エラー処理の中でそのエラーを実際にランタイムの中で使うか、使わないかにあります。
オンファイヤー
であった場合には、エラー変数をわざわざ取らなくても オンファイヤー
であることが決まっています。この時にもしエラー型が値を持っていたと仮定すると、例えば 温度
の値を持たせることができます。今回は、仮に double
型で持たせるとします。 とりあえずエラーとして温度が添えられているとして、そういった時に値が実際に何だったかを拾いたい場合について説明します。
例えば、ここで温度 (temperature
) として添えられている値があったとします。この値が必要だった時には、次のようにキャッチして受け取らなければなりません。
if case let .onFire(temp) = error {
// 温度(temp)を使用する例
print("Temperature: \\(temp)")
}
ただし、その温度が必要でない場合は、以下のようにすることもできます。
if case .onFire = error {
// 値を無視してエラーの種類だけを判定
}
このような方法は、パターンマッチングを利用してエラーの種類だけを確認するのに非常に有効です。実際に値が欲しい場合はアイデンティファイヤーパターンを使用し、値を取り出します。
例えば、次のように温度を受け取る場合です。
if case let .onFire(temp) = error {
print("Temperature: \\(temp)")
}
この他に、関連値だけを取りたい場合にも、このようなパターンマッチングを利用します。例えば、劣化しパターンやバリューバインディングパターンなどがあります。
さて、25行目のキャッチがない場合にはどうなるか、という疑問ですね。実際に試してみると、エラーが網羅されていない場合にはコンパイルエラーになります。エラーを返さない関数の中で定義するとわかりやすいのですが、「網羅されていないエラーがある」というエラーが発生します。
例えば、次のようなコードがあるとします(Playgroundではエラーになりにくいですが、実際のプロジェクトではエラーになる可能性があります)。
do {
try send()
} catch .onFire(let temp) {
print("Temperature: \\(temp)")
}
// ここに catch { エラーを網羅するための記述 } がないとエラーになる
この例では、send
関数がエラーを返す可能性があるにもかかわらず、網羅的にキャッチされていないため、コンパイルエラーとなります。これを回避するためには、全てのエラーをキャッチするキャッチ文を追加する必要があります。
do {
try send()
} catch .onFire(let temp) {
print("Temperature: \\(temp)")
} catch {
// その他のエラーをキャッチする
print("Other error: \\(error)")
}
このようにして、すべてのエラーを網羅的にキャッチすることができます。
Swiftには、Javaで言うところの検査例外と非検査例外の概念に似たものがあります。検査例外では、どのエラーが返るかをあらかじめ定義しておく必要があります。それをキャッチできない場合にはコンパイルエラーになる仕組みです。Swiftのエラーハンドリングは、非検査例外に近い動作をしますが、ちょっとした違いがあるため注意が必要です。
まとめると、エラーハンドリングをしっかりと行うことが重要であり、それにはエラーの種類を網羅的にキャッチする仕組みが必要です。Swiftではこのためにもパターンマッチングを利用することが多いです。 とりあえず、そういったノリで Swiftのエラーハンドリングについて説明します。エラーが具体的に想定されていないことは、抑えておきたいポイントです。
例えば、「send」関数がどこかのプリンターモジュールに規定されていたとします。将来、ネットワークプリンターができて、この「send」関数が対応するとします。そうなると、エラーとして「オフライン」が増える可能性があります。このようにエラーの発生するケースが変わることがあります。
プリンターエラーで捕獲できる場合もありますが、全く別のエラーが「send」関数の中で発生する可能性も考えられるでしょう。具体例として、プリンターエラーではなく、ネットワークエラーとして「オフライン」を定義する場合です。その「send」関数が新たに返すネットワークエラーに対応する必要があります。
エラーがないと対応できなくなる場合には、コンパイラが気を利かせて想定しないエラーがあった場合には、それを致命的エラーとして処理するコードを書いてくれることがあれば良いですが、Swiftは全ての場合を想定してプログラムを組むように促しています。
プレイグラウンドでなぜこのキャッチが不要かというと、致命的エラーを自動で保管してくれるからです。他にもメイン関数などでも同様に自動で保管してくれる場合があります。しかし、基本的には自分で書く必要があります。
全てのエラーをキャッチする場合に致命的エラーが適切かどうかはプログラムによります。想定できないことが発生したときには、最終的には処理を停止させたり、進めないようにするなどの対策が必要です。例えば、グラフィカルユーザーインターフェイスを持つアプリでは、ボタンを押しても次の画面に遷移させない対応を取ることがあります。
致命的なエラー処理を行う際には、データベースのロールバックなどの適切な配慮を行ってからエラーを処理します。一番確実な安全な手配を行ってからエラーハンドリングを行うことが重要です。
ここで、全てのエラーをキャッチできればいいので、ワイルドカードパターンを使ってもコンパイルは通ります。ただし、ワイルドカードパターンを使った場合にはエラーメッセージを変数に取れません。さらに、Swiftのエラー型はFoundationのNSエラー型と完全に互換性があるため、キャストしても問題ありません。例えば、as NSエラー
といったキャストも問題なく通ります。
タイプキャスティングパターンを使った場合でもコンパイルが通ることを確認しましょう。全てのエラーを網羅できるようにすることが大切です。
時間が来ましたので、今日の勉強会はこれで終わりにします。お疲れ様でした、ありがとうございました。