今回は The Basics
における オプショナル
の最後の項にあたる自動で強制アンラップされるオプショナル
について眺めていきます。かつては "オプショナル型とは別の型" として存在していたものですけれど、今は "オプショナル型" を支援する機能の一環として用意されている機能、そんな観点を意識しながら自動的に強制アンラップされるオプショナルについて眺めてみると何か良いことあるかもしれないです。
今回もゆめみ社外に向けた参加者公募もありまして、ゆめみの外の人も招いた開催になります。どうぞよろしくお願いしますね。
—————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #170
00:00 開始 00:55 前回のおさらい 01:59 インデントが浅くなる利点 03:24 インデントを浅くするのを意識してみる 05:33 インデントを浅くしてみる 06:12 タブ文字の問題 08:32 似通った意味合いのものを単一行に集める 09:25 短絡評価を踏まえた解釈は必要 10:11 条件適否のパターンを整理 12:23 guard による早期判定 13:30 ラベル付き if 14:31 ラベル付き break は普段、使うもの? 16:05 ラベルによる制御は避けがち 19:24 少し訂正と、少し所感 20:29 ラベル付き if における guard の挙動 23:37 良い意味で応用力が高い機能かもしれない 26:57 これよりシンプルな構造で表現できるならそれが良さそう 27:14 ラベルを付ける対象に do は必要? 27:41 波括弧で始まるものはクロージャーと解釈 28:57 ブロックをシンプルに整理してみる 29:25 if を入れ子にしたときの早期脱出には向くかもしれない? 30:49 複雑なブロック構造の制御を迫られたときに知っておきたい 32:25 実際にラベルを使ってみたときの例 35:35 無限ループや多重ループを疑ってみる 37:20 終端があればループの見通しが効く 38:47 今回の所感とクロージング ——————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #170
さて、始めていきましょう。社内ではオプショナルの自動強制アンラップについて話すと予告していましたが、今日はまずオプショナルバインディングについてのスライドを見ながら進めていきます。非常に初歩的な内容になりますが、理解を深めるために重要です。残り時間で自動強制アンラップについても触れていけたらと思います。
オプショナルバインディングで複数の条件判定ができるというスライドに戻ります。これに関して、前回少しだけ触れましたが、今日はその続きからです。
上の例では、複数の条件を繋げた if
文の書き方です。この方法では、条件が満たされなかった場合の書き方が対照的に並べられています。このような書き方をすると、インデントが深くなりがちです。インデントが深すぎるとコードが見にくくなりますよね。インデントの深さが問題になることが多く、最近ではインデントの空白を4文字から2文字にする議論も見かけます。その方がコードが読みやすくなるという意見もあります。
一般的に、インデントは浅いほうが良いです。そして、自然に収まるならば、インデントが浅いほうが見やすいです。このように、インデントが深くならないように工夫することが大事です。
例に戻って見てみると、上の方法ならインデントは1段だけです。しかし、1行が長くなると画面から外れてしまうこともあります。条件式が見にくくなるかもしれません。下の方法では、インデントが3段になりますが、3段目は1行しかありません。この程度であれば読みやすいです。どちらの方法が良いかはケースバイケースですが、適切にインデントを使い分けてコードを書くことが重要です。
さて、このインデントの問題をどのように解決できるか、コードを使って試してみます。まず、例として以下のコードを取り上げます。
if let value1 = optional1, let value2 = optional2 {
// 処理
}
このコードに対して、インデントを浅くする方法を試してみましょう。また、グローバルフェンスも使わないでインデントを工夫する方法を考えます。 こうやって動かしてみた結果、画面表示が非常に見にくくなってしまいました。勝手に解除を行ったり、自動解除機能があるために、このような現象が起きたのかもしれません。まず、これを直そうと思ったんですが、この現象も興味深いですね。
タブの問題について話しましょう。以前はタブ文字ではなく、空白文字を使うようにと言われていましたね。これはタブ文字が環境によってインデントがずれてしまうことがあるからです。これはインデントに依存するプログラムを書く際に問題になることがありました。例えば、横何文字にするかなどの設定が大事になります。
この設定では、80文字を超えると色をつけるとか、そんな設定があったと思います。このエディターでは「80文字にバイトをつける」という設定はしていないみたいですね。エディターによっては80文字以内に改行するといったルールは結構存在していますが、今みたいな問題になることもあります。エディターによっては妙なところで改行されることもありますよね。これを考えると、タブの問題はあまり変わらないかなと思いましたが。
インデントの話に戻します。例えば、以下のようなコードがあるとします。
if condition1 && condition2 {
// 処理
}
このコードでは、&&
で条件を書いていますね。どちらかと言えば1行目に条件がいっぱい記されているバージョンが好みですが、条件が似通っているならば、1つの変数にまとめてもいいかもしれません。
例えば、以下のように書き直すことが考えられます。
let firstNumber = getFirstNumber()
let secondNumber = getSecondNumber()
if firstNumber != nil && secondNumber != nil {
// 処理
}
このようにインデントを1つ減らすと、コードが見やすくなります。この場合、firstNumber
とsecondNumber
を取得して、両方とも nil でないことを確認することで、条件を簡潔に書けます。
短絡評価の話もありますね。&&
や,
などの短絡評価による条件分岐も同じ動きをしますので、最初の条件が満たされれば次の条件に進むという形です。今のコードは確かに読める範囲内だと思います。
問題は、片方の条件が満たされなかったときの処理です。例えば、以下のような場合です。
if firstNumber != nil {
if secondNumber != nil {
// 両方満たす場合の処理
} else {
// secondNumber が nil の場合の処理
}
} else {
// firstNumber が nil の場合の処理
}
このように条件分岐を重ねると、どちらかが失敗したときにも適切に処理が行われることが保証されます。
さらにコードを綺麗にする方法として、ガード文を使うことが考えられます。ガード文を使うことで、異常系を弾いてから主要処理に進むことができます。
guard let firstNumber = getFirstNumber(), let secondNumber = getSecondNumber() else {
// 異常系の処理
return
}
// 両方取得できた場合の処理
このように、ガード文で一度に両方の変数をチェックすると、主要な処理部分がインデント浅くなり、コードが見やすくなります。
他にも良い書き方があるかもしれませんが、以上のような手法でコードの可読性を上げることができるでしょう。 とりあえず、こうやってガード節を使うと、異常系を弾いた後で一つ一つのインデントで処理ができるという感じですね。やっぱりブレークはダメですよね、スイッチとかラベル付きの処理では。ラベル付けのブレークを使うといいですが、普段あまり見かけないですよね。
普段、if
文でラベルがないと、ブレークするとそのブロックを抜けようとしますが、確かにラベルがあれば便利ですね。「ラベル付きブレーク」というのはあまり普段使わないですね。ブレークを使うのはスイッチ文やループ文の中で、って感じですね。一度だけ質問されて、ラベル付きブレークがあると答えたことがある程度で、自分ではあまり使いません。
伝統的な手続き型言語っぽく、上から順番に処理を分岐させる場合には、ラベル付きブレークが有効な場合もありますが、モダンなプログラミングではあまり見かけませんね。自分で使うことはほとんどないですし、教えることはあっても、実際にコードに書くことは少ないです。
構造化手続き型プログラミングでも、ラベル付きブレークはなるべく避けるようにします。一見すると便利ですが、制御が難しくなる場合が多いからです。goto
的な感じで、どこに飛ぶのかがわかりにくく、制御が複雑化します。ブレーク文は、そのスコープの外に戻ると考えられますが、ラベルを使った場合はその制御がさらに分かりにくくなります。
たとえば、以下のようなコードを考えてみましょう:
L1: for i in 1...5 {
L2: for j in 1...5 {
break L1
}
}
これはL1
のラベルに対してブレークすることで、L1
のスコープを抜けるということです。
このようなコードは、一見してどこに飛ぶのかがわかりにくくなります。制御を理解するのが難しく、コードの読みやすさも低下するため、ラベル付きブレークはなるべく避けたほうが良いですね。普段のプログラムでは、構造化プログラミングの一環として、なるべくシンプルな制御構造を使いたいものです。 そういったことを考えると、コンティニューを使った場合、構造が壊れているように見えるかもしれません。少し言い過ぎかもしれませんが、全体を見たときに慣れていればどっちでも良いかなと思います。全体としてコンティニューするというのは、確かに少しわかりにくい部分もあります。特に コンティニュー L2
と書かれていたら、慣れていない人にはわかりにくいかもしれません。ですので、積極的に使うことを勧めにくい機能ですね。
上の例にあるブレークテストでは、ガードのスコープを外に15まで抜けることができるんです。ガードのスコープを考えると、例えばガードの中で変数を使用する場合、その変数のスコープは14行目までなので、ガードで一気に15行目まで来ると違和感があります。これがなぜ言語仕様上許されているのか疑問に思うかもしれません。13行目では絶対に行かない状況で、ガードでスキップして15行目に行くのはなぜ許可されるのかと考えると、構造化プログラミングの観点で見ると、ガードはブロック内での早期リターンを意図しているからかもしれません。
具体的には、ガードの下のスコープを抜けることができるのは、そのブロックの中での処理を早期リターンするためです。例えば、関数の中でガードを使用して早期に処理を終了させる場合、以下のように書くとわかりやすいです。
guard someCondition else {
return
}
このように、16行目に対する早期リターンのブロックと考えると理解しやすいですね。ガードの下に他の処理がある場合、その処理をスキップするためのリターンのような役割を果たすのです。
初めてこの文法を見たときは、確かに少し不思議に思うかもしれません。ガードがあったらこの下には行かないだろうと感じますが、実際にはガードの中で条件が満たされなかった場合、そのブロックを抜ける動作になります。
この機能は使い方に注意が必要です。個人的には、ガードを使うことが多い場面があるとしても、イフ文との違いを理解して使わないと、コードがわかりにくくなる可能性があります。そのため、ガードの下でのスコープ抜けを適切に理解して使用することが重要です。
いろいろ試してみて、この機能がどのような挙動をするか、そして何が意図された設計なのかをよく理解することが大切ですね。ガード文がその場でのブロックを抜ける意思を明確にするので、イフ文よりもわかりやすいかもしれません。 ブロックを抜けるとき、このブロックがどこなのかが分かることは重要です。また、活動しているガードには意味のあるラベルを結果に付けることが一般的です。特定の例では難しいですが、普通なら例えば22行目が実行されるべき場合に、その内容が24行目に関連することがあります。
ラベルを付けるためにDo
を使うかどうかについてですが、スコープの問題です。Do
ブロックを作成すること自体が言語仕様の問題であり、エラーが出る場合は仕様上必須となります。必要がなければDo
ブロックを使わずに済むこともありますが、計画的にスコープを管理することが重要です。
この例では、もしDo
ブロックを使うのならば、If
文で代用した方が早いかもしれません。ただし、その後の処理によってはブロックが長くなることもあるでしょう。その場合、ブロック内の特定の条件で何か処理が必要ならば、その判断が必要です。
また、guard
文に関しては、どのような条件下で使うかを理解することが重要です。例えば、print
やbreak
のような明確な条件がある場合にはguard
を適用しやすいです。しかし、条件が複雑になってくると、guard
文がどのように使われているかがわからなくなることがあります。
必要なときにguard
やbreak
を使うとしても、それによって条件が漏れてしまうリスクを避けるために、しっかりとテストされていない限りは慎重に使うべきです。
複雑な条件によってスコープやロジックが入り組む場合には、条件分岐やループから一気に抜ける形で使用することが有効です。しかし、そのような場面ではしっかりとコードを維持しつつ、意図的に使うことが望まれます。
これから様々な条件でブロックを扱うことになるでしょうが、条件分岐やスコープ管理には特に注意が必要です。関数に切り出したり、switch
文でさばくなどの工夫が必要ですが、それだけでは追えない状況も出てくるでしょう。
最後に、具体的なコード例として、以下のような場面で使用することがあります。
for i in 1...10 {
if i == 5 {
break
}
print(i)
}
この例では、i
が5になった時点でループを中断し、それ以降の処理を行わないようにしています。while
ループやif
文でも同様の条件処理が考えられます。
たとえば、複雑な条件下でのループ管理が必要なときに、ブレークポイントを設けることで効率的に管理することができるでしょう。このように、コードの読みやすさと保守性を考えながら適切に管理することが重要です。 次のループで進めたい場合、a
を引かずにコンティニューを書きたいのですが、これだと法文にかかってしまうので、while
の中でコンティニューを書きたいという話です。例えば、フラグを作って特定の条件が成立した場合のみ特定の部分をスキップするようにすることが考えられます。ですが、コードが非効率になるので、もっと効率的な方法を検討しています。
フラグをいかに効果的に使うかがポイントの一つです。例えば、if
文を多用しないようにし、continue
やbreak
を上手に使うことで、コードのシンプルさを保ちながら希望する動作を実現できます。このような場合、無限ループにならないように注意が必要です。無限ループにならないための設計は非常に重要です。
無限ループはビジネスデータのダウンロードなどのシステムで発生すると致命的な問題になります。無限ループによるリソースの浪費などを防ぐための書き方や設計の工夫も大切です。特に、フラグを使う場合は、それが無限ループの原因にならないかを十分に検討する必要があります。
ラベルについても見直す良い機会かもしれません。ただし、ラベルを使うとコードが複雑になる可能性があるため、ラベルを使用しない方法が見つかれば、それに越したことはありません。
コードの制御フローをどのように管理するかは経験を積む中で身につけていくことが望まれます。個人のプロジェクトなどで積極的にこれらの制御フローを試してみることで、新しい発見や効率的な方法を見出すことができるかもしれません。
では、時間になりましたので、今日はここまでとします。お疲れ様でした。ありがとうございました。