2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Swift] caseの使い方を覚えて一段上のプログラミングを

Last updated at Posted at 2025-02-05

なにこれ?

あんまり知られてないcaseの使い方

基本

caseといえばswitch
switchの簡単な例を見てみましょう。

func isA(_ c: Charactor) -> Bool {
  switch c {
    case "a", "A": true
    default: false
  }
}

普通ですね!
これは変数cに入れられた文字と文字"a"もしくは文字"A"同じであればtrueになります。

準備

caseといえばenum
ということで以降で使用するenumを定義します。

indirect enum Box {
  case value(Int)
  case box(Box)
  case empty
}

enumの要素宣言のcaseは今回の話とは無関係です。

if-case

switch-caseに次いでよく使われるのがif-caseです。
switch-casecaseとは違い後ろに = 変数が続きます。

func isEmpty(_ box: Box) -> Bool {
  if case .empty = box {
    return true
  }
  return false
}

guard-case

あまり見たことがないですがif-caseと同様にgurad-caseも可能です。

func isValue(_ box: Box) -> Bool {
  guard case .value = box else {
    return false
  }
  return true
}

for-case

ごくまれにみられますが、for-caseも可能です。
switch-case同様後ろには何も続きません。
Sequenceの要素のうち条件に合致するときのみコードブロックが実行されます。

func printValues(_ boxes: [Box])  {
  for case .value(let i) in boxes {
    print(i)
  }
}

caseでは.valueの値にある部分にlet iと記述することで、Associated Valueを変数iに代入することができます。
これはcaseが使える場面では常に有効です。

while-case

僕も初めて見ました。while-caseもいけます。

func deepSearch(_ box: Box) -> Int? {
  var box = box
  
  while case .box(let inner) = box {
    box = inner
  }
  
  guard case .value(let value) = box else {
    return nil
  }
  return value
}


let box = Box.box(Box.box(Box.value(1)))

print(deepSearch(box))
/// prints Optional(1)

repeat-case ?

出来ません。
Swiftの制御フローで唯一repeatだけは条件にcaseが使えません。
残念。

またcaseは式ではないため式が使える場所どこでも使えるわけではありません。
例えばなんか使えそうな気がする三項演算子でも使用不能です。

Equatableとcase

当たり前ではありますがEquatableな型はすべてcaseで使えます。

let v: Int = 0
switch v {
  case 0: print("0")
  case 1: print("1")
  default: print("other")
}

範囲指定などはどうなっているのか

範囲を指定しているcaseはどのようになっているのか

let v: Int = 0
switch v {
  case ..<0: print("Minus")
  case 0: print("0")
  default: print("Plus")
}

これは~=という関数(オペレータ)を実装することで実現できます。
上の場合は

extension RangeExpression {
  public static func ~= (pattern: Self, value: Bound) -> Bool {
    pattern.contains(value)
  }
}

が実装されているため機能します。

第一引数(左辺)がパターン、第二引数(右辺)が指定される値になります。
この関数(オペレータ)はパターンマッチ演算子と呼ばれています。

実はcaseはすべてパターンマッチ演算子~=で検査が行われています。
先に書いたEquatable==を使っているのではなく、

public func ~= <T: Equatable>(a: T, b: T) -> Bool {
  return a == b
}

が標準ライブラリに入っていることでcaseに使えるようになっています。

カスタムタイプとパターンマッチ

ここまでに書いた通り、~=を書けばいろいろなパターンマッチが可能となります。
例えば正規表現でパターンマッチなども可能になります。

struct RE {
  private var re: Regex<AnyRegexOutput>
  
  init(_ reg: String) {
    do {
      re = try Regex<AnyRegexOutput>(reg)
    }
    catch {
      re = try! Regex<AnyRegexOutput>("")
    }
  }
  
  static func ~= (_ lhs: Self, _ rhs: String) -> Bool {
    (try? lhs.re.firstMatch(in: rhs)) != nil
  }
}

を導入すれば、

func hoge(_ string: String) -> String {
  switch string {
    case RE("a"): "a"
    case RE("ho*ge"): "ho*ge"
    default: "other"
  }
}

print(hoge("abc"))
/// prints a

print(hoge("hooooooooooooooooooge"))
/// prints ho*ge

print(hoge("piyo"))
/// prints other

のようにswitch-caseで正規表現が利用できるようになります。
(ここで提示したREは最低限動くようにしただけのものです。実際に利用するのはやめましょう。)

enumのAssociated Valueとパターンマッチ

Box.valueが特定の範囲の値の時などに何かしたいというときに使うのがこちらです。

func isValuePlus(_ box: Box) -> Bool {
  switch box {
    case .value(1...): true
    default: false
  }
}

見た目としてはかなり不思議です。
これは少しややこしくて、box.valueであればカッコ内の値とAssociated Valueをパターンマッチ演算子で検査する、という動作になります。

これは以下と同じです。

func isValuePlus(_ box: Box) -> Bool {
  switch box {
    case .value(let value) where 1... ~= value: true
    default: false
  }
}

// あるいは
func isValuePlus(_ box: Box) -> Bool {
  if case .value(let value), case 1... = value {
    return true
  }
  return false
}

オプショナルとパターンマッチ

オプショナルを検査するcaseでは糖衣構文を利用することができます。

func isZero(_ i: Int?) -> Bool {
  if case .some(0) = i {
    return true
  }
  return false
}

これを糖衣構文を利用して書き換えるとこうなります。

func isZero(_ i: Int?) -> Bool {
  if case 0? = i {
    return true
  }
  return false
}

次に、あるOptional<Int>な値を3つの場合、nil, nilでなく0未満, nilでなく0に分けることを考えます。
これも先ほどの「enumのAssociated Valueとパターンマッチ」と同じです。

func sign(_ value: Int?) -> String {
  switch value {
    case .some(..<0): "Minus"
    case .some(0...): "0 or Plus"
    default: "Nil"
  }
}

これは以下のように書くこともできます。

func sign(_ value: Int?) -> String {
  switch value {
    case (..<0)?: "Minus"
    case (0...)?: "0 or Plus"
    default: "Nil"
  }
}

タプルとcase

タプルをswitch-caseで場合分けすることがありますが、こちらにも少し変わった方法があるので見てみましょう。

みんな大好きFizzBuzzです。

func fizzBuzz1(_ i: Int) -> String {
  guard i > 0 else { fatalError() }

  switch (i.isMultiple(of: 3), i.isMultiple(of: 5)) {
    case (true, true): return "FizzBuzz"
    case (true, _): return "Fizz"
    case (_, true): return "Buzz"
    default: return "\(i)"
  }
}

こちらの2番目3番目のcaseに含まれる_はマッチしてもしなくてもよい、つまりどんな値でもtrueになるパターンです。

まとめ

Swiftのcaseはとても強力です。
うまく使うことで複雑なコードが簡素になることもあります。
条件分岐を考えるときはcaseのことを少しだけ思い出してあげてください。

2
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?