enumeration, 줄여서 enum이라고 하는 것(열거형이라고 번역됩니다.)은 의미상으로 관련된 값들에 특별한 이름을 붙여 모아놓은 것입니다. Swift에서의 enum은 다른 언어에서보다 훨씬 다양한 기능을 가지고 있는데, 오늘은 이 enum을 알아보겠습니다.

이 글은 다음 문서를 참고하여 작성되었음을 밝힙니다.
Swift Language Guide - Enumeration

C언어를 사용하는 프로그래머에게 enum은 ‘Int 타입의 값에 의미를 명확히 하기 위한 이름을 부여하는 것’ 일 것입니다. Swift에서는 String 타입, Character타입, 정수/실수 타입까지 사용할 수 있게 될 정도로 제한이 완화되었고(특정 조건을 만족시킨다면, 어떠한 타입도 사용이 가능합니다.), 모든 케이스에 대해서 값을 제공해 줄 필요도 없습니다.

Swift에서 enum은 1급 객체입니다. 이 뿐 아니라 이전의 클래스가 가지고 있던 기능들이 상당수 적용되었는데, 다음과 같습니다.

  1. 계산 프로퍼티(computed property)를 가질 수 있습니다. enum의 현재 값에 대한 추가적인 정보를 제공하기 위한 수단으로 사용됩니다.
  2. 인스턴스 메소드(instance method)를 가질 수 있습니다. enum이 나타내는 값에 관련된 추가 기능을 제공합니다.
  3. 생성자(initializer)를 가질 수 있습니다. enum의 기본 값을 제공하기 위해 사용합니다.
  4. 익스텐션(extension)을 적용할 수 있습니다.
  5. 프로토콜(protocol)을 채택할 수 있습니다.

하지만 저장 프로퍼티를 가질 수 없고 enum은 값 타입이라는 점에서 클래스와는 분명한 차이가 있습니다.


기본적인 enum의 문법은 다음과 같습니다.

enum SomeEnumeration {
    case CaseName1 
    case CaseName2
    //필요한 수 만큼의 Case를 작성합니다.
}

// Example
// 4방위를 나타내는 enum
enum CompassPoint {
    case north
    case south
    case east
    case west
}

case를 다음과 같이 한 줄에 몰아서 쓸 수도 있습니다.

enum Planet {
    case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}

모든 enum은 개별적인 타입이기 때문에 다른 타입과 같이 대문자로 시작하도록 이름을 지어줍니다. 또한, enum의 이름은 단수형으로 지어서 자신의 역할을 스스로 드러내도록(self-evident)합니다. 또 타입 추론이 가능한 경우에는 타입명을 생략할 수도 있습니다.

var directionToHead = CompassPoint.west
directionToHead = .east // 타입 생략

enum은 switch문과 쓰는 경우가 많습니다. swift에서 switch문은 가능한 모든 case를 처리할 수 있도록(exhausive) 정의되지 않은 경우에는 컴파일 자체가 안되기 때문에, 일부 case를 의도적으로 생략하려면 default 구문을 작성해야 합니다.

//모든 case를 명시한 switch문
switch directionToHead {
case .north:
    print("Lots of planets have a north")
case .south:
    print("Watch out for penguins")
case .east:
    print("Where the sun rises")
case .west:
    print("Where the skies are blue")
}

// 일부 case를 defalut를 이용해 처리하는 switch문
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
    print("Mostly harmless")
default:
    print("Not a safe place for humans")
}

특정 enum에 대해서는 모든 경우를 순회하고 싶을 수 있습니다. 이 경우에는 CaseIterable 프로토콜을 적용하면, 컴파일러가 allCases라는 프로퍼티를 추가해주고, 해당 프로퍼티를 통해서 case의 개수, 전체 case 순회등이 가능해집니다.

 enum Beverage: CaseIterable {
    case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available") //3 beverages available

for beverage in Beverage.allCases {
    print(beverage)
}
// coffee
// tea
// juice

swift의 enum에는 특별한 기능이 있는데 바로 Associated Value(연관 값) 이라는 기능입니다. 이는 특정 case에 대한 추가 정보를 저장하기 위한 것으로, enum을 사용하는 문맥에 따라 다른 값을 가질 수 있습니다. 이 Associated Value는 어떠한 타입도 가능하고, 값의 수의 제한도 없을 뿐 아니라 case별로 다른 타입의 Associated Value를 가질 수 있습니다. 이러한 기능은 다른 언어에서는 discriminated unions, tagged union, variants 등으로 알려진 기능과 유사합니다.

예시로 다음과 같은 두가지 바코드를 하나의 enum으로 모델링하는 경우를 생각해봅시다.

UPC QR

enum Barcode {
    case upc(Int, Int, Int, Int)
    case qrCode(String)
}

// 초기화 할 때 값을 제공해줍니다.
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

// switch문에서 사용할 때는 다음과 같이 Associated Value를 추출해서 사용할 수 있습니다.
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
    print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(var productCode): // var, let 모두 사용 가능합니다.
    print("QR code: \(productCode).")
}

// 모든 Associated Value가 동일하게 상수 혹은 변수로 추출된다면, 앞에 한번만 쓸 수도 있습니다.
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case var .qrCode(productCode):
    print("QR code: \(productCode).")
}

연관 값과는 별개로, enum의 case는 미리 정해진 값을 가질 수 있는데, 이를 원본 값(Raw Value)라고 합니다. 이 Raw Value는 case마다 모두 같은 타입을 가지며, 코드상에서 미리 정의된 값을 가집니다. 따라서 동일한 case의 두개의 enum이 있다면 연관 값은 다를 수 있지만 원본 값은 반드시 같습니다.

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}

enum CompassPoint: String {
    case north, south, east, west
}

let earthsOrder = Planet.earth.rawValue // earthsOrder == 3

let sunsetDirection = CompassPoint.west.rawValue // sunsetDirection == "west"

Swift의 enum은 기본 타입으로 Int를 사용하지 않습니다. 규칙은 다음과 같습니다.

  1. 명시적으로 타입을 제공하지 않을 경우 : enum의 모든 케이스들은 값을 가지지 않습니다. 따라서 값을 기반으로 하는 생성자도 사용할 수 없습니다.
  2. 정수 / 실수 타입: 이전 케이스의 값에서 1을 더한 값을 사용합니다. 첫 case의 기본 값은 0입니다. 값을 제공해 줄 경우 제공된 값을 우선적으로 사용합니다.
  3. String 타입: case명을 값으로 사용합니다. 값을 제공해 줄 경우 제공된 값을 우선적으로 사용합니다.
  4. Character타입: 컴파일러가 값을 줄 수 없습니다. 모든 case에 대해 명시적으로 값을 제공해야만 합니다.

Raw Value를 사용하는 enum의 경우에는 컴파일러가 자동으로 Raw Value를 인자로 받는 생성자를 제공해줍니다. 다만 Raw Value에 해당하는 case가 없을 수 있으므로 해당 생성자는 옵셔널을 반환함을 염두에 두어야 합니다.(Failable Initializer 를 참고하시면 도움이 됩니다.)

let possiblePlanet:  Planet? = Planet(rawValue: 7) // Planet.

let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
    switch somePlanet {
    case .earth:
        print("Mostly harmless")
    default:
        print("Not a safe place for humans")
    }
} else {
    print("There isn't a planet at position \(positionToFind)")
}

// "There isn't a planet at position 11"

enum은 Assosiated Value의 타입으로 자기 자신을 지정할 수 있습니다. 이 경우에는 case의 앞에 indirect 예약어를 사용하여 컴파일러에게 필요한 조치를 취할 수 있도록 해야합니다.

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

혹은 다음과 같이 쓸 수 있습니다.

// Assosiated Value를 가진 모든 case에 대해 indirection을 허용한다.
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

위의 enum을 이용해서 (5 + 4) * 2 를 계산하는 예시입니다.

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case let .number(value):
        return value
    case let .addition(left, right):
        return evaluate(left) + evaluate(right)
    case let .multiplication(left, right):
        return evaluate(left) * evaluate(right)
    }
}

print(evaluate(product)) // 18

여기까지 enum의 기본 사용법에 대해 알아보았습니다. 다음 글은 enum에 대해 좀 더 심화된 내용을 알아보도록 하겠습니다.