https://www.youtube.com/watch?v=gzMT-IY57-M
今回は Swift.org の About Swift
に記載されている Swift の追加機能、そこの最後の項目にあたる 高度な制御構文
について見ていきます。Swift には guard
や defer
といった特徴的かつ便利な制御構文があるので、その辺りの特徴をしっかり押さえておきましょう。 どうぞよろしくお願いしますね。
————————————————————————— 熊谷さんのやさしい Swift 勉強会 #7
00:00 開始 00:25 guard 05:05 早期 Exit 10:34 前提条件 12:03 Fall through 14:56 for ループでの guard 17:27 do 構文での guard 21:41 do 構文とラベル 26:16 Never 29:56 複数の guard を使う 32:54 precondition 36:22 switch 文と guard 38:12 guard と fallthrough 39:06 オプショナルバインディング 42:44 repeat 48:10 repeat と continue 50:39 質疑応答 53:34 構造化プログラミング 55:01 次回の展望 —————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #7
今日は、その次の「ガード」「リファー」「リピート」あたりを見ていこうと思います。これだと時間内に終わるかな? 終わりそうなら次に進んでいきたいと思います。では、まずガードからやってみましょう。
このガードがまたいい感じの機能で、一般的には「早期リターン」と呼ばれています。この機能はSwift言語自体も含めて、迅速に処理を抜け出すことを保証するための言語構文です。口で説明しても分かりにくいので、Playgroundで具体的に例を見ていこうと思います。
デバッグを行う際、ガード文を活用する方は多いかと思います。構文自体はとてもシンプルです。例えば、次のように関数を書いてみましょう。この関数内でガード文を活用してみます。
まず、基本的な関数 dividing(by:)
を定義します。この関数は整数を引数にとり、ゼロで割ることを回避するためのガード文を含んでいます。
extension Int {
func dividing(by value: Int) throws -> Int {
if value == 0 {
throw CalculationError.dividingByZero
}
return self / value
}
}
この際、エラー処理も活用してゼロで割る操作を防ぐ方法も合わせて見ていきましょう。具体的なエラーとして CalculationError
を定義し、特に dividingByZero
というエラーを用意します。
enum CalculationError: Error {
case dividingByZero
}
上記のコードの流れを確認しましょう。もし value
がゼロでない場合、通常通り計算が実行されます。しかし、もしゼロで割ろうとすると、エラーをスローするようにしています。
ただし、ガード文を活用すると、よりシンプルかつ明確なコードになります。以下のように書き換えます。
extension Int {
func dividing(by value: Int) throws -> Int {
guard value != 0 else {
throw CalculationError.dividingByZero
}
return self / value
}
}
このように、ガード文を用いることで、条件が満たされない場合に早期に処理を終了させ、下のコードの可読性を高めることができます。ガード文を用いることにより、インデントの増加による読みにくさを解消することが可能です。
もし、このスローで処理が終わることがわかりにくい場合は、エラーハンドリングに慣れていない人のために、以下のようにイメージしてもらえるかと思います。
extension Int {
func dividing(by value: Int) throws -> Int {
guard value != 0 else {
throw CalculationError.dividingByZero
}
return self / value
}
}
この例から分かるように、ガード文を積極的に活用し、可読性の高いコードを目指しましょう。 こういう便宜上リターンが入っているものと思ってもらえればいいかなと思います。ここで大事なことに繋がるんですが、このようなコードを書いたときに、うまくいっていればいいんですけど、例えばこの条件処理の中が何らかの形でコードが長くなってしまったときや複雑な条件があったときに、何らかの事情でエラーを返すタイミングやリターンするタイミングを逃してしまうことがあります。
こうしたときに、この書き方だとエラーが何も表示されず、エラーとして扱われずに計算が進行してしまいます。今回の場合はゼロで割ったときにランタイムエラーが発生するため、それで気づくわけですが、コードによってはもっと後になってから気づくことや、データを壊してから気づくことがあり得ます。それを防ぐのが、ガードの大事な役割となります。
実際にエラーが起こると、「bad instruction division by 0」というランタイムエラーが発生します。例えば、print
を使ってみましょう。このように表示された後にランタイムエラーが発生します。本来ならここでエラー処理をしてエラー想定を終わらせるつもりだったのに、print
まで行ってしまう場合があります。
こういったうっかりミスを防ぐのがガードです。今からガードに書き換えていこうと思います。大事なポイントとしては、if
文の場合はそれを満たしたときの条件を書きますが、ガードの場合はそれを満たさなかった条件を書くという点です。else
を使うんですが、これは前提条件となっていて、その前提条件を満たさなかったときにはガードのブロックが実行されます。前提条件を満たした場合にはガードの中は実行されず、処理が進みます。
ガードの重要なポイントは、ガード内では必ずガードのスコープからその先に行かない手段を取らなければならないというルールがSwiftにより課されることです。これはとても大事なところです。逆に何か条件を間違えてガードの中できっちり処理を終わらせないと、Swiftがそれを検知しコンパイルエラーを返してくれるポイントになります。
例えば、ガードの中でエラーを返す際には、スローまたはリターンを使ってスコープを抜けなければならないといったルールがあり、これをフォールスルーしてはいけません。フォールスルーとは、この処理ブロックをそのまま抜けて下へ行く意味ですが、ガードではフォールスルーは許されません。
ガードを使うと、前提条件を満たさなかった場合にエラーを返す処理を確実に書くことができます。例えばforループの中で、ガードを使ったときの処理ですが、以下のように書くことができます。
for element in array {
guard condition else {
continue
}
// 処理
}
このようにすることで、条件を満たさない場合には次のループに進むといった処理が可能です。また、強制終了するfatalError
やbreak
を使うことも可能です。
最後に、do
文を使うことで、ガードの抜ける範囲を限定することができます。例えば以下のように書くと、do
ブロック内でのみガードが適用されます。
do {
guard condition else {
break
}
print(x)
}
このように、ガードの使い方を工夫することで、コードの安全性と可読性を高めることができます。 でもエラーですよね。なにこれ、かっこよくないですか?これ通るんだ。逆にこのtrue
は間違ってないですかとか、警告ぐらい欲しい気がしますよね。賢いけどちょっと「俺様賢いぜ」的な仕様。こんなこともできちゃうんだよぐらいにしかなってないような印象があります。
そもそもこのコードいらないですかとか、何か書けば言ってくれるかな。そこは到達しない可能性がありますね。
ああ、言いますね。素晴らしい。これ言ってくれてればOKですね。
なるほど、ここ嘘でも何かの条件を書かないとダメですね。クロージャにしちゃおうか。クロージャならごまかせるでしょう。
ここでは使えませんか?ああ、そうですか。じゃあ、まあここでいいでしょう。let flag = true
。これだとガード文で何もやってないからエラーです。エラーとされるわけですね。ここでループガードを活用させるって言ったらどういう感じになるんでしょうね。break
っていけるのか?ループではダメですね。
どこの発想が間違っているかもしれません。でも、ここから何かを見つける人とかが登場したりするのが面白いんですよね。とりあえず何か絡めてみると、何か見つけられることもあるかもしれません。
ここでexit
とかで抜けちゃうとか。exit
ってそういうものはないのか。return
しかない。これはPlaygroundだからかな。ダメか。exit
あるはずですよね。インポートがちゃんとしてないからかな。Playgroundだからじゃないですかね。そうかもしれないですね。
Cライブラリ入れればできるかもしれないですけど、import
。今DarwinCって使うの?わからないですけど。exit
、色変わった。こうするとexit(0)
ですね。これならいいですけど、これだとそもそもDoブロックはいらないですね。でもちょっと面白い。このDoとガードで何か面白いことを見つけたっていう人がいたらちょっと勇者になれる。
そのブロックでラベルつけられるかな?ラベル、ラベル、ラベル。loop
でよく使われてますか?よくじゃないですけど使えますよ。ここに書くんですよね、確かに。こんな感じですよね。ラベルは付くんですね。じゃあbreakラベル
っていけるんじゃないですか?もしかして。いけたら嬉しいですよね。
行けました!行けました!勇者が誕生した。つまり、何か局所的にガードがいけるって面白いですね。ラベル使うといきなりbreak
OKになるんだ。まぁ言われてみれば、今exit
消しましたよね。そうそう。
そうしたらあれですね、Doの中にもう一個Doを作って、2つラベル。中の方のDoにラベルをつけてあげると、ガードの中で内側のラベル付いてるDoスコープを抜けることが可能ですね。そうですね。その内とか中とかに限らず、とにかくラベルを付けてるDoの中にガードがあれば、そのラベルで指定したDoを抜けられるってことですね。
書きましょうか。ラベル1とあって、ラベル2とあって、Doと書いて、それでこうやってラベル1を抜けることもできるし。プリントを書いておこうかな。これがラベル2でしょ。で、これがラベル1。で、print("Outside")
と書くと、今の状態だとラベル1を抜けるから。
ここを無くすとどうなるかをちょっと調べてみたい。こうするとちゃんとパスしますね。ラベルなしbreak
が禁止されてるってことですかね。エラーメッセージが点々になってましたけど、ラベルbreak
が要求されるってちゃんと書いてありましたね。
エラーを見るといろいろ発想が広がりますね。コメント見て笑ってしまった。もはやGoTo文みたいですね。派手には戻れないですけどね。
これを駆使するときっと煙たがられるんでしょうね。GoTo文みたいに自由にあちこち飛び回れないから。そうですね、急にもう一回ガードに飛ぶとか、そういったことはできないんでね。
面白かった。こういうふうにガード文を駆使していくと、ガード文を途中途中で実行したい場合、都度都度後始末をしたいときに役立つかもしれない。派手なコードを書くときでしょうけどね。面白かったです。
もう一個コメントにあるfatalError
とかの話も面白そうだから見ていきましょうかね。例えば、関数ignore
みたいな関数を作ったとして、Never
を返す関数を書いたときに、これだけでいいんでしたか。 関数を自分で定義できるのか、これはダメですね。func is unhappy
という知らない単語が出てきてしまいました。ネバーを返す関数に繋げないといけないと言われたのですかね。ここでフェータルエラーを呼ばないといけないんですね。ああ、なるほど。
この時に突破したくなるんですよね。unsafeBitCast to Never
。一応リターンを省略しないで書いてみますか。無理やりネバーを作って返したのですが、こうした時にここでbreak
とかではなく、ignore
を呼んであげると、これでもう間違っていますね。
func
の綴りが違いました。これで公文エラーは出なくなるのですかね。bad instructionだけね。true
にしてあげると分かるかな。コンパイルエラーはなくなって実行が進みます。
そうですね。とにかくフェータルエラーが呼ばれるとか、コメントでいただいているexit
もそうですね。たぶん、とにかく戻り値がネバー、要は戻り値がないよっていう状態、そういう状況では戻り値がないと先へ進めないので、たぶん、こういうコードを書いてあればガード分の役目は果たせたということですね。ここでランタイムエラーで落ちてくれるっていうね。そんな感じになっています。
まあ、ここまでは知らなくても大丈夫ですが、とりあえずね。ついでに見ておきましょう。そう、exit
もネバー型返してますね。ここですね。なるほど、なるほど。ネバーを返す関数なら強制中断できるよという話は、意味不明な方は意味不明で全然大丈夫です。そういうコードは消しておきましょう。
このbreak
面白かったですね。こういったものがまず基本的な基本、ちょっと飛び越えていますが、ガード分の動きになるのですが、あと面白いのがガード分が何個もあった時のお話。入れ子や並行処理ができるのですよ。
これはガード分の紹介をする時に詳しく言った方がいいかな。ガード分が出てくるのも相当先だと思うんで。例えば、ガード分の中にガードを書く。すごい勘違いでしたけどやってみましょう。
例えば、let f1 = false
、let f2 = false
、guard f1 else guard
。まあ、書いていけば普通ですね。これでreturn
でしょ。これでprint
でしょ。まあ、普通ですね。これを実行すると、何もせずに終わるまで行く。print
ここガードでしたね、return
。print("A")
、print("B")
。ここですね、ここ。
とにかくその今ガードを早期エグゼットし忘れてエラーでなんか自分がコード直してましたけど、これがガードの大事な特徴メリット。これがもしif
文でやってたら気づかないまま書き進めてましたからね。そういうところがあるので、前提条件を書くときには必ずガード分を積極的に使っていくと、コードを間違えにくくなるし、メンテナンスした時にもリターンが抜けちゃってたよみたいなことが防げるのです。
そういった意味で、ガードを使うのとプレコンディションを書くのはなんとなく似ているイメージ。だからここでプレコンディション、これラベル使ってるからちょっとややこしいな。さっきのところに戻りますかね。
ガードを使うのとプレコンディションを書くのは似ているとして、ここでプレコンディション、ここは難しいな。入れ子にしちゃってるからちょっと違うんですけど。要するにガードの方が使いやすいわけです。
デバッグ時にはこの条件判定をして、ダメだった時にはランタイムエラーを起こすみたいな、そういったものと似たような動きになります。今回言いたかったのは、前提条件を書くためにガードを使うと、ガードを使うポイントやタイミングが見えやすくなるということですね、ちょっと紹介してみました。
プレコンディションは多分リリースビルドでも引っかかります。プレコンディションは最適化のところで-Ounchecked
を入れると受けます。リリースビルド通常は-O
だけなので、アサートですね。アサートがデバッグだけのやつですか。
そうですね。なんか3種類ぐらいありましたよね。もう1個がフェータルエラーになるのですね。思い出しました。3つが、アサート、フェイタルエラー、プレコンディションフェイタルエラー。そうですね。フェイタルエラーは無条件ですもんね。はい、そうですね。なんか3つ似たようなのがあった気がしましたけど。まあ とりあえずこんな感じですね。
はい、そうですね。なるほど、ガードがプレイコンディションに近いということですね。最適化しすぎると消えることがあるのは別として、ガードについては大体こんなところで大丈夫だと思います。次に、スイッチ文でもガードが使えることを一応紹介しておきますね。
スイッチ文で例えば:
let number = 10
として、スイッチ文を書きます。例えば、ケース0だったら何か処理をして、ケース10だったらというところにガードを使います。例えば:
switch number {
case 0:
// 何か処理
case 10:
guard 条件 else {
// エラー処理
return
}
// 条件が成立した場合の処理
default:
break
}
このようにガード文を使って、適切なエラー処理を行うことができます。スイッチの中でブレイクが使えたり、関数の中であればリターンやエラースローも可能です。ガードは条件に失敗した場合に指定した処理を行い、次のケースに移行できるため、スイッチ文の中でも有効です。
さらに、ガード文ではオプショナルバインディングが使えることも補足します。例えば、オプショナル型の変数があったときに:
guard let x = n else {
// エラー処理
return
}
// xが使える
このように、n
がnil
でない場合にx
を使って処理を継続させることができます。
if let
文での書き方と比較してみましょう:
if let x = n {
print(x)
} else {
fatalError("値がnilです")
}
この場合、if
とelse
のブロックが対等に見えますが、ガード文を使うと前提条件が満たされなければエラー処理を行い、満たされた場合のみ処理を継続するという、より明示的な意図が伝わります。
次に進みましょう。次はリピート文について話します。リピート文は、他のC言語系列の言語のdo while
に似た構文です。例えば:
repeat {
// 処理
} while 条件
このように書きます。例えば:
var flag = true
repeat {
// 処理
if Int.random(in: 1...10) == 1 {
flag = false
}
} while flag
ここで、flag
がtrue
の間はリピートします。リピート文のポイントとしては、少なくとも一度は処理が実行される点です。while
文では最初に条件判定が行われますが、repeat
文では少なくとも一度処理が実行され、その後条件が判定されます。
この違いを理解して、適切に使い分けることが重要です。リピート文が適しているケースもありますので、使いどころを見極めて使っていきましょう。
以上で、ガード文とリピート文の説明を終わります。他に質問があればどうぞ。 とりあえず、この特性を覚えておいて、最初の文字「while」を書いて、最初の条件が何かダメっぽいなと思った時には「repeat while」を使って、次からの2回目以降の条件判定を行うと、条件判定が1回だけ得するという感じの構文です。まあ、知らなくても「while」を使えば大丈夫だと思います。
ここで「continue」するとどうなるのか、ご存知の方はいますかね?どういうコードを書くと試せるかな。「continue」すると条件判定が飛ぶのか。だから、flag
をここでtrue
にして、false
にして、ここで「continue」した時ですよ。だから、ここでprint("repeat")
、ここでprint("while")
とします。そして、flag
をfalse
にして、「continue」と書きます。でもこれだけだとダメですね。flag
をtrue
に書き換え直して、どうなるか見てみます。「repeat」が1回、「while」が1回、「repeat」が1回か。
じゃあ、「continue」って言ってもここへ来るのね。そういう話じゃないか。ここへ来るんだ。そうそう。「continue」って聞くと、このブロックの先頭に戻るというイメージを勝手に持っていましたが、条件判定はするようですね。言っている意味、わかりますかね?まあ、分からなくてもいいんですけど、ちょっと脱線もしましたが、こういった話が「repeat」文になります。
一応、今日話しておこうかなと思う題材はこんな感じです。せっかくあと5分あるので、思いついたことや聞きたいことがあれば、どんどん割り込んでもらえればと思います。コメントでも、声でも。そう言いつつ、いろいろ適当に自分も思いつくことを話しているので、割り込んでくださいね。
さっき初めの方に紹介した「guard文」ありますよね?これを「while」文の中でも使うことができます。「guard」で例えばflag
がオプショナルだった時、その部分はあまりクローズアップしなくていいんですけど、ガード文を書いてみて、それで「continue」みたいな書き方もできます。「while」と「guard」文を組み合わせた時、別に「while」文に限った話ではないんですけど、「guard」の保護するエリアはこのブロック内で、このブロック内はループするたびにその都度「guard」が走るというイメージです。慣れてくると普通にそう感じるかもしれませんが、慣れないうちはループとこういう特殊な処理が組み合わされると混乱しがちなので、注意が必要です。また、理解しておくと便利です。「continue」ならループが続きますが、「break」ならブロックを抜けるという違いがあります。これだとループを抜けますね。「continue」と「break」の性質の大きな違いです。「continue」は次のループへ回る。「break」はループを終えるという大きな違いがあるので、全然違うコードになってしまいましたけど、こんな感じですね。
なかなかコメントを見て思いますけど、さっきお話ししたケースと「guard」と「fallthrough」が必要な場合、便利そうというコメントがありますね。確かに、組み合わせ次第でいろんなことができます。構造化プログラミング的に言えば、ファイルとかループとか、要はスコープを作ってそのスコープを組み立てて、そのスコープの扱いによって制御していく、といった感じです。ケース分ももちろんそうです。構造化プログラミングって多分昔はそう言っていましたが、それを駆使していく。その応用力の幅を広げるという意味で、「guard」や「fallthrough」や「case」が使えるわけです。確かに賢いほど、すごい応用が期待できそうな印象がしますね。
これでおおよそ時間になりましたので、今日の勉強会はここで終わりにしようと思います。また次回は、続き「defa」のお話をしようと思いますので、時間が合えばぜひ来てください。それでは、お疲れ様でした。ありがとうございました。