Swift의 조건절 패턴 매칭 총정리

Swift의 조건절은 사실 패턴 매칭을 판단한다

if 문은  표현식을 평가한 결과가 true 인지 false 인지를 판단하여 분기를 처리한다는 것이 분기문에 대한 가장 간단한 설명인데, 사실 Swift의 if 문은 패턴매칭에 의한 평가를 우선지원하고 있기 때문에, 실제로는 적용할 수 있는 문법의 종류가 매우 다양하다.

여러 패턴으로 표현할 수 있는 조건이 다양하면 중첩되는 if 문을 그만큼 줄일 수 있어서 깔끔하고 명료한 코드를 작성하는데 도움이 된다. 특히 Swift는 ifguard문 외에도 switch, while 및 심지어 for 문에 이르기까지 조건절을 붙일 수 있다.

조건절에 사용되는 패턴 매칭에 대해 모든 것을 알아보도록 하자.

조건절의 문법

조건절의 문법은 다음 중 하나의 경우에 해당한다.

  1. Bool 값으로 평가될 수 있는 표현식
  2. 조건리스트
  3. Bool 표현식, 조건리스트 (둘을 컴마로 연결해서 쓸 수 있다.)
  4. 호환성체크, Bool 표현식 (컴마로 연결했다.)

이 때 조건리스트는 하나 이상의 조건 패턴이 컴마로 연결된 것을 말한다. 각각의 조건 패턴은 다음 중 하나가 될 수 있다.

  1. 호환성체크 패턴
  2. 옵셔널 바인딩 패턴
  3. case 조건1 패턴

옵셔널 바인딩은 흔히 if let ... 문으로 알려져 있는 패턴이다. 옵셔널 값이  nil 이 아닌 경우 wrapping 되어 있는 값을 임시 변수에 바인딩하고 조건을 만족했다고 판정한다.

if let x = someOptional { // someOptional != nil 인 경우에만 조건을 만족한다.
    print(x)
}

중요한 것은 case-조건 패턴인데, (사실 옵셔널 바인딩은 case 조건 패턴 중 특수한 경우 하나를 syntatic sugar로 꾸며놓은 것이다.) 이 패턴은 다음과 같은 문법으로 생겼다.

case :패턴: = :표현식: [where :표현식:]

여기서 = 표현식의 패턴을 initializer-pattern이라 한다. 즉 case 조건 패턴은 case로 시작하며 좌변 = 우변의 모양을 가지며, 그 뒤로 별도의 where 가드가 존재하여 추가적인 필터링을 할 수 있다.

이 때 좌변이 되는 패턴은 기본패턴인데, 여기에는 7가지 종류가 있다.

  1. identifier-pattern
  2. value-binding-pattern
  3. tuple-pattern
  4. enum-case-pattern
  5. optional-pattern
  6. type-casting-pattern
  7. expression-pattern

이제 각각의 패턴을 설명하고, 이에 대한 예시를 들어보자.

1) identifier-pattern

이패턴은 그냥 변수/상수이름을 그냥 쓰는 것을 의미한다. 실제 매칭은 좌변을 평가한 값을 우변에 매칭2하는 것이다.

let a = 42
var b = 42

if case a = b {
    print("a is equal to b.")
}
// > a is equal to b.

2) value-binding-pattern

이 패턴은 옵셔널 바인딩과는 다르게, 우변의 값을 좌편의 이름에 바인딩한다. 이 때 우변은 옵셔널이 아니어도 상관없다.

if case let x = b where x == a {
    //  ^^^^^^^^^ 단순 값의 바인딩 패턴은 항상 매칭하므로 where 가드를 적용했다.
    print("a is equal to b.")
}
// > a is equal to b.

3) tuple-pattern

튜플 패턴은 튜플 형태의 표시를 의미한다. 다음 예제는 두번째 요소가 0인 모든 튜플에 매칭하는 패턴을 검사한다.

let a = (4, 0)
if case (_, 0) = a {
    print("a is on the X axis.")
}

switchcase 절에서 보듯이, 튜플 패턴은 값 바인딩 패턴과 다음과 같은 식으로 결합하기도 한다.

if case let (x , y) = a where x == y {
    print("a is on the line f(x) = x.")
}

4) enum-case 패턴

enum-case 패턴은 우변의 값이 enum 타입의 특정 case에 해당하는지를 보는 것이다.

enum Either {
    case Left(Int, Int)
    case Right(Double, Double)
}

var a = Either.Left(13, 169)
if case .Left = a {
    print("a is .Left type")
}

Either 타입은 튜플로 된 연관값을 갖고 있기 때문에 다시 튜플 패턴과 결합할 수 있다.

if case .Left(let x, _) = a where x % 2 == 0 {
    print("a is .Left type and its first element is even number.")
}

5) optional 패턴

옵셔널 패턴은 식별자뒤에 ?가 붙은 식이다. 이는 nil이 아닌 옵셔널 값에 매칭한다. 가장 간단한 패턴은 다음과 같다.

let p: Int? = 10
let q: Int = 10
if case q? = p {
    print("p is not nil, and its wrapped value is equal to q.")
}
옵셔널 패턴과 값 바인딩 패턴을 결합하기

이 패턴이 값 바인딩 패턴과 결합하면 옵셔널 바인딩의 원형이된다.

if case let x? = p {
    ...
}

// 위 패턴은 아래의 옵셔널 바인딩 패턴과 동일하게 동작한다.

if let x = p {
    ...
}
옵셔널 패턴과 튜플 패턴이 결합될 때

튜플 패턴과 결합하는 경우에 한가지 고려할 점은 (Int?, Int?) 타입과 (Int, Int)? 타입은 다르다는 것이다. 전자는 두 개의 정수형 옵셔널의 짝으로 된 튜플이고 튜플 자체는 옵셔널이 아니다. 후자의 경우에는 반대로 튜플 자체가 옵셔널이며, 각각의 원소는 옵셔널이 아니다.

let k: (Int?, Int?) = (4, 8)
// k -> (Optional(4), Optional(8))

if case let (x?, y?) = k where x * 2 == y {
    print("k.0 * 2 == k.1")
}

let l: (Int, Int)? = (4, 8)
// l -> Optional((4, 8))

if case let (x, y)? = l where x * 2 == y {
    print("l is not nil and is pair of \(x) and \(y)")
}

6) type-casting

타입 캐스팅 패턴은 is 패턴as 패턴 두 가지가 있다.

  1. is-pattern: is Type
  2. as-pattern: pattern as Type

is 패턴은 간단히 좌면을 is 타입, 우변은 검사할 값으로 둬서 우변이 해당 타입에 매치하는지 본다. as 패턴은 as 왼쪽의 패턴에 먼저 매치한 후, 매치가 성공하면 해당 패턴을 as 오른쪽의 타입으로 캐스팅해본다. 캐스팅에 성공하면 통과, 성공하지 못하면 실패로 간주한다.

var c: Any = 23
if case is Int = c {
    print("c is Integer")
}

c = 23.4
if case let x as Double = c {
    print("c is Double")
}

c = "23.4"

if case let x as Double = c  { 
    // 값 바인딩 패턴엔 매치하지만 이후 as 패턴에 매치 실패한다.
    print("this is never printed")
}

7) expression 패턴

표현식 패턴은 가장 단순하게는 같은 값인지 보는 것이다. 실제로는 Swift 표준 라이브러리에 정의된 ~= 연산자에 의해서 검증한 결과를 매치한다.

예를 들어 Range 타입은 ~= 연산자를 통해서 정수가 해당 범위 내에 있는지를 판별할 수 있다.

let t = 8
if case (1...10) = t {
    print("t is between 1 and 10")
}

표현식 패턴은 ~= 연산자를 적용한 결과라고 했으니 이는 아래와 같이 쓸 수 있다.

if (1...10) ~= t {
    print("t is between 1 and 10")
}

따라서 ~= 연산자를 적절히 오버로딩해주면 임의의 패턴 매칭 방법을 쓸 수 있다는 뜻이다. 만약 정수와 그 수를 문자열로 표현한 값을 서로 매칭하려면 아래와 같이 연산자를 오버로딩하면 된다.

func ~= (pattern: String, value: Int) -> Bool {
    return pattern == "\(value)"
}

let u = 128
if case "128" = u {
    print("It is 128.")
}

정리

지금까지 case 조건패턴을 구성하는 기본 패턴들의 사용방법을 살펴보았다. 흥미로운 점은 조건절은 실제로는 case 조건패턴의 리스트로 구성될 수 있다는 점인데, 이는 하나의 if 문에서 여러개의 패턴을 한꺼번에 매칭해볼 수 있다는 점이다. 이 때 조건이 리스트로 구성되면 모든 조건 패턴이 매칭되어야 한다. switch 문의 case 절이 조건절 중 하나와 매치되면 해당 블럭을 실행하는 것과는 대조적이다.

아래 예시는 하나의 if 문에서 변수 i가 짝수이며, z.Left 타입인데 연관 값이 1~10 사이의 범위에서 제곱의 관계를 맺고 있는지 확인하는 것을 보여준다.

enum Either {
    case Left(Int, Int)
    case Right(Double, Double)
}

var z = Either.Left(3, 9)
let i = 34

if i % 2 == 0, case .Left(1...10, 1...10) = z, case let .Left(x, y) = z where x * x == y {
    print("Ok.")
}

또한 이러한 조건절 패턴매칭의 문법은 guard, for...inwhile 문에서도 사용가능하다.

Swift의 조건절이 단순한 값 판단이 아닌 패턴매칭으로 동작한다는 점은 코드를 더욱 간결하고 가독성을 높일 수 있는 장치로 활용가능하니 눈여겨 봐야 할 부분이다.

부록

다음 코드는 ~= 연산자를 오버로딩하여, 문자열이 올바른 핸드폰 번호인지를 검사하는 코드를 if 문의 패턴 매칭으로 구현하는 예이다.

func ~= (pattern: String, value: String) -> Bool {
    guard let regex = try? NSRegularExpression(pattern: pattern, options:[])
        else { return false }
    return regex.numberOfMatchesInString(value, options:[],
        range:NSMakeRange(0, value.characters.count)) > 0
}

let s = "011-123-3456"

if case "(?:010[-\\.\\s]?\\d{4}[-\\.\\s]?\\d{4})|(?:011[-\\.\\s]?\\d{3,4}[-\\.\\s]?\\d{4})" = s {
    print("it is phone number.")
}

  1. 여기서는 case라는 키워드를 쓰기 때문인데, 이는 switch문의 case와는 다르다. switch 문에서는 case 절이 된다. 
  2. 이 때는 해당 이름의 값에 대해서 표현식 패턴에 대한 매칭을 수행한다. 
  • Wanbok

    좋은 글 잘 봤습니다.
    중간에 Type-casting 예제에 오타가 있는 것 같아요. if case let x as Double이 아니고 if case let x = c as Double 인 것 같습니다.

    • 지적 감사합니다. `if case let x as Double = c` 가 맞는 표현입니다. as 패턴 자체가 좌변에 오게 되기 때문입니다. `as Double`을 뒤로 보내면 as 로 캐스팅이 불가능한 타입인 경우 에러가 납니다.

      • Wanbok

        오오… 이 글에서 여러개 배우네요.
        감사합니다.