Page icon

第74回

2022/02/02
A Swift Tour
Empty
Empty
Empty
Empty
12 more properties
本日も 
A Swift Tour
 から、まずは前回からの続きの 
Error Handling
 の最後のパートを眺めます。
そしてその後に時間があったら、この章の最後の章にあたる 
Generics
へと進んでいきます。こちらの話題はとても広大で難しいところですけれど、まずは Swift ツアーとして Apple がどんなあたりに着目しているのか、そんな感じにざっくりと捉えていけたらいいかなって思っています。
どうぞよろしくお願いしますね。
—————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #74
00:00 開始 00:41 複数の catch によるエラー対応 01:29 練習問題 02:13 エラー送出の見通し 03:47 エラーの捕獲 04:49 関数からエラーを送出する方法 07:08 try? 07:34 エラーの種類を気にする必要がないとき 08:35 try? を使ってみる 12:22 エラー情報が欠落する 13:13 do-catch も捕捉もれに注意 14:11 網羅性における switch 文との性格の違い 16:30 try! 17:16 エラー対応における defer の扱い 17:43 defer の特徴 18:00 エラー処理と defer を組み合わせたときの効能 18:52 エラー対応における defer の例 24:07 do の中で defer を書くと? 28:33 みんなの予想 31:25 実際の挙動 32:03 catch 内で値がどうなっているかに影響する 32:07 do ブロックの中に含めるかどうかの見極めが大切 34:30 defer の実行タイミング 35:56 defer の動きを再現する 36:30 defer は finally のように使える 37:23 defer の finally とは異なるところ 38:38 return の直後に動く defer ブロック 40:49 質疑応答 41:20 defer ブロックを途中で抜けるには? 42:24 guard の中で制御構文を使える場面 42:45 defer の中で guard は使える? 45:30 guard の中で fatalError を使う 46:41 変数の生存期間と withExtendedLifetime 関数 48:46 いったんクロージング 49:21 インクリメント演算子を guard を使って実装する例 51:56 質疑応答 52:53 インクリメント演算子の実装における guard の挙動確認 53:36 インクリメント演算子を defer を使わずに実装した場合 55:02 クロージング ——————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #74
はい、では始めていきますね。今日もエラーハンドリングのお話ですが、その中の最後の部分についてです。といっても、ほんの少しだけの内容です。若干復習的に今まで話したことがいくつか並び、その後にまだ話していない簡単なことが一つ出てくるという感じです。
まずは、前回のスライドです。今画面に映っているやつですね。「複数のキャッチを利用可能」というところで、いろいろとこのコードを使ってPlaygroundで遊んでみたかと思います。このお話に関することが少し出てくるので、前回使ったこのコード、今表示中のやつを使って話を進めていこうと思います。
今日のお話はこれ――練習問題です。練習問題といっても、前回やったとおりですが、「エラーを創出するコードをdoブロック内に追加してみましょう」。エラーを創出するコードをdoブロック内に積み込んで、どのようなエラーを創出すれば最初のcatchブロックでエラーを捕獲できますか?というお話です。
前回やった内容ですので、要所を押さえて見ると、doブロック内にエラーを創出するコードを書けばよいという単純なものです。いきなり
throw
してもいいし、何かのエラーを返す関数を作って実行してあげてもOKです。とにかく一番最初のブロックでエラーを捕獲したいわけですから、プリンターエラーの
onFire
をdoブロック内で創出してあげましょう。
前回のコードはこうですね。プリンターエラーがあって3通りのエラーがあり、ネットワークエラーは今回は使わないので消しても大丈夫です。
send
メソッドの中で
noToner
だったときには
noToner
エラーが発生するようになっています。それ以外は正常終了となっているところを、次のようにdoブロック内でエラーを創出する方向に変えていきましょう。
今、実行するとどこがキャッチするのかを見ると、正常に終わって18行目で終了しています。しかし、この中で最初のエラー、つまり19行目にマッチしたいので
onFire
を創出するコードを用意します。一番端的にはここで
throw
して
PrinterError.onFire
を投げてあげると、18行目には行かずにエラーが発生し、20行目に行きます。これで1つの課題はクリアです。
別の方法としては、例えば
send
方法の中でエラーを返してあげるシンプルな方法も考えられます。これでも22行目が実行されます。どちらもやっていることは大して変わりませんが、もう一つ具体例を挙げるとすると、プリンターの名前が
burning
だった場合にエラーをスローしてあげる方法もあります。
if printerName == "burning"
というようにして、この時に
throw
するというコードです。これでプリンター名がちょうど
burning
になっているため、24行目が実行されます。
実際にはもっと適切な条件を考えてエラーをスローすることになります。例えば温度計が高温を検知したときにエラーをスローするというように、実際の状況に即したエラーハンドリングになりますが、今回はこれで十分です。
以上が今回の練習問題の内容です。他に23行目でキャッチする方法は特にないですね。あまり有意義な練習問題ではなかったかもしれませんが、次に進みましょう。
続いて、エラーハンドリングの話に入った最初に少しお話しした
try?
を使って結果をオプショナルで得る方法について、復習します。これもざっくりとやります。大事なところは何があるかというと、次の行です。 関数がエラーを創出すると、エラーは破棄されて、ここが重要です。結果として、
nil
が得られます。ここで重要なのは、エラーの内容を気にする必要がない場合、つまり、エラーがあったかないかだけを確認したい場合には、
try?
が適切だということです。
エラーが起こらなかったときには、その戻り値がオプショナルとして得られるという作りになっています。これを活用して、
try?
を使っていきます。実際にやってみましょう。
例えば、プリンターのエラー処理を考えます。仮にプリンターエラーが「オンファイヤー」しかなかったと仮定します。この仮定はあまり現実的ではありませんが、先に進めます。
let printerResponse = try? sendJob()
21行目から下は
do-catch
ブロックで長々と書いていますが、
try?
を使うと、このように簡潔にエラーハンドリングができます。
printerResponse
nil
であればエラーがあったということになります。逆に、エラーがない場合にはちゃんと結果が
printerResponse
に格納されます。
このとき、
sendJob
関数の戻り値は
String
型ですが、この例では
String?
、つまりオプショナルとして得られます。これは、エラーが起こらない場合にはその値が得られ、エラーが起こると
nil
が得られるためです。本来、
String
型ですが、このようにオプショナルになることもあります。
これは、エラーが起こった場合にそのエラーの詳細は分かりませんが、エラーハンドリングが簡潔になるという利点があるからです。他の言語ではこういった柔軟なエラーハンドリングができない場合もあります。
このオプショナル型の話を、先日勉強会で話した共変と反変の話に関連付けることもできます。オプショナル型は、もともとの型のサブタイプと見なされるためです。つまり、
String
型の戻り値であっても、
String?
(オプショナル
String
)として扱えるということです。
エラーが起こったときに
nil
しか得られないため、例えば、本来エラーとして「オンファイヤー」が返ってきているとしても、その詳細はわかりません。エラーがあったということしか分からないのです。もし重要なエラーが発生しているのに気づかないと、重大な問題になる可能性もあります。
そのため、
try?
の使いどころは注意が必要です。
do-catch
を使った方が良い場合もあるかもしれません。
do-catch
でも、キャッチを忘れると重大なエラーを見逃す可能性があります。エラーハンドリングは高度な技術や気配りが求められるということですね。 とりあえず、スイッチ文について話しますね。スイッチ文では完全網羅性が求められますが、例えば
default
を使用すると、その安全性が欠けることがあります。27行目のような記述がない場合でも、ディフォルトのケースを使うことで急にスイッチ文の安全性が低くなってしまいます。
エラーハンドリングについてもそうです。あらかじめ想定されるエラーに対してキャッチを書いていけば、未対応のエラー(例えば未予期のエラー)を拾い損ねることがないわけです。しかし、実際の使用上、どうしてもデフォルト的なキャッチが必要になることがあるため、難しくなります。
この点で言えば、スイッチ文はよくできていますね。ただし、エラーハンドリングについては前回も話した通り、試験差の例外処理なんかが絡んでくる場合もあります。想定できるエラーのときには検査をしてもいいかもですが、現時点での仕様では、どんなエラーが出てくるか完全には想定できない状況です。
ですから、しっかりとプログラマー側が想定しながらコードを書く必要があります。コードレビューを行う際にも、ルーキャッチ(例外キャッチ)はその視点でチェックしていく必要がありますね。そんな感じです。 とりあえず、難しそうだということが感じ取れましたが、そういったのが
try?
から伺える感じですね。エラーが起こったら
nil
を取らなくても落としてもいいという場合は、
try!
を使います。しかし、なぜかこの Swift ツアーでは
try!
は出てこないんですよね。
try!
は例えば Swift で簡単なシェルスクリプトを作るようなときには結構使えます。エラーが発生したらすぐに中断して落とすような使い方ですね。こういった感じがエラーハンドリングの基本的なやり方です。
try!
try?
try-catch
、あとはエラー対応における
defer
の動作。この辺りが Swift ツアーに綴られていましたが、よく紹介されています。ここは結構面白いポイントです。
defer
を使うと、エラーが発生したかどうかに関わらず実行できる、そういったことが実現できます。別々のタイミングで実行されるセットアップとクリーンアップを隣り合わせて記載可能で、エラーが発生しようともそのときに行うクリーンアップをこうやって隣り合わせて記載可能です。
具体的な例を示すと、例えばクラスのインスタンスプロパティのようなものを関数を読んだときに
True
にしておいて、終わったときに
False
にするような処理を並べて書けます。この説明だけだと
defer
の話になってしまいますが、エラーが発生してもそのクリーンアップが隣り合って記載できるというのが大事なポイントです。
これがエラー対応における
defer
動作の基本です。次に、
defer
を使うことでどれだけ便利か、具体的な例を紹介します。例えば
defer
の関数をざっくりと作ってみましょう。
func exampleFunction() { var flag = false flag = true defer { flag = false } // ここで何か処理をするが、途中でエラーが発生するかもしれない if someCondition { return } // 正常な処理 }
エラーハンドリングが絡んでくると、途中でエラーが発生した場合でも、
defer
ブロックに書かれたクリーンアップ処理が必ず実行されます。これにより、冗長なエラーハンドリングコードを避けることができますし、クリーンアップ処理の漏れを防ぐことができます。
具体的にエラーが発生した場合の例を見てみましょう。
func exampleWithErrorHandling() { var flag = false flag = true defer { flag = false } do { try someFunctionThatMightThrow() // 追加の処理 } catch { // エラー処理 } }
このように書くことで、
someFunctionThatMightThrow
がエラーを投げたとしても、
defer
ブロックに書かれたクリーンアップ処理が必ず実行されることが保証されます。
ここで気になるのは、
defer
ブロックを
do
ブロック内に入れる場合の動作です。例えば、以下のように書いた場合、
func exampleWithDoBlock() { do { var flag = false flag = true defer { flag = false } try someFunctionThatMightThrow() } catch { // エラー処理 } }
この場合、
defer
ブロックは
do
ブロックの終了時に実行されます。これは
exampleWithErrorHandling
と同じ動作となります。
defer
を使うことで、エラーが発生しようとしまいと、クリーンアップ処理を漏れなく実行できます。これにより、コードの可読性と保守性が向上します。 ドゥブロックが終わったときに実行されるのがキャッチブロックが終わったときに実行されるのがちょっと気になりました。本当にちょっとやってみましょう。シンプルなコードに変えてみますね。とりあえずファンクションを呼べば済むんですけど、ファンクションを取り除きます。そしてここでファンクションを取り除くとテストがしにくくなるかもしれませんが、とりあえずこうしてそのままやってみましょう。
まずはこれでやってみます。このときに
print
defer
を使います。あとは、えーっとどこに入れればいいか考えます。そうですね、まずは実行してみましょう。ドゥの後かキャッチの後なのかが知りたいので、ちょっとこんがらがってきましたが、動かしながら修正します。
こうした場合、これだと何の問題もなく
print
defer
が出るだけですね。
defer
だから、ここでえーと、スタート。書いてみますが、今回はドゥを抜けたときなのか、キャッチも含めて終わったときなのかを見極める必要があります。エラーが出ればドゥブロックを抜けるので、キャッチのタイミングを見ましょう。
とりあえず
do
のスタートとエンドを確認するために、以下のようにしてみます。
do { print("do start") // 他の処理 print("do end") } catch { print("catch start") // エラー処理 print("catch end") }
そして、実行してみます。正常なときには、
do start
do end
が表示され、その後
defer
が実行されればいいですね。そして関数の終了も見ればよいでしょう。
エラーを返す場合、ドゥを抜けたときにキャッチが出てから
defer
が実行されるのか、または
defer
が出てからキャッチが実行されるのかを調べるために、以下のようにします。
do { print("do start") // エラーを発生させる throw someError print("do end") } catch { print("catch start") // エラー処理 print("catch end") } defer { print("defer") }
このとき、
catch
の前に
defer
が実行されるかを確認するために、コメントを読みながら考えましょう。
コメントには「ドゥブロックを抜けたときに1票」「キャッチは省略可能なので」などの記載があります。
defer
は単純にスコープを抜けた後に実行されるイメージです。ディファーの中でドゥブロック内で実行され、関数を抜けた後に実行されるという考えもあります。
自分の予想では、
do
ブロックを抜けたときに
catch
の直前に動作するような気がしますが、これは意外と
defer
catch
の前に動くかもしれません。いずれにしても、
do
の中で
defer
が積まれて実行されるだけかもしれません。
では実際に動かしてみましょう。
この結果から得られることは、
defer
のスタックがどこに置かれているかということです。
do
ブロックの中に置くか、
do
の外に置くか、クリーンアップが必要な観点では重要です。
試してみた結果、
catch
がどうなるか、どの順番で動くかを確認しましょう。
defer
の後
catch
になった場合、
do
ブロック内で動いた感じですね。
これをまとめると、
defer
をどのスコープに入れるかが重要であり、正しく意識して配置する必要があります。 キャッチの中で
freezeOpen
のフラグを確認したいときなど、多分こういった仕様は大事ですね。
そうですね、このフラグ確認の仕方も重要です。ここでフラグを表示しておけば良いでしょう。後始末がちゃんとできて
false
になっているかどうか、後始末前の状態が確認できると見やすくなります。つまり、まだオープン中ですよという状況でキャッチされるかどうかですね。そういうことです。
例えば、
do
ブロック内でファイルハンドルをオープンしている場合など、これは非常に自然な
defer
の使い方だと思います。外で考えると確かにその通りですね。だから最初このコードを書くときに、
freezeOpen
do
ブロックの中に入れるべきか、外に出すべきか迷いました。しかし、論理的には
do
の外に出しておかないと少し誤りが生じますね。
do
ブロックの中で実行した時点で処理がエラーハンドリングを始めてしまうので、それより前にちゃんとフラグを
true
にしておかないと、例え問題がなくても論理的には少しおかしいように思います。
defer
の動作も面白いです。他のコメントにもある通り、
defer
はブロックの最後で必ず動きます。今回の例では、20行目の
defer
の場合、そのブロックの一番最後、具体的には
print
文の直後に動くと考えて間違いありません。他にもリターンの後などで動くために少しややこしくなる場合もあります。要は40行目の並括弧の直前、つまり関数の抜ける直前で動作するのです。
簡単に整理すると、リターンが実行された直後に
defer
が動作します。このように理解するとわかりやすいでしょう。
次に、
defer
の実装方法について考えたときに、クロージャを持っていて変数として持っておくイメージで、スコープを抜けたら変数が解放されるという形です。ローカル変数の解放のような感じです。そういった考え方が一般的です。
defer
のポイントは、エラーであろうとなかろうと後始末の処理を追加できるという点です。つまり、他の言語にある
finally
構文に似ています。エラーハンドリングとしては
do-catch
に直接結びつけられていないため、完全に切り離せます。これにより、エラー発生の有無に関係なくクリーンアップが行えます。
do-catch
を使用していない場面でも
defer
を利用できるため、活用範囲が広がる点が非常に面白いところです。
最後に、デストラクタを使った例について紹介したいところですが、それと加えてもう一つ話したいことがあります。それはブロックの最後で
defer
が動くという話です。 リターンの直後に動くっていう話を、少し見ていきましょうか。せっかくなので、まずはそうですね、実装について話してみましょう。
例えば、関数があって、そこに
defer
があって、次に
print("defer")
があって、さらに
print("return")
があって、その後にリターンするというコードがあったとします。
func myFunction() { defer { print("defer") } print("return") return }
この関数を実行すると、
return
の後に
defer
が実行されることがわかります。実行結果は以下のようになります。
return defer
次にもう少し複雑なケースを考えてみましょう。例えば、
defer
の中に初期化子を使ってみたい場合です。
func myFunction() { defer { print("defer") } print("return") return let x = 10 }
この上で、
defer
の動作を理解するためには、
defer
ブロックがどのようにスタックの最後に積まれるかをイメージすると良いかもしれません。関数の呼び出しやスタックの動きが理解できる人は、そのスタックの直前(つまり関数がリターンする直前)に
defer
が実行されるというイメージを持つとわかりやすいでしょう。
また、
defer
ブロックの中でリターンを書くことについて考えてみましょう。これはエラーになります。
defer { return }
これはコンパイルエラーになりますね。一方で、
do
ブロックや
catch
ブロック内で
defer
を使うと、少し動作が変わります。
do { defer { print("defer") } throw SomeError.example } catch { print("catch") }
この場合、エラーが投げられると同時に
defer
が実行されることになります。
また、コメントを拾いますと、「タップが戻る直前に
defer
を積むイメージ」という意見がありました。まさにその通りで、関数やブロックが終了する直前に
defer
が実行されると考えると理解しやすいです。
さらに、
defer
ブロックで
break
continue
を書けるかどうか試しましたが、こちらもエラーになります。例えば、以下のように書くとコンパイルエラーになります。
defer { break // エラーになります }
guard
文と
defer
の組み合わせについても少し見てみましょう。
for i in 1...5 { guard i != 3 else { defer { print("defer") } break } print(i) }
このコードを実行すると、
guard
条件が満たされないときに
defer
が実行されてからループが終了します。
さて、最後に Swift Playgrounds の実行でちょっとした問題が発生しましたね。
guard
defer
の組み合わせについても少し実践してみましたが、うまくコンパイルが通らない場合があります。この場合は、コンパイラのバグであるかもしれませんし、特定の条件下での動作が想定外の結果を招いている可能性もあります。
面白かったですね。次回も引き続き、Swift の言語仕様や関連するプログラミングの理論について一緒に学んでいきましょう。 やっぱり、はい、まあ、それはさておきという話になりますが、では始めましょう。コードを例に説明しますね。
必ずしも
deinit
defer
の代わりができるとは限らないので、注意が必要です。ライフタイムに関する話なんですが、
withExtendedLifetime
というものがあります。これを使うと、少なくともブロックが実行されている間はオブジェクトが解放されないという関数ですね。これがあるということは、生存期間をしっかりと延ばさないと、このコードの11行目が解放されてしまう可能性があるという解釈ができます。ですので、必ずしも
defer
のようなクラスを作ったからといって、必ず動くとは限らないという点には注意が必要です。
もう一つ紹介したいコードがありますが、時間がない方はここで終わりにしてもらっても大丈夫です。お疲れ様でした。後でスラックに貼るので、そこを見てもらえばいい話ですが、デファーの面白い使い方をもう一つ紹介します。
たとえば、
Postfix
演算子を実装する際に、
i++
のようなコードがありますね。例えば、変数
i
が 0 として
print(i++)
とすると、最初に 0 が表示され、次に 1 が表示されるというコードです。これを実装する際に
defer
が面白い動きをします。
postfix func ++(value: inout Int) -> Int { defer { value += 1 } return value }
こうすると、現行の値を返し、その後に値を1増やすという動きをします。
defer
がどのタイミングで実行されるかが明確になる良い例です。
defer
の記述はスコープの最後に実行される約束になっているので、
return
が終わった後に実行されるわけです。
プレイグラウンドで確認すると、最初に0が表示されて、その次に1が表示されるように動きます。
defer
を使用しない場合は、値の増加を先に行ってしまうため、
return
の値が異なります。
defer
を使うことでこうしたテンポラリな変数なしで、期待通りの動きを実現できるのです。
このように
defer
の動くタイミングを理解すると、より柔軟なコードを書くことができます。
このコードは
OJT
チャンネルに貼っておきますね。では、今日の勉強会はこれで終わりにします。何か質問はありますか?大丈夫そうですね。
今回は
defer
について理解が深まったと思います。また、活用の幅が広がり有意義な回になりました。では、この辺で勉強会を終了します。お疲れ様でした。ありがとうございました。