Page icon

第37回

2021/10/22
Patterns
Empty
Empty
Empty
Empty
12 more properties
今回も引き続き、前回から寄り道している 
Language Reference
 の「Patterns」の続きから。その中の 
実行時にマッチしない可能性のあるパターン
 に分類される残りの3つについて眺めていこうと思ってます。どうぞよろしくお願いしますね。
—————————————————————————————— 熊谷さんのやさしい Swift 勉強会 #37
00:00 開始 00:35 今回の展望 01:32 オプショナルパターン 04:29 nil 判定のさまざまな書き方 07:35 どの nil 判定が良さそう? 11:22 オプショナルパターンを for-in で使う 13:54 for-in ループに条件を加える 16:25 オプショナルパターンを使うことのメリット 17:26 異なる型同士の等価比較 18:33 共変性を活用した等価比較 19:15 異なるサイズの整数型同士での比較 21:22 BinaryInteger 22:03 異なるサイズの浮動小数点数を比較しようとすると? 22:47 型キャスティングパターン 23:40 is 演算子 25:19 共変 26:27 as 演算子 27:28 動的キャスト 32:13 nil リテラルの条件付きキャスト 33:31 as でオプショナル型を指定するときは注意 36:07 評価式パターン 39:06 評価式パターンで使う ~= 演算子 40:46 評価式パターンの独自定義 43:47 異なる型の値で評価式パターンを実施する 46:27 範囲を用いた評価式パターン 47:30 評価式パターンの独自定義 49:42 変数とパターンマッチング可能 51:43 クロージングと次回の展望 ——————————————————————————————
Transcription & Summarize : 熊谷さんのやさしい Swift 勉強会 #37
今日も引き続きパターンマッチングのお話ですね。振り返る必要はないかなと思いますので、早速始めましょう。今回はSwiftのパターンマッチについて、特に必ずマッチするパターンとランタイム時にマッチしない可能性があるパターンに大きく分けて話していきます。それらのうち、実行時にマッチしないことがある4つのパターンのうちの3つ目から詳しく見ていく流れになります。
これから見ていけば分かると思いますが、今までいろんなパターンを試しながら話していた中で、先取りして話した内容もあると思います。今回はそのおさらいのような感じになる気がします。特にオプショナルパターンについては多く出てきたので、せっかくなのでおさらいをしつつ、前回やった内容を見直しながら進めていきましょう。
では、オプショナルパターンについて始めましょう。オプショナルパターンは、列挙型であるオプショナル型の
Some
の値の付属値にマッチするパターンになります。識別子パターンの後にハテナを添えたものになります。列挙パターンと同じ箇所で利用できます、と書いてあります。これらの説明を聞いて、大体イメージがつきますかね?
パターンマッチングは慣れていないと難しいかもしれません。列挙型であるオプショナル型の特徴はパターンマッチとは少し違う話になりますが、識別子パターンの後にハテナを添えたものは次の例で見ることができます。
例えば、このコードを見てください。
let x = optionalValue
この
x
が識別子パターンにハテナを添えたものと言っています。そして、
let
というキーワードはバリューバインディングパターンに該当します。列挙パターンの中で使っている形を取っていますが、同じ箇所で使用できるという話です。列挙パターンの
sum
と同じ箇所で使えるということになります。
ここで、いろんな書き方を紹介してみましょう。プレイグラウンドで実際に書いてみます。
if case let x? = optionalValue { print(x) } else { print("nil") }
この例では、オプショナル値に対するパターンマッチングのいくつかの方法を試してみましょう。列挙パターンやバリューバインディングパターン、アイデンティファイヤーパターンを組み合わせて使えるようになっています。例えば以下のように書けます。
let optionalValue: Int? = 3 if let x = optionalValue { print(x) } else { print("nil") } switch optionalValue { case .some(let x): print(x) case .none: print("nil") }
上の例では、バリューバインディングパターンやオプショナルパターンを使用しています。
さて、パターンマッチングには様々な書き方がありますが、どれが直感的かとか好みの問題も出てくると思います。今回示した5行目や10行目のようなパターンマッチング、14行目や18行目のような純粋な
if
文のどちらがSwiftらしいかについては、色々な意見があるでしょう。個人的には、目的やユースケースに応じて適切な方法を選んで使うと良いと思います。 個人的には14行目が一番直感的かなと思います。ただ、見たブログの情報や自分の記憶に残っている印象では、18行目がいいという人が多かったように感じます。これは自分の身の周りだけの話かもしれませんが、5行目と10行目を含めたときに、どの書き方が一番馴染むかについて教えてもらえると嬉しいです。
特にないかなと思いますが、やはり14行目かなと感じました。こういった点で、どれが最適かみたいな話は、今日じゃなくても大丈夫ですので、イメージを膨らませてもらえると嬉しいかなと思います。
ちなみに、18行目は SwiftLint でワーニングが出るようです。SwiftLint 的にも好ましくないということですね。値を代入する必要がない場合、つまりバリューバインディングする必要がない状況でバリューバインディングパターンを使うのは、深く考えさせられるかもしれません。
やはり14行目がベストかなと思います。個人的におすすめするのは、1行で書ける場合は10行目や5行目ですが、この辺で良いでしょうか。
次に、オプショナルパターンを
for-in
ループで使うという話になります。前回、自分はぼーっとしていて、「何か珍しい書き方だね」と言っていましたが、実は自分も自然に使っていました。この
for case let
は、この勉強会の第1回目から使っていたので、人は忘れるものだなと感じました。
for case let
を使ってオプショナルパターンを利用することで、
nil
以外の値を処理することができます。これは前回のお話ししたとおりの例です。ここもせっかくなので、コードを紹介しておきましょう。
let array: [Int?] = [1, nil, 2, 3, nil, 5] for case let number? in array { print(number) }
このコードの説明をすると、最初のループでは
nil
なのでブロックを実行せず、次の値は
2
が取れます。それで2が表示され、次は
3
が表示されます。次の
nil
はパターンマッチしないので飛ばされ、5が表示されるという流れです。
さらに、
switch
分を用いたパターンマッチングも紹介しましょう。このように書くと、表現力が高くなります。
let array: [Int?] = [1, nil, 2, 3, nil, 5] for case let number? in array where number > 3 { print(number) }
このコードは、
nil
以外かつ
3
より大きい要素だけが抽出されます。こうすることで、特定の条件に合った要素だけを処理できます。
また、
for number in array
と書く場合も、条件を追加することができますが、オプショナルのアンラップが必要になります。
for number in array { if let num = number, num > 3 { print(num) } }
パターンマッチングを使った場合には、オプショナルが解けた
Int
型で受け取れるため、そのまま比較ができます。しかし、パターンを使わなかった場合は
Int
のオプショナル型で受け取るため、比較する際にアンラップが必要です。
この違いから、パターンマッチングを使うことが有意義だということがわかります。これは重要なポイントだと思います。 さて、オプショナルな値の比較について紹介しておきましょう。余談になりますが、Swiftでは原則として、同じ型同士で演算を行うというルールがあります。しかし、最初はこのコンセプトを厳格に守っていましたが、徐々にバージョンアップを重ねることで、必要のない型の違いが暗黙的に調整されるようになってきました。標準ライブラリの中で、そのような演算子が定義されています。
例えば、
Int
のオプショナル型の変数
A
があるとします。この
A
10
と一致しますか、という比較は、オプショナルを解かなくても行うことができます。等価比較ですね。これはジェネリクスを使っているのではなく、両辺がオプショナルだった場合に配慮してくれる演算子が用意されているからです。具体的には、
A
を渡すときに
10
の方をオプショナルに包んでくれて、最終的にオプショナルな
Int
型同士で比較を行います。
もう一つ面白い例として、
Int
型の変数
B
C
を使ってみましょう。例えば、
Int16
型と
Int8
型の変数 B と C がそれぞれ10だとします。この2つの間の大小比較も可能です。サイズが異なっていても、バイナリのサイズに関係なく大小比較が成立する演算子が用意されているのです。このように、以前は
Int16
型に変換してから比較しなければならなかったものが、現在では簡単に比較できるようになっています。同じように、片方が符号なしでも比較することができます。これはなかなか便利な機能ですね。
使う機会は少ないかもしれませんが、バイト列と整数型を比較するといった場合など、意外と使い道が多いかもしれません。例えば、
B
C
の比較ですが、等価比較も問題なく行えます。このような仕様はなかなか面白いですね。Swiftの標準ライブラリで提供されている整数型はすべてBinaryIntegerに準拠しています。
次に、タイプキャスティングパターンについて話を進めましょう。タイプキャスティングパターンは、パターンマッチの文脈で
is
演算子を使って指定した型と同じか、そのサブクラスである場合にマッチするパターンです。
is
演算子を使った場合、結果を気にしません。結果は捨ててしまいます。
一方、
as
を使ったパターンですと、同じようにマッチした値を使用することができます。例えば、以下のようなコードを考えてみましょう。
switch a { case is BinaryInteger: print("Binary Integer") default: print("Not a Binary Integer") }
この場合、
Binary Integer
と表示されます。もし具体的な型を指定しないとエラーとなる場合があります。例えば、
case is Int
とすると、
a
Int
のオプショナル型の場合に
1
と表示されます。このようにパターンマッチングを行うことができます。
ただし、
Double
Float
については異なる仕様です。メッセージには「型が違う」と出ることがあり、バイナリ整数型のみが対象となります。
Double
Float
は精度が異なるため、比較が難しくなる場合もあります。
このように、Swiftの型キャスティングパターンは非常に強力であり、使い方によっては非常に便利な機能となります。後でプログラムコードやドキュメントに追記することもできますので、引き続き学んでいきましょう。 例えば、互換性のある型の場合には、それがマッチすると判定されます。もちろん、ダブルの場合には互換性がないため、ゼロの方が適用されるといった感じです。同様に「
as
」という演算子もありますが、こちらは結果も考慮するパターンで、ワイルドカードパターンで捨てることもできます。しかし、普通はそんなことせずに「
is
」を使います。
普通はバリューバインディングパターンとアイデンティファイヤーパターンを使って表現します。そのときに
x
はダブル型として解釈されます。右のユーティリティエリアのところで、例えば「
as Double
」とすると、アイデンティファイヤーはダブル型として認識されます。
次に、少し面白い動きについてお話しします。「
is
」演算子と「
as
」演算子について、まだ完全に把握しきれていないのですが、少し不思議な動きをすることがあります。例えば、
A
が今
Int
のオプショナル型だとします。これが
Double
のオプショナル型ですかと言ったら、違いますよね。ただ、ゼロが適用されます。これを
nil
にしたとき、どう動くか見てみましょう。予想通り、
nil
のときには、
Int
のオプショナル型にも
Double
のオプショナル型にも入るため、互換性があるとみなされるようです。
このように、少し微妙な動きをするのです。問題を引き起こしかねないケースも考えられ、特にパーサーなどを作る場合には、文字列型の
nil
と整数型の
nil
が区別できなくなる恐れがあります。これにより、想定と異なる動作をしてしまう可能性があります。
is
」演算子と「
as
」演算子はダイナミックキャストであることが重要です。ダイナミックキャストの中でも、特に
nil
に関しては緩い判定がされます。
具体的には以下のような動きになります:
var a: Int? = nil var x = a as? Double
このとき、
x
Double?
型になります。しかし、次のようにした場合、
x = nil as Double
これはコンパイルエラーとなります。なぜなら、
nil
そのものを型キャストする場合には、コンディショナルキャストが必要だからです。
興味深いのは、型情報を見失う場合があり、例えば
type(of: x)
がどうなるか試してみると良いでしょう。型キャストをしっかり理解しておかないと混乱を招く可能性があります。
is
演算子や
as
演算子を使う場合、特にオプショナル型の取り扱いに関しては注意が必要です。オプショナルでない場合には、
nil
が入っていたとしても互換性がなくなるので安心ですが、やはり意識しておくべきポイントです。
このように、Swiftでは型キャスティングパターンがいくつか用意されており、それらを理解し使いこなすことが重要です。 繰り返しになりますが、
as
パターンはバリューバインディングで受けたときに
as
で指定した型になってくれるというのはとても嬉しいところですね。
次は最後のパターンかな、エクスプレッションパターンです。これがまた面白いパターンマッチングで、多くの人が自然に使っているパターンマッチングになります。先に紹介しちゃいましょうかね。エクスプレッションパターンは評価式の合致を表すパターンです。ちょっと表現がややこしいですが、評価式を使ってマッチングを判定するパターンになります。スイッチのケースラベルでのみ使用可能だったっけ?いや、そんなことない気がしますが、とりあえずそう書いてあったので、スイッチのケースラベルでのみ使用可能で、指定された式と値を
==
演算子を使って比較するというパターンになります。
なので、このパターンを使うためには
==
演算子が定義されている必要があります。標準ライブラリにも基本的に同じ型同士で値が一致するかっていう感じのパターンマッチ演算が定義されているので、すぐに使い始めることができます。これも実際にいじってみるとわかりやすいので、プレイグラウンドでいじってみますね。やることはとても簡単で、主に整数型でパターンマッチングするときによく使っているものになります。
たとえば
value
Int
型で
10
とか、何の値でもいいですが、これがあったときに
switch value { case 1: // 何をする case 10: // 何をする default: // 何をする }
という感じです。他にもたとえば、
3
から
5
までだったら何をするというコードで使われているのがエクスプレッションパターンになります。今、
value
10
なので該当するケースの処理が行われます。これが
5
だったら別のケースが実行されるなど、こういうふうな動きを見せています。
たとえば、
if value
~(3...5)~ どっちが先だったっけ?確かパターンが先ですね。
if (3...5) ~= value { print("Value is in the range 3 to 5") }
みたいに書きます。これで動くことが確認できます。スイッチ文のケースだけでなく、
if
文の条件でも普通に使えます。
こんな感じで、Swiftのプログラミング言語は時折、注意しないといけない部分もありますが、ケース文であればエクスプレッションパターンが使えます。
エクスプレッションパターンは自分で定義することもできます。たとえば、
func ~= (pattern: ClosedRange<Int>, value: Int) -> Bool { return pattern.contains(value) }
という感じで、範囲演算が定義されているので、これを使って独自の型に対するパターンマッチングを実装してあげることが可能になります。
たとえば、これを
Int
型と
Int8
型で考えた場合、次のように定義できます:
func ~= <T: BinaryInteger, U: BinaryInteger>(pattern: T, value: U) -> Bool { return pattern == value }
このように定義してあげると、型の違いによるパターンマッチも考慮できます。このようにエクスプレッションパターンをオーバーロードすることで、様々なケースに対応できるようになります。 とりあえずコメントアウトで誤魔化してみようかなとすると、こうやって
Int
型と
Int8
型でパターンマッチもできるようになります。ただ、ちょっと問題があるコードになってしまっていますが、このように独自のスタイルでパターンマッチングすることも可能です。
次に行きますね。範囲を用いた評価式パターン、これは先ほどやったやつですね。クローズドレンジを使った例になります。この2つ目のパターンですね、これをタプルパターンを使って一気にそれぞれの要素をレンジで比較する、みたいなことをしています。ちょっと複雑なコードに見えるかもしれませんが、タプルパターンを見なかったことにすれば、さっき紹介したレンジでパターンマッチした例と全く一緒です。これは大丈夫ですね。
そしてこれも先ほどお話しした評価式パターンの独自定義が可能という話です。これによって独自の型とか何でも応用を効かせたパターンマッチングが可能です。例の仕方としては、ケースの後に書くパターン、これもタプルパターンを使っています。タプルパターンでちょっと複雑な感じがするかもしれませんが、1つの要素に対してのお話をしていますね。これはパターンとして文字列を受け取り、値として
Int
型の
x
y
座標のようなポイントを受け取り、
x
に対して独自に定義したエクスプレッションパターンの演算子で判定し、さらに
y
座標に対しても同じようにエクスプレッションパターンで比較します。
ケースの後がパターンで、スイッチで比較するのが値ですね。それをケースの後のパターンに対して変数を割り当て、その後で、それと合わせる値を受け取る演算子を書きます。この流れでチルダイコール演算子(
~=
)を定義すれば、期待通りの演算子が作れます。
こんな感じですね、エクスプレッションパターン。ついでにもう一つ、Swift の特徴であるスイッチのお話ですが、Swift のスイッチはケースに対して変数も使えます。変数や定数ですね。これがすごく当たり前に感じるかもしれませんが、C言語などではここに変数を書けず、確か定数リテラルをバシッと書いてあげないといけなかったと思います。
何気なく、Swift ではケース文を変数で書けるということは、その変数の値によって動的にランタイムでマッチするケースブロックを変えられるということです。これはなかなか面白いところで、何かしらの環境変数からフラグを取得し、そのフラグによってスイッチ文を切り替えるなど、いろいろなことができます。乱数でスイッチを切り替えるというのはやらないと思いますが、とにかく変数でケースを当てる価値が当時はなかったので、これができるというのは結構大きいと思いました。このあたりに慣れてくると、プログラム、特にスイッチのコードの書き方も変わってきそうだなという印象を持ちました。
こんな感じで全部のパターンを見終えることができました。ちょうどいい時間ですね。これで今回のパターンマッチのお話は終わりですが、何かありますかね。また次回は Swift Programming Language のスイッチツアーに戻っていこうかなと思います。何にしても8つのパターンマッチングの種類があり、今表示中の4つともう4つを色々な場面で組み合わせて、さらにタプルパターンを使って複数まとめていくことで、複雑な条件処理をケース文でサクッと書くことができるのが Swift のパターンマッチのとても面白いところです。ぜひこの8つのパターンをマスターして、自由自在に組み合わせて、きれいなコードを書いていただけたら嬉しいです。
今日は1時間お疲れ様でした。ありがとうございました。