Declare variables in ‘case’ labels with multiple patterns
01 何が問題だったのか
Swift の switch 文では、ひとつの case にカンマ区切りで複数のパターンを並べることができます。しかし Swift 2 までは、そのパターンの中で変数を束縛(let x など)したい場合には、パターンをひとつしか書けないという制限がありました。
たとえば、次のような enum を考えます。
enum MyEnum {
case case1(Int, Float)
case case2(Float, Int)
}
case1 と case2 のどちらでも、Int 側の値を x として取り出して同じ処理をしたい、という場面はよくあります。素直に書くなら次のようになってほしいところです。
switch value {
case let .case1(x, 2), let .case2(2, x):
print(x)
case .case1, .case2:
break
}
ところが Swift 2 ではこれがコンパイルエラーになり、次のメッセージが出ます。
'case' labels with multiple patterns cannot declare variables.
そのため、同じ処理であっても case を分けて書くか、あるいはインラインの関数やクロージャに切り出して重複を避けるしかなく、次のような冗長で見通しの悪いコードになりがちでした。
switch value {
case let .case1(x, 2):
print(x)
case let .case2(2, x):
print(x)
case .case1, .case2:
break
}
if case でも事情は同じで、複数のパターンをまとめて扱いながら変数を束縛する手段がありませんでした。結果として、パターンの構造は違っても取り出したい値の「意味」は同じ、というケースで、本質的でない繰り返しが生まれてしまっていました。
02 どのように解決されるのか
複数パターンを持つ case ラベルでも、すべてのパターンが同じ名前・同じ型の変数を宣言している 限り、変数束縛を認めるようになりました。これにより、パターンごとに case を分けずに、ひとつの case のもとで同じ変数 x を受け取って処理できます。
enum MyEnum {
case case1(Int, Float)
case case2(Float, Int)
}
switch value {
case let .case1(x, 2), let .case2(2, x):
// case1 でも case2 でも、Int の位置の値が x として取り出される
print(x)
case .case1, .case2:
break
}
束縛の書き方は柔軟で、パターンごとに位置が違っても、同じ名前の変数に揃えれば問題ありません。複数の変数を束縛することもできます。
// case1 の1番目、case2 の2番目をそれぞれ x として束縛
case let .case1(x, _), let .case2(_, x):
// x と y の対応位置がパターン間で入れ替わっていてもよい
case let .case1(y, x), let .case2(x, y):
// パターンごとに let を書く形も、位置ごとに let を書く形も使える
case .case1(let x, _), .case2(_, let x):
同じ文法は if case や guard case などでも使えます。
if case let .case1(x, _) = value, case let .case2(_, x) = value {
// ...
}
守るべきルール
この機能が成立する条件はシンプルで、次の2点だけです。
- すべてのパターンが、まったく同じ名前の変数 を宣言していること。
- 同じ名前の変数は、どのパターンでも同じ型 になること。
この条件を満たさない場合、たとえば片方のパターンでしか宣言されていない変数があったり、パターンによって型が食い違ったりする場合は、従来どおりコンパイルエラーになります。これは、どのパターンにマッチしても case の本体で使える変数が一意に決まるようにするための制約です。
使いどころ
この変更によって、「パターンの構造は違うが取り出したい値は同じ」というケースを、余計な関数やクロージャに切り出さずに表現できます。enum の複数ケースをまとめて同じ処理にしたいときに、コードの重複を素直に減らせるのが主な利点です。