🕊

[Swift] enum の switch-case 文で where を使うと Xcode の補完機能の恩恵を得られない件

2022/10/22に公開

伝えたいこと

  • enum の switch-case 文で where を使うと Xcode による条件漏れの補完機能がうまく働いてくれない。なので、あんまり、 where は使わない方がよい
  • where の代わりに switch-case 文にさらに追加で条件を付与したい場合は以下の 3 通りが考えられる
    • (1) タプルを用いる
      • Xcode の補完機能をフルで活かせる
      • (enum, enum) など条件が多い場合は、場合分けが見づらくなる
    • (2) where 条件を内部に持ってくる
      • 直感的である
      • ネストが深くなってしまう
    • (3) やっぱり where で条件を記述する。ただし、その場合、以下のことに気をつける
      • Xcode の補完機能を使うと、default を記述しないかぎり条件が重複してしまう
      • switch-case 文で条件が重複している場合は、最初にあてはまった条件で処理されて break する
      • 重複を防ぐために default 条件を書く場合は、条件の漏れを防ぐために assertionFailurefatalError で気付けるような仕組みを作ったほうがよさそう
      • (上記のようなデメリットがあるので、タプルで書いたり、where 条件を内部に持ってくることができないか、もう一度検討する)

もろもろの検証

以下のような enum を定義して、swtich-case文を記述してみます。

enum Menu: String {
    case rice
    case salad
}

var menu: Menu = .rice

switch menu {
case .rice:
    print("menu: \(menu.rawValue)")
    
case .salad:
    print("menu: \(menu.rawValue)")
}

where で条件を追加してみる

ここで rice の場合に、さらに以下のように where によって外から条件を絞ります。

var menu: Menu = .rice
var isChild: Bool = true

switch menu { // ❌ Switch must be exhaustive
case .rice where isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")

case .rice where !isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
case .salad:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
}

↑ 一見、上記の記述で全パターン網羅しているように見えますが、これではコンパイルエラーになります。

Xcode の的外れな補完

Swift に自動補完してもらうと、なんと以下のように case .rice: が足りないと補完されます。

どうやら、where の条件は、switch-case 文の管轄外みたいです。

switch menu {
case .rice where isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")

case .rice where !isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
case .salad:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
case .rice: // ← 明らかに通過することのないプログラムが補完される
    <#code#>
}

Xcode に言われた通り、case .rice: を追加して、問題になるのは、明らかに通過することのないプログラムが補完されてしまうことです。

Xcode の的外れな補完の対策

そのため、そこの処理に break を記述すればよさそうですが、『rice のケースでは処理をしない』というわけではないため、混乱の元になってしまいます。

switch menu {
case .rice where isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")

case .rice where !isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
case .salad:
    print("menu: \(menu.rawValue), isChild: \(isChild)")

case .rice: // ← 明らかに通過することのないプログラム
    break // ← break するのは問題ないが rice のケースがないわけでは、混乱する
}

上記のように重複した条件を書くと混乱してしまうので、意味合い的には default: を定義してあげたほうがよさそうです。

そして、以下のように絶対に通過することのない default: の場合は fatalError などで、条件の考慮を漏れを気付けるような仕組みを作った方がいいと思います。

switch menu {
case .rice where isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")

case .rice where !isChild:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
case .salad:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
default:
    fatalError("unexpected") // ← fatalError などしておくとよさそう
}

正直、この対応はあんまりやりたくないですよね。
なので、以下に where を回避する方法を紹介します。

where を回避する方法(その1)

もし、switch-case 文の Xcode の補完機能の恩恵を得たいなら、以下のようにタプルを使って書くのが良いかもしれません。

switch (menu, isChild) {
case (.rice, true):
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
case (.rice, false):
    print("menu: \(menu.rawValue), isChild: \(isChild)")
    
case (.salad, _):
    print("menu: \(menu.rawValue), isChild: \(isChild)")
}

where を回避する方法(その2)

そもそも、Xcode の補完機能の恩恵を得られなくても良いのであれば、以下のようにも書けます。

switch menu  {
case .rice:
    if isChild {
        print("menu: \(menu.rawValue), isChild: \(isChild)")
    } else {
        print("menu: \(menu.rawValue), isChild: \(isChild)")
    }

case .salad:
    print("menu: \(menu.rawValue), isChild: \(isChild)")
}

ただ、where をつけることで、中のネストが少なくなって、条件が見やすくなる利点もあるので、case-by-case ですね。

以上になります。

GitHubで編集を提案

Discussion