Page icon

第72回

2022/01/28
A Swift Tour
Empty
Empty
Empty
Empty
12 more properties
今回は 
A Swift Tour
 の 
Error Handling
 にあるエラーに対応する方法のうちの 
do-catch
 に注目して眺めていきます。初歩的なところをおさらいする感じでその文法的な特徴や、いくつかあるエラーの受け方などなど、エラー処理の基礎的なところを今日の時間で見渡してみますね。どうぞよろしくお願いします。
————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #72
00:00 開始 01:11 do-catch 03:33 エラー対応の特徴として何があるかを考えてみる 04:30 do ブロック 07:26 do ブロックでエラー処理を使う 08:59 トップレベルでの try 10:40 エラーの文字列化 11:35 エラーの catch が必要な場面 19:23 try を書かなくても動いた? 23:16 error の型 24:28 パターンマッチング 27:55 キャストしつつエラーを取得 29:59 複数の catch を記載可能 32:11 パターンマッチングは身につけておくのをおすすめ 34:06 catch の網羅性 36:01 catch の網羅性判定 38:25 エラー型は NSError 互換 40:46 NSError による網羅性判定 44:40 NSError への変換方法を規定する 46:52 クロージング —————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #72
はい、じゃあ今日はエラーハンドリングの3回目ですね。今まではエラー型の定義の仕方について学びました。標準ライブラリのエラープロトコルに準拠させて型を作ることでエラーを表現できて、それを
throw
で送出できるという話をしました。今回は、この送出されたエラーを捕獲し、リカバリーや通知といった処理を行うための
do-catch
について見ていきましょう。
ちなみに、他のエラーの扱い方、例えば
try?
try!
については前回お話ししたので、気になる方は前回のアーカイブを見ていただければと思います。今日は完全に
do-catch
の基本的なところに焦点を当てます。
スライドで見ていきましょう。基本的に Swift では
do-catch
は非常によく使う構文です。
do-try-catch
という名前のほうが理解しやすいかもしれませんが、公式的には
do-catch
として制御ブロックの一部として説明されています。
do-catch
はエラーハンドリングを行う際によく使われますが、基本的に
try
とセットで使用されます。
まず、
do
ブロックを作り、その中でエラーを送出する可能性のあるメソッドに
try
を付けて実行します。
do
ブロック内で
try
を使うことで、エラーが発生した時にそれを捕捉するための
catch
ブロックを作るという流れです。これにより捕捉されたエラーは暗黙的に
error
という変数名で利用できます。
では、具体的に見ていきましょう。
Playground
を使っていくつかの例を見ていきますね。
まずは基本的な
do-catch
の構文からです。
do { try someThrowingFunction() } catch { print("Error: \\(error)") }
このように、
do
ブロック内でエラーを送出する可能性のある関数に
try
を付けて実行し、エラーが発生した場合は
catch
ブロックで捕捉し処理を行います。
他の言語では
try-catch
構文が一般的ですが、Swift では
do
ブロックを活用します。この
do
ブロックはローカルスコープを作成するため、スコープ内でのみ利用可能な変数を定義でき、外部には影響を与えません。
例えば、次のように書けます。
do { let localVariable = "I'm local" try someThrowingFunction() } catch { print("Error: \\(error)") } // localVariable はここでは使えません
このようにしてローカルスコープを作ります。
また、Swift の
do
ブロックは、最初エラーハンドリングとは別の目的で導入されました。例えば、
do-while
ループが存在していた時代もありましたが、現在は
repeat-while
に変更されています。そのため
do
ブロックは、純粋にスコープを作成する目的もありますが、エラーハンドリングの
do-catch
としても使われます。
ここで例を挙げると、以下のようなエラー型とエラーハンドリングのコードがあります。
enum SomeError: Error { case somethingWentWrong } func someThrowingFunction() throws { throw SomeError.somethingWentWrong } do { try someThrowingFunction() } catch { print("Error: \\(error)") // エラーは SomeError.somethingWentWrong になります }
このように書くと、エラーが発生し
catch
ブロックでエラーメッセージが表示されます。
catch
ブロック内では
error
という変数が自動的に定義されるので、それを使ってエラーメッセージを表示できます。
さらに、Swift の
try
do
ブロックの中だけでなく、トップレベルでも使うことができますが、この場合にはエラーがランタイムエラーとして扱われます。例えば以下のように書けます。
try someThrowingFunction() // Playgroundsではランタイムエラーが出ます
このように、Playground では全体が
do
ブロックで囲まれているように動作します。
以上が基本的な
do-catch
の使い方です。次回はさらに応用的な使い方や他のエラーハンドリングの方法についても見ていきましょう。 プレイグラウンドのエグゼキューションがターミネーティブ、つまり強制終了する場合について説明します。意図的にこれを起こすにはどうすればいいか、あまり重要ではないかもしれませんが、とりあえず説明しますね。
プレイグラウンド内では、
do-catch
文を使ってフェイタルエラーを発生させることができます。以下のようなイメージです。
do { // ここでフェイタルエラーを発生させる fatalError("Intentional Error") } catch { // キャッチする print(error) }
このコードでは、9行目を消さないと正しく動作しませんが、エラーが発生すると13行目でランタイムエラーで強制終了します。あまり重要ではないかもしれないですが、これが基本の流れです。
エラーを文字列に変換する場合、デバッグ用の
localizedDescription
が使えます。しかし、このエラーを適切にキャストしないと使えません。
let error: Error = NSError(domain: "", code: -1, userInfo: nil) print(error.localizedDescription)
上記の方法がうまく行かない場合は、インターポレーション(文字列補間)を使ってエラーを文字列に変換できます。
print("\\(error)")
これで、エラーが13行目でランタイムエラーとして表示されます。
do
ブロック内で
try
を使う場合、必ず
catch
ブロックが必要です。
catch
がないと
try
の部分でエラーになります。プレイグラウンドのトップレベルではOKですが、関数内では
throws
が付いていないと
try
が使えません。これは、エラーが生成される場所を限定するためです。
func someFunction() throws { // エラーを投げる可能性のあるコード try someThrowingFunction() } do { try someFunction() } catch { print(error) }
関数に
throws
が付いていない場合、エラーをスローすることができません。
throw
を使うとエラーになります。以下はその例です:
func someFunction() { // スローを試みるが、throwsがないのでエラーになる // try someThrowingFunction() }
throws
を付けると、エラーをスローできるようになります。
func someFunction() throws { try someThrowingFunction() }
また、
do
ブロック内で
catch
を書かない場合、そのエラーは呼び出し元に伝えられます。プレイグラウンド内では問題なく動作するかもしれませんが、通常の構文では
do
ブロック内で
catch
を書かないとエラーになります。
以下のように書くと、エラーが呼び出し元に伝えられます。
func someFunction() throws { try someThrowingFunction() } do { try someFunction() } catch { print(error) }
この例では、
do
ブロックの中でエラーを生成し、それを捕獲しない場合、そのエラーは外部に伝えられます。これにより、呼び出し元でキャッチすることができます。エラーハンドリングの基本的な流れを理解するために、これを参考にしてください。 なので、
do-try-catch
って言われているけど、それらがね、
do
だけでも成り立つし、
try
だけでも状況によっては成り立つし、キャッチがなくても状況によっては成り立つし、みたいな感じで、結構柔軟に分割できるところが面白いですね。まあ、だからといって「何?」っていうこともないかもしれないですけど、大事なのはエラーを発生できる環境の中でしか
try
が使えないことです。そして、
catch
を使ってエラーを捕捉する。捕捉されなかったエラーについては、さらに上流に伝えられるので、最終的には状況に応じてエラーハンドリングが可能になります。
一般的な言語の例外処理と似ていて、どこからエラーが来るか分からないみたいな捉え方もできますが、言語仕様で
throws
があるかどうかによってメソッドがエラーを返せるかどうかが決まるので、メソッドに
throws
がついていることによって「このメソッドはエラーを発生させる責任がある」と意識することが重要ですね。そのため、エラーがどこから出てくるか分からないという状況よりも、必ず呼び出した先から返ってくるという感覚に変わってきます。それが興味深いところです。
また、エラー発生源の責任が明瞭になるという点で、
throws
は非常にメリットが大きいと思います。さらに、
catch
文もいろんな書き方ができるという点も面白いですね。
最も単純な例として、何も指定しない
catch
文がありますが、これによって
error
という変数が得られます。このとき、
error
はSwiftのエラープロトコルに準拠した独自の型が指定されているので、例えば自分で作ったエラー型(例えば
MyError
)が発生した場合、
catch
で捕捉されたものはその
MyError
型のインスタンスになります。
type(of:)
で確認すると、エラーの型がわかることがあります。動かしてみないと確信は持てないのですが、たまにプレイグラウンドが
try
を省略しても動作することがあります。これは非常に珍しいケースで、その仕組みがどうなっているのか興味深いです。何を飛ばすとそうなるのか、確認してみると驚きです。普通に考えられない最適化が行われているのだと思います。
個人的に感動したこととして思い出しましたが、シンタックスハイライトについても触れてみたいです。自分でシンタックスハイライトを実装するとなると、しっかり解析してこれはキーワードだ、これはメソッド名だと把握してから色を付けるという発想をしがちです。でも、最近のシンタックスハイライトを見ると、とりあえず色を付けて、その後必要に応じて修正するといった動きを見せます。特にXcodeでは、最初は一般的な色が付いていますが、次第に正しい色に直っていきます。例えば、プロジェクト内の型名や他のモジュール内の型名で色が変わることもあります。
このように、シンタックスハイライトが動的に変わっていく様子は興味深いですね。 とりあえず当たりをつけてパッとやっておいて、裏で整合性を整えていく、みたいなやり方が上手だなぁと思います。そういったことがコンパイルレベルでも起こっているのでしょうかね。すごいですよね。もしかすると2回動かしているとか、全然最適化と関係ないことが起こっていたりすると面倒ですね。例えば、コンソール用の出力とプレイグラウンド用の出力が2回動いていたら困りますが、まぁいいでしょう。
とりあえず、自動的に捕獲したエラー変数には、ちゃんと
MyError
のインスタンスが入っていることが
type(of:)
で確認できました。ただ、このエラー変数自体は少し見にくいかもしれません。ここに書いてある通り、Swiftの標準のエラープロトコル型、つまり存在型が型として採用されています。
要は、エラー変数が自動的にSwift標準のエラー型として規定され、その中に抽出されたエラーが入っているということです。これが13行目の
catch
文のところです。また、変数名は自由に変えられるとスライドに書いてありました。この後出てくるのかな?その変え方はどうやるのかなと思いました。
didSet
みたいな時に変数名を変えるには丸括弧で書けば済むじゃないですか。ただ、ここはそういう書き方ではないと思いますが、多分、
catch (let err)
のようにすれば、コンパイルを通ると思います。これで、
err
という変数名でエラーを捕獲することができます。
この
catch let error
というのは、バリューバインディングパターン(value binding pattern)を使って変数名を割り当てていると思われます。エラー変数を暗黙的に定義するのではなく、明示的に名前をつけたければ、このような書き方をします。ただ、エラーという名前で十分な気もします。他に名前をつける機会はそんなに多くないと思います。だいたい
error
という名前で通じるか、適切だからです。ここで
ActionError
なんてつけても意味がないし、
try
が複数あったりすると、全然別のエラーが出てきたりすることもあるので。
コメントをいただいたのですが、型を独自にキャストしたい時には、このパターンマッチングを使うことがありますね。その時に具体的なエラーを定義するかもしれません。例えば、
OperationError
のように名前を変えるときもあります。このようにして、
catch let operationError as OperationError
という書き方になります。確かに、この書き方は適切ですね。
ちなみに、パターンマッチングではいろいろなことが書けますが、
is
を使ったタイプキャスティングパターンを使った時には変数が自動定義されなくなります。この使い方をした時には、エラーの内容を詳しく取ることはできません。エラーの内容を取りたい時には、先ほど書いたようなバリューバインディングパターンとタイプキャスティングパターンを使って、エラー型を具体的に指定します。この時には、エラー変数がちゃんとタイプキャスティングによって
OperationError
型として使えるようになりますので、例えばこのエラー型が何かしらの関数を持っていたときに、そのメソッドを実行することもできます。
こういった感じでエラー処理を行うことができます。 これは普通のキャストのお話です。普通のキャストがなかったとき、
as?
がなかったときには、普通のエラー型として扱われます。そして曖昧さを解消するためにプリント文などを使います。要は、
XXX
がないよというエラーが発生するはずです。ここまでコードを絞っていくと、ちゃんとしたエラーが出るのではないでしょうか。こういう風にエラーの型をキャストしていけます。
キャッチ文は複数定義することができます。例えば、エラーがもう一つ別の何かであったときに対応できます。その場合、エラーを例えばオペレーションエラー (
OperationError
) を返すようにしているけれども、実際にはどんなエラーが返ってくるかわかりません。その中でエラーがオペレーションエラーだった場合には特定の処理を行い、それ以外だった場合には別の処理を行うというように、複数のキャッチを連ねて書くことができます。
しかし、ケースを間違えている場合もあります。その場合、適切なキャッチ文を書き直します。今回の場合、オペレーションエラーに合致して19行目が実行されます。違うエラーだった場合には、その次の処理が動くという風に、
switch
文のケースをたくさん書いているのと同じようなイメージになります。キャッチ文の中ではパターンマッチングが使えるところも
switch
文ととても似ています。
また、アイデンティファイヤパターンやサイトキャスティングパターンなどのパターンマッチングについては、列挙型の回で話したと思います。どんなパターンマッチングがあるかについては、その回のアーカイブを見てもらえればと思います。パターンマッチングをしっかりと抑えておくと、表現力が高まり、いろんな場面で使えます。例えばエラーハンドリングや、
if
文、
while
文などの条件分岐で自由に使えるので、パターンマッチングはスキル向上に役立ちます。
さて、話を戻しましょう。例としてオペレーションエラーが返ってきているため、20行目が実行されています。もしオペレーションエラーでないエラーが出てくると、23行目の方に処理が移ります。要するに、19行目にマッチしない場合の動きです。
このようにキャッチ文を2つ付けました。他にも様々なキャッチ文の書き方があります。例えば
MyError
を付けたり、他にも色々と状況に応じて書いていけます。ただし、
switch
文とは表現が少し異なります。
switch
文では
default
が必須ですが、エラーハンドリングの場合は、このキャッチが
default
的な役割をします。しかし、キャッチ文がなくてもコンパイルが通る場面もあります。すなわち、それ以外のエラーは呼び出し元に伝達されるということです。
もしこのメソッドが
throws
を伴っている場合、捕獲されていないエラーが存在してもコンパイルは通り、発生したエラーが呼び出し元に伝達されます。
throws
がなければ、捕獲していないエラーがあった場合にコンパイルエラーになります。これは
switch
文のエラーと同じような感じです。 なので、必ずマッチするパターンをどこかに入れておく必要があります。そうすることによってコンパイルが通るようになります。
例えば、今アクションが
MyError
を返している場合、
MyError
のすべてのケースを網羅しておく必要があります。現在は1つのパターンしかありませんが、それでもすべて網羅できそうに見えます。しかし、Swiftの場合、この状況ではすべて網羅されていないとみなされ、デフォルトのキャッチが必要になるという動作をします。
これは、アクションメソッドがどんなエラーを返すかをSwift言語では明記できなくなっているためです。要は、どんなエラーが起こるかを限定できないということです。
MyError
だけに限定することができません。今後、万が一エラーが増えたり、モジュールが別に定義されていた場合、またはモジュールが仕様変更されて全く別のエラーが発生するようになった場合でも、すべてのエラーに対して対応できるようにしないといけないという言語仕様になっています。
他の言語では、どのエラーを返すかを明記できる言語もありますが、Swiftではこの方法を取っていません。これは言語を設計している人たちのポリシーによるものでしょう。
とりあえず、使う側としてはエラーを明記できないので、あらゆるエラーの可能性を想定したコードを書く必要があります。これがSwiftのエラーハンドリングの特徴です。
他にはどんな特徴があるかというと、Objective-Cのころのエラー表現として
NSError
という型がありましたが、Swiftのエラー型と互換性が保たれています。具体的には、例えばエラー型があって、それをインスタンス化するとします。
MyError
型としてエラーを作成できますが、このエラー型を
NSError
型にそのまま入れることができます。
以下のようにします:
let myError = MyError() let nsError: NSError = myError as NSError
これでコンパイルが通るようになります。
print(myError)
print(nsError)
の両方を表示してみると、何が出るか確認できます。
myError
MyError
型で、
nsError
NSError
型です。この型キャストは
as
を使用しているので絶対に失敗しないという仕様があります。これによってObjective-CとSwiftのエラーのブリッジができているというわけです。
このように、Swiftには特殊なエラーハンドリングの特徴があります。 さっきディフォルト的なエラーの話をしましたけど、要は何かエラーがあった時に
try do catch
でキャッチする必要があります。
catch
が複数続く場合でも、ディフォルト的なキャッチを書かなければいけないということです。どんなエラーにもマッチするキャッチがあれば良いという状況になるので、例えば理解しやすいように、関数内にこのブロックを入れてみましょう。
do { try someFunction() } catch { // デフォルトキャッチ }
これを入れておけば、特にコンパイルエラーは起こらないはずです。もしキャッチがなければ、コンパイルエラーになるでしょう。スロー文に関しても同様です。
try
の際にエラーが出るのと同じ理由です。
ディフォルトの他にも、ワイルドカードパターンを使うことでコンパイルエラーもなくなるでしょう。
do { try someFunction() } catch { // 何もしないキャッチ }
また、
as NSError
のようにキャストすることでエラーを捕捉することもできます。
do { try someFunction() } catch let error as NSError { // NSError型としてキャスト }
Swift のエラー型は
NSError
と完全に互換性があるので、特に問題はありません。どんなエラーもマッチするので、すべて網羅できたということです。このようにキャッチをつけなくてもコンパイルエラーにはなりません。
さて、
NSError
についてもう少し詳しく見てみましょう。例えば、エラーが
NSError
型なので、
localizedDescription
プロパティを使うと、規定のエラーメッセージが出てきます。
do { try someFunction() } catch let error as NSError { print(error.localizedDescription) }
このようなコードを書くことで、エラーメッセージを表示することができます。
NSError
型としてもちゃんと扱えるのですね。また
NSError
に変換する際、特に指定しなくてもデフォルトの実装によって適切に変換されますが、カスタムの
NSError
に変換したい場合は
CustomNSError
プロトコルを使います。
struct MyError: CustomNSError { static var errorDomain: String { return "com.example.MyError" } var errorCode: Int var errorUserInfo: [String : Any] { return [ NSLocalizedDescriptionKey: "Description of MyError" ] } }
これに準拠させると、
NSError
をしっかりとカスタマイズできます。
CustomNSError
プロトコルには、
errorDomain
errorCode
errorUserInfo
という三つの主要なプロパティがあり、それらを使ってエラーメッセージや情報を制御できます。
特に Cocoa フレームワークを使う場合、カスタムエラーを適切にフレームワークに伝えることができます。これによって余計なコードを書かずに済むので、便利です。ぜひ、
CustomNSError
も使ってみてください。
では、今日は時間になりましたので、これで勉強会を終わりにしたいと思います。お疲れ様でした。ありがとうございました。