enum in Swift

Enumerations

Enumeration(열거) 타입은 서로 연관을 맺고 있는 특정한 값들의 그룹을 의미한다. C의 enum과 비슷하다고 할 수 있는데, 그것보다는 하스켈의 대수타입과 보다 더 닮아있다.

C의 enum의 개념에 익숙하다면, C에서는 enum이 일련의 정수에 대해서 고유의 이름을 붙이는 것과 같다는 것을 알 수 있다. 하지만 Swift에서는 보다 유연한데, 각각의 멤버에 대해서 고유한 값을 붙일 수 있으며(물론 붙이지 않아도 상관없다) 이 값은 정수 뿐만 아니라 실수나 문자열, 유니코드 문자가 될 수도 있다.

또한, 열거 멤버 하나하나가 대수 데이터 타입으로 쓰일 수도 있다. 연관이 있는 멤버들을 하나의 열거 타입에 정의하되, 각각의 멤버는 다시 멤버를 구성하고 있는 하나 혹은 그 이상의 다른 값의 세트를 가질 수 있다.

열거 타입은 Swift에서 일급 객체이며, computed property 라든지, 인스턴스 메소드를 가질 수 있다. 또 initializer를 정의하여 기본적인 초기값을 가지고 생성될 수도 있다.

문법

기본적으로 enum 키워드를 이용하며, case를 이용해서 각각의 멤버를 선언한다.

enum CompassPoint {
    case North
    case South
    case East
    case West
}

컴마를 이용해서 case 하나에 여러 멤버를 한 번에 정의하는 것도 가능하다.

enum Planet {
    case Mercury, Venus, Earth, Mars, Jupiter, Saturn, Uranus, Neptune
}

각각의 열더 정의는 새로운 타입을 정의하는 것이다. 다른 타입이나 클래스와 마찬가지로 그래서 대문자로 시작해야 한다. 주로 명칭을 정의할 때는 복수형보다는 단수형을 쓰는 것이 코드내에서 구문의 의미가 명확해진다.

var directionToHead = CompassPoint.West

만약 문맥에서 타입의 추론이 가능한 시점 (등호 좌변의 변수 타입이 확정적일때)에는 이를 생략하는 것도 가능하다.

directionToHead = .East

위 예제에서는 directionToHead의 타입이 이미 정해졌으므로 우변의 CompassPoint를 생략할 수 있다.

Switch 문에서의 활용

switch 문은 기본적으로 패턴 매칭을 적용하므로 enum 타입과 잘 들어맞을 수 있다.

directionToHead = .South

switch directionToHead {
case .North:
    println("Lots of planets have a north")
case .South:
    println("Watch out for penguins")
case .East:
    println("Where the sun rises")
case .West:
    println("Where the skies are blue")
default:
    break
}

연관 타입

enum의 개별 멤버는 다른 멤버와 구분가능한 값처럼 생각하는데 이는 실질적으로는 파라미터를 가지지 않는 값 컨스트럭터라고 보는 편이 옳다. 왜냐하면 enum 타입의 멤버는 다시 연관값이라는 형태로 다른 값을 받아서 그 값들을 쥐고 있는게 가능하기 때문이다.

예를 들어 바코드를 보자. 바코드의 종류는 매우 많지만 그 중에서도 UPCA라는 흔히 마트 계산대에서 찍는 바코드형식과 QR코드라 불리는 형식을 생각해보자. 이 둘은 넓은 의미에서는 바코드의 종류인데, UPCA는 4개의 정수값을 표현하는 그림이고, QR코드는 1개의 문자열 정보로 해석된다. 기존에는 이 둘을 하나처럼 취급하는 것이 쉽지 않았는데 (추상 클래스로부터 파생한 콘크리트 클래스로 볼 수 있다.) Swift에서는 enum 타입으로 쉽게 묶을 수 있다.

enum Barcode {
    case UPCA(Int, Int, Int, Int)
    case QRCode(String)
}

이를 실제로 사용할 때 변수는 Barcode 타입이고, 신기하게도 UPCA나 QRCode 둘 중 어느 하나도 될 수 있다.

var productBarcode = Barcode.UPCA(8, 87325, 14332, 3)
productBarcode = .QRCode("HELLO")

그런데 언뜻 아예 구성 형식이 다른 두 타입에 가까운 것이 하나의 타입으로 묶여 있다. 이 경우 역시 패턴매칭을 통해서 연관값들을 꺼내올 수 있는데 이를 위해 switch 문을 사용한다.

switch productBarcode {
case .UPCA(let a, let b, let c, let d):
    println("UPCA: (a), (b), (c), (d)")
case .QRCode(let str):
    println("QRCode: (str)")
}
//  두 가지 가능한 패턴을 모두 검사했으므로 default 구문은 필요없다.

패턴 매칭을 통한 destruction은 enum 타입의 연관 값을 꺼내는 경우에는 맨 앞에 한 번만 쓰는 것도 허용한다.

switch productBarcode {
case let .UPCA(a, b, c, d):
    println("UPCA: (a), (b), (c), (d)")
case let .QRCode(str):
    println("QRCode: (str)")
}

Raw Value

enum 타입이 C와 유사하게 쓰이고자 한다면, raw 값을 줄 수 있다. 서두에서도 말했듯이 이 값은 정수, 실수, 문자열, 문자가 될 수 있으나, 멤버들에 대해서 모두 같은 타입을 가져야 한다. 이 경우 enum은 특정한 값 타입의 프로토콜을 적용하는 듯 한 문법을 사용한다.

enum CompassPoint : Int {
    case North = 1, East, South, West
}

이경우 East, South, West는 자동적으로 2, 3, 4…. 순으로 열거되는 값을 가진다. enum:Int 는 enum 과 별개의 구조를 가진 타입이기 때문에 rawValue 라는 프로퍼티를 통해서 해당 멤버의 원값을 얻을 수 있으며, 동시에 CompassPoint(rawValue:) 라는 initializer가 자동으로 생성된다. 물론 이는 존재하지 않는 rawValue를 받을 수 있기 때문에 CompoassPoint? 타입을 리턴한다.

원값을 주고 멤버를 선언하는 경우, rawValue는 디폴트로 0부터 시작하며 1씩 증가한다. 중간에 다시 강제로 건너뛴 값을 주게 되면 그 다음 멤버는 1씩 증가한 값을 갖고 나간다.

rawValue 타입이 정수가 아니면 모든 멤버에 대해서 수동으로 값을 정의해주어야 한다.