https://www.youtube.com/watch?v=8SNQK-FlH0g
今回は A Swift Tour
の「制御構文」で扱われている「switch」について眺めていきます。この機能は Swift でかなり徹底的に機能強化が図られたものになっているので、その初歩的な仕組みのところからさまざまな表現方法などなどを限られた1時間の中で見れるだけ見ていけたらいいなって思っています。どうぞよろしくお願いしますね。
——————————————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #34
00:00 開始 00:17 switch の記法 00:50 数値の等価性判定だけに制限されない 02:21 switch 文の終了点 03:17 文字列による switch 判定 04:45 break 07:40 チャプタ 7 09:06 fallthrough 11:11 列挙型による switch 判定 14:19 列挙子に値はない 16:53 fallthrough の使い道を考えてみる 19:45 fallthrough の存在意義 24:17 repeat-while 27:44 ディフォルト的な意味を持つ列挙子と fallthrough の併用 29:47 @unknown と @frozen 41:17 複数のケースをまとめて記述 43:59 関連値を扱うケース 48:28 関連値と fallthrough 49:34 次回の展望 50:08 パターンマッチング 54:31 練習問題 ———————————————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #34
ええと、今日はスイッチ文のお話からですね。様々な種類の値を様々な比較方法で条件処理できるというのが、Swiftのスイッチ文の大事なポイントになります。この「様々な種類の値を様々な比較方法で」、つまりいろんなことができるスイッチ文になっています。
特に、他のプログラミング言語をやっていた方にとっては馴染んでいるようで意外と忘れがちなポイントです。Swiftのスイッチ文の大事な特徴として、数値の等価性や一致性に限らないというところが大きなポイントです。Swiftのツアーの例の中でも紹介されていますが、下に書いてあるコードを見て分かる通り、文字列型でスイッチ文が処理されているという点が大事なポイントの一つです。C言語でスイッチ文をやっていた人にとっては、文字列でスイッチができるというのはかなり斬新です。JavaScriptなら文字列でスイッチができるのは普通ですが、こういったところが大きなポイントです。
次に進みますね。この次のポイントも、多くの言語で見られるものですけど、スイッチ文でケース内のコードが実行し終わるとスイッチ文は終了します。これが当たり前のことのように書かれていますが、慣れている人にとっては当たり前のだけかもしれません。終了したところで明示的に break
を書かなければならない、これがスイッチ文の大事なポイントになっています。
このあたりをまず実際にコードを読みながらやってみましょう。さっきの例題が良いかな、と。たとえば、以下のようなコードをコピーしてきました。
let vegetable = "black pepper"
switch vegetable {
case "celery":
print("Add some raisins and make ants on a log.")
case "cucumber", "watercress":
print("That would make a good tea sandwich.")
case "black pepper":
print("Black pepper is great with eggs and fruits.")
default:
print("Everything tastes good in soup!")
}
このコードでは、変数 vegetable
が "black pepper" の値のときにスイッチ文が実行され、それにマッチした行が実行されて終了します。この点がSwiftのスイッチ文の大事なポイントです。他の一般的なプログラミング言語ですと、次のケース文へ突入しないように break
を書かなければならないことが多いですが、Swiftでは break
が不要です。この点に注意が必要です。
Swiftに慣れ親しんだ後にJavaScriptでスイッチ文を書くと、break
を書き忘れることが自分でもよくあります。しかし、break
という構文はちゃんと存在するので、書くことは可能です。書いても間違いではありませんが、自分の主観としては、書かないほうが良いと感じます。
ところで、Swiftの場合セミコロンを書いても良いですが、基本的には推奨されていないのと同じぐらいの感覚でしょうか。無駄な記述は避け、短く簡潔に書くことがSwiftの設計理念に沿っています。
このように、スイッチ文の中に if
文や while
文をさらに書き込むと過読性が失われるので、避けた方が良いでしょう。どの break
がどの制御文に対応しているかが分かりにくくなります。
以上、スイッチ文の基本についてのおさらいでした。他に質問があればどんどん聞いてください。 これはもしかするとパターンが強力だから、中に入れるのではなくて外に平行に並べるのかもしれません。確かにそう考えていくと、基本的には要らなくて書かないこともありますね。
ブレイクという制御構文でフローを調整する必要がないという感じでしょうか。なるほど、そうですね。このような書き方で進めていくのが良いかもしれませんね。ブレイクについてはこれで OK だと思います。
次に、フォールスルーについても紹介しておきましょう。フォールスルーは、フォールスルーを明示的に書いたときに、そのケース文(例えば11行目のケース文)にマッチするパターンがあると、そのまま次のケースの実行ブロックまで処理が移っていきます。今回の場合、プリント文が2回実行されるという動きになります。
フォールスルーの使いどころは少し難しいのですが、ケース文が上から順に評価されることを理解している場合には、使えないことはありません。私の最近の感触では、フォールスルーを使うことで、下のケースとセットにして追加処理を行う場合などに活用できるかもしれないと感じています。以前はフォールスルーを使うと分かりにくいため敬遠していましたが、順番を変えると処理がおかしくなるのは当然のことですので、使いどころさえ見極めれば役立つこともあります。
さて、スイッチ文についてもう一つ話をすると、列挙型(enum)を使ったスイッチ文が非常に相性が良いということです。例えば、enum で定義されたアクションとして case build
と case clean
があり、その列挙型に対してスイッチ文を使うことができます。ここでは switch アクション
という形で具体的なケースに対して処理を行うことができます。
このように、列挙型に対してもスイッチ文が使えるというのがポイントです。他の言語、例えば C 言語や C++ でもスイッチ文で同様のことができますが、Swift では数字を使わずに列挙型の値そのもので条件分岐が可能です。
Swift の列挙型 action
の case build
には実際の値が割り当てられていません。これは他の言語(C++ など)と異なる点で、Swift では列挙型のケースに具体的な値を割り当てることができないのです。そのため、case build
などに具体的な値(例えば数値)を与えるには、Swift では別途 RawValue
を使って設定する必要があります。他言語では列挙型を整数型に変換することができる場合があり、そのため直接スイッチ文で使うことも可能です。 でも、switch
の場合、Int
に変換する仕組みがネイティブ、純粋なピュアな列挙型には存在していないというところが大事なポイントです。値が具体的に割り当てられていなくても、ちゃんとswitch
文で列挙型をパターンマッチできるというところですね。これが仕組み上とても大事なポイントで、ここを押さえておくと、switch
文の基本的な動きを理解しやすくなります。switch
文になれてきたら、列挙型は純粋な形だと値を持っていないということを押さえておくといいでしょう。
では、次にswitch
文を実行してみます。ビルドを実行してクリーンビルドに変更してみると、クリーンビルドが実行されますね。例えば、クリーンビルドをクリーンしてビルドする動作にしましょう。プリントだけでは分かりにくいので、関数を作ってみます。「func clean
」というクリーン処理と、「func build
」というビルド処理を作ってみましょう。
func clean() {
print("クリーン")
}
func build() {
print("ビルド")
}
そして、クリーンビルドの場合はクリーン処理をしてからビルド処理を呼びます。クリーンビルドが選択された場合、クリーンが表示されてからビルドが動きます。このように、switch
文で「fallthrough」を使うことも可能です。
switch action {
case .cleanBuild:
clean()
fallthrough
case .build:
build()
default:
break
}
この書き方、どうでしょうか。ネットプログラム的には、「if action == .cleanBuild
」の場合にクリーンを実行してからビルドを実行するという書き方と全く同じですね。個人的には下の書き方の方が安心できますが、上記の書き方も一応可能です。
特に複雑なswitch
文を書くときに、fallthrough
を使うこともありますが、書いていてあまり気持ちの良いものではないですね。特定のケースに飛ぶgoto
文のようなものですが、使い道が非常に限定されています。
たとえば、4つのケースがあって1番目と2番目が実行された後に4番目も実行したい時など、fallthrough
だけでは実現が難しいです。そのため、列挙型を用意する時にfallthrough
を入れるべきかどうか悩む部分です。
fallthrough
は、C言語の互換性のために入れているような感じもしますが、使い道が非常に限定されているため、設計上使わない方が良いと考える人も多いようです。そもそも、この文法がSwiftに存在していることすら知らない人もいるかもしれません。
以上が、switch
文とfallthrough
の使い方についての解説です。 普段のプログラミングでは、フォールスルーを使うことはほとんどないですよね。
そうですね。だからこそ、フォールスルーを最初から使おうとはあまり思わないんですよね。いろんな事情でフォールスルーを活用できる場面を見つけられていないというのもあるんじゃないかと思います。
繰り返し構文には while
文と repeat-while
文がありますが、個人的にはあまり repeat
文を使わないですね。
そうですね。でも repeat
文にはたまに使い道があるんです。特に、条件を先に設定できない場合は repeat
文でなければならないので。
私も個人的には10年間で一回だけ使いました。それを使わないと非常に面倒な場合がありましたね。
フォールスルーの後ろにケースが付けられる形がありましたが、使えませんでした。例えば、フォールスルーの後ろに付けられるラベルが、現在の行より下にしか行けない制限があれば、読みやすくなるので、そのような制限があれば良いと思います。
フォールスルーが下にしか行けないという制限をつければ、どこに飛ぶのか分からないという問題が減るので、まだマシかなと思います。
特に fallthrough
と default
の組み合わせは、意外と需要があるのではないかと思います。
フォールスルーデフォルト、つまり try-catch-finally
みたいな印象ですね。ファイナルブロックみたいに。
そのような機能を追加すると難しくなるのか、良くなるのかは未知数ですが、シンプルなフォールスルーでないと制御が難しくなりそうです。
そもそもフォールスルーが必要なのかどうかという疑問もありますね。使う場面を増やしたいのであれば、それなりの面白さが欲しいところです。別に無くても問題ないんですよね、今までも普通に書けていたから。
せっかくだから、フォールスルーのイケてる書き方を知っている人がいれば、ぜひ教えて欲しいです。
では、スイッチ文の他の特徴についても続けていきましょう。
すみません、1つ思い出しました。デフォルトのアンノウンです。これは通常的に使うときですね。上から流れるときに使います。
そうですか。
default
を使う場合ですね。あと、 unknown
というケースがある場合、上からデフォルトに流すときにそれを使うことがあります。でもそれはかなり限られたケースです。
例えば、デフォルトアクションがあったときに、それをすべてデフォルトで同じ処理をする場合には使いますね。
なるほど。
そもそも unknown
を無くして欲しいと思いますが、外部のモジュールがいつ変わるかわからないので仕方ないですよね。外部のモジュールに列挙型が定義されていて、その列挙子が増えたときにあらかじめコードを埋め込んでおくことができないので、 unknown
をつけてデフォルトを書いておくと、ライブラリー側の列挙子が増えたときにデフォルトにフォールスルーしてくるという設定になります。
Swift ではビルド時に静的にわかるので問題ないのですが、Objective-Cのキット系で定義されているものにはそういう問題がありますね。
なるほど、そちらを考慮しているのですね。 あれはなんで静的に解決できないのかわからないです。Swiftではなく、Objective-Cの列挙型はNS_ENUM
で定義して、それで名前と型を定義します。その中で、例えばビルドだったら、名前とともに数値で管理しています。確か、NS_ENUM
でクロージングが付いていると不要で、付いていない場合は必要です。
今、チャットで聞いた内容を貼ったんですけど、クロージングがObjective-Cでどうかという話ですね。ノンフローズンenum
だと結構出ないんですよ。ノンフローズンenum
というのが、多分UIKit系のenum
だと付いていて、それだと必要ですね。バイナリーレベルで固定ですよ、と明記するために。
これ、プライベートのやつはあるんですかね?プライベートがないObjective-C側でそんなことできるか覚えていないんですけど、要はunknown
のデフォルトってケースを一部隠すことができましたっけ?いや、隠すことはできないですね。だから、ノンフローズンのenum
って何であるんだろう。ビルドするときにはもうわかることで、動的にケースが追加されるわけじゃないですよね。でも動的ではなくて、設計が変わったときだと思っていました。
設計が変わってもビルド時にはわかるじゃないですか。そう、ビルド時にはわかるんですけど、それはライブラリをビルドしたときにライブラリにとってはわかるけれど、それをリンクしているアプリはわからないですね。ビルドが済んでいて新しいライブラリを買って、新しいビルドをもう一回作るんだったらわかるんですけど、ダイナミックリンクって話ですね。
なるほど、UIKit系でOSだけがアップデートされたときって、古いバイナリのアプリが動いていますよね。古いバイナリで動いているときに、ダイナミックリンクされるのは新しいSDKですよね。例えばiOS 15でUIKitの何かを使っているとき、ノンフローズンのものを使っていて、iOS 16になったときにiOS 15 SDKでビルドしたものが本体がiOS 16の場合、新しいSDKにダイナミックリンクされるとしたらどうなるんでしょう?
iOS 15のバイナリでそのまま機械語で動いていると私は思っています。Embeddingしないですね。UIKitがEmbeddingしないということは、プラットフォームOSのほうに入っているUIKitをリンクするはずなので、iOS 15用のアプリがiOS 16の中に入っているUIKitをリンクするわけですね。ケースには@available
を付けられるんですか?ケースには付けられないです。付けられないから、unknown
が来る可能性があります。
@available
は新しいOSを想定している場合に、新しい方だけで有効にするというものなので、仮に付けられたとしてもiOS 15のビルドではiOS 16を想定できないですよね。でもiOS 16が出る前だと、想定できないですからね。
そうですね。iOS 16のUIKitフレームワークが新しいケースを追加するときに、このケースは16でしか動きませんよといったら解決する可能性はありますが、今度はiOS 17を想定できますかという話になります。要は、Xcodeでビルドするとき、例えばアプリがiOS 15のSDKでビルドして、それをiOS 16で動かしたときにリンクされているのはiOS 16のUIKitになるわけです。iOS 15のSDKの時点ではiOS 16がどんなケースを追加しているか分からないため、unknown
を付ける必要があるということです。
基本的にノンフローズンenum
になっているということですね、UIKit系は。そもそもUIKit自体がObjective-Cなので、どうなんでしょうかね。確か、記憶の中ではunknown
を付けた記憶がありますが、ちょっとあやふやです。何かあった気がします。@frozen
を付けない列挙型を定義した時点で拡張される可能性があると捉えられるはずです。この@frozenを付けると、将来的に増やしませんよというAPI作成者側の主張になりますね。それが保証ではなく、あくまで主張だということです。 それは安心して使ってもらうための仕組みです。逆にデフォルトの可能性があるとすれば、その辺りを軽く試してみる限りではうまく実現できなかったんですけどね。例えば、構造体にもフローズンが付いているでしょう。確かStr型とかにも付いていて、内部的なメモリ構造は変わらないよという保証です。APIが確定したからという意味で、これは面白い仕組みですね。
フローズンがないと、例えばInt型のサイズが将来的に変わるかもしれません。でもIntにはフローズンが付いているので、これで128ビットの時どう対応するのかも将来的に考慮されています。32ビット環境で32にするという推奨もありましたね。これでもフローズンで問題ないということです。これは少し難しいテーマです。
他にスイッチ文で面白いところとしては、ケース文をいくつも書けることがあります。例えば、列挙型をたくさん使う場合、String.Encoding
なんかがそうですね。例えば、let encoding = String.Encoding.utf8
のようにして、スイッチ文でエンコーディングを判定します。
let encoding: String.Encoding = .utf8
switch encoding {
case .ascii, .utf8:
// まとめて処理
print("これはASCIIまたはUTF-8です")
case .utf16:
print("これはUTF-16です")
default:
print("他のエンコーディングです")
}
また、列挙型を使ったエラー処理などもスイッチ文で切り分けることができます。例えば、以下のような列挙型を考えてみます。
enum MyError: Error {
case compatibleError
case unexpectedError(String)
}
let error: MyError = .unexpectedError("エラーが発生しました")
switch error {
case .compatibleError:
print("Compatible Error")
case .unexpectedError(let message):
print("Unexpected Error: \\(message)")
}
このように、各ケースに対して処理を定義できます。また、各ケースに付属値を持たせることもできます。
このように、複数のケースを端末切りでまとめることもできるし、特定のエラーが発生した時にメッセージを取得して処理することも可能です。型が違う場合にも、スイッチ文の中で特定の変数に対応することができます。再度確認が必要ですが、このような使い方ができます。
このようにして、エラーや複雑な列挙型を使った処理もスイッチ文でうまく制御できます。例えば、複数のエラーメッセージがある場合や、特定の条件に基づいて異なる処理をしたい時などにも有用です。 コンパチブル、コンパチブル、ここコンパチブル? あ、上に書いてるからか。エラーで、こっちがシンプルエラー。ちょっと変数名がバランスというか語弊を含んでいますけど、とにかく下がエラーになることを紹介したくて。これはね、エニータイプ型とストリングタイプ型がミスマッチだから、こういった1個のケースでのパターンマッチはできないとか、そういったルールがあります。
あと他にね、これもフォールスルーとかで言えることなんですけど、例えばここがコンパチブルエラーだからタイプでしょ。で、こっちがメッセージ。同じ名前であるのも大事なんですけど、これでプリントをメッセージして、フォールスルーできるできないも色々あって。ここでね、例えばフォールスルー。こっちはできるんです、フォールスルーコンパイル通る。でもこっちのフォールスルーはできない。型が違ってきちゃうんで。このフォールスルーの時に、このタイプこっちにマッチする情報が上流で得られていないからエラーになったり、こういうふうにフォールスルーも若干動きが不思議だったりとか。
いろいろ話していたら、全然話し尽くせていないのにそろそろ1時間経ちそうな雰囲気ですね。どうしようかな、スイッチ文は大事な見方ぐらいを紹介してやめてみようかな。もういっぱい、もうちょっと複雑なスイッチ文を紹介するっていうことはしてもいいかもしれないですけどね。
スイッチ文の大事なポイントとして、またやっぱり次回お話ししますね。これはパターンマッチっていうのがとても大事なもので、パターン図、スイッチとプログラミングランゲージにも出てきますけど、あとでね。随分後かな、このスイッチとのパターン、これがとても大事なんですよ。パターンマッチングを理解する上で。
どういうことかっていうとね、このパターンというのはケース文のさっき使うもの、これをパターンと言って、ここのパターンが複数のパターンを組み合わせて作っていくっていうものになって。このパターンは8種類あって、その8種類を自在に組み上げていけると、より表現力の高いパターンを書いていくことができる。
なのでどんなパターンがあるかっていうのがとっても大事。ワイルドカードパターンっていうのは何にでもマッチするよっていうパターンと、アイデンティファイヤーパターンって言って指定した変数とかの値とマッチするパターン。さらにバリューバインディングパターンって言ってマッチした値を変数に取り出すパターン。そしてタプルパターンっていうのがあって、これはタプルの各要素とマッチするパターン。
イナムケースパターンって、これは今日の例で頻繁に出てきた列挙子とマッチさせるパターン。で、オプショナルパターンは前回のこの勉強会でお話ししたアテナを使う構文ね。あのパターン、要はオプショナルの値があるかないかっていうのを判定するパターン。タイプレキャスティングパターンっていうのは、あるインスタンスがこのインスタンスと同等ならばというis
っていう演算子、それと同じパターンマッチ。あとエクスプレッションパターンっていうのは、あらかじめ~=``チルダイコール
っていう演算子で評価式を作っておいて、渡された値とケース文に書いた式とが一致するか、その式として一致するかっていうのを判定するパターン。
この8個、これを一つ一つしっかり整理するとスイッチ文の表現力が相当広がるのでね、スイッチ文いまいちうまく使いこなせていないなって感じる人はぜひこのパターンマッチング、スイッチとのパターンを一回じっくりと読んでみると結構面白いところだったりします。このスイッチ文についてはどれぐらい先で出てくるんだろうな。これによっては特別回作っちゃってもいいのかなと思います。
パターンマッチングをじっくり見る回、多分一回で終わらないんじゃないかなって気がするんですけど、まあいいや、ちょっと企画してみよう。とにかくね、これを読むと非常にスイッチ文が磨かれるので絶対おすすめです。読んでおくといいです。
まあそんな紹介ぐらいでいいかな、とりあえず。スイッチ文、もうちょっと複雑なスイッチ文を次回紹介するかはまた考えさせてもらいます。スイッチ文喋ってるとキリがないんですよね。じゃあ、とりあえずこれぐらいでいいですかね。
次何かだけ見ておこう。スイッチ文の次問題文か、練習問題か、今やっちゃうか。default
を削除してどんなエラーが出るか確認してみましょう。答え言っちゃうか。全てのケースを網羅していませんというエラーかな。そういった感じで全網羅制さえ言えてないね。ちょこっと紹介するかな。まあ迷ってる場合じゃないからね。
はい、とりあえず今回はこれぐらいにしましょう。パターンね、ぜひ興味ある人は見てみてくださいね。お疲れ様でした。ありがとうございました。