본문 바로가기
Swift 공식 가이드/Swift 3

Enumerations

by 토끼찌짐 2017. 2. 27.

Apple 제공 Swift 프로그래밍 가이드(3.0.1)의 Enumerations 부분을 공부하며 정리한 글입니다. 개인적인 생각도 조금 들어가있습니다.


들어가며

Enumeration(이하  ENUM)은 관련된 값들을 그룹으로 묶어서 common type 으로 정의해준다. ENUM을 이용하면 타입에 안전한(type-safe) 코드를 작성할 수 있다.

C에서의 ENUM은 각 케이스마다 Integer 값을 할당했었다. 이것을 raw value라고 한다. Swift에서의 ENUM은 raw value로 여러가지 타입을 사용할 수 있고 심지어 항상 raw value 를 할당하지 않아도 된다. 또한 케이스 각각에 연관된 값들(associated values)을 지정할 수 있다.

Swift에서의 ENUM은 기존에는 class에서만 지원되던 기능들을 많이 제공해준다. 예를들면, 계산된 프로퍼티(computed properties), 인스턴스 메서드 등이 있다. 또한 ENUM의 이니셜라이저를 정의할 수도 있고, 기능을 확장시킬 수도 있고, 특정 프로토콜을 따르게 할 수도 있다. 자세한 것은 나중에 살펴보자.



Enumeration Syntax

enum 키워드를 통해 정의할 수 있다.

case 키워드를 통하여 enumeration case 들을 정의할 수 있다.

enum CompassPoint { // 대문자로 시작하게끔
    case north // c, objective-c 와 다르게 자동으로 0부터 채워지지 않는다
    case south
    case east
    case west
}

enum Planet { 
    case mercury, venus, earth, mars, jupiter, saturn, 
          uranus, neptune
    // 콤마로 구분하여 정의 가능
    // 복수형말고 단수형이 자명하다
}


이렇게 정의한 ENUM을 변수에 할당하는 법을 알아보자.

var directionToHead = CompassPoint.west
// 변수가 CompassPoint ENUM 타입이라고 유추되는 타이밍은
// west 케이스로 초기화가 될 때이다

directionToHead = .east
// 한 번 타입이 판단된 변수는 이런 식으로
// ENUM 이름을 생략하고 값을 할당할 수 있다



Matching Enumeration Values with a Switch Statement

directionToHead = .south
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")
}
// prints "Watch out for penguins"


// <주의> ENUM의 모든 케이스를 포함할 수 있어야 한다.
// 그게 안 된다면 default 케이스를 사용하자.
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
    print("Mostly harmless")
default:
    print("Not a safe place for humans")
}
// prints "Mostly harmless"



Associated Values

ENUM의 케이스 각각에 연관된 다른 타입의 값들을 저장할 수 있다. 따라서 필요한 추가정보를 저장할 수 있으며, ENUM 변수에 접근할 때마다 이 정보를 변경하는 것도 가능하다. 이 값을 associated value라고 부른다. associated value 의 타입과 개수는 케이스 별로 각각 상이하게 정의되어도 된다.

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


// 아래와 같이 사용
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
productBarcode = .qrCode // 컴파일에러

// <추가설명>
let qrCodeBarcode = Barcode.qrCode
// 이 상수의 타입은 (String) -> Barcode
let myQrCode = qrCodeBarcode("ABCDEFGHIJKLMNOP")
// 이 상수의 타입은 Barcode이고 값은 qrCode("ABCDEFGHIJKLMNOP")


// let과 var로 associated value를 추출할 수 있다
switch productBarcode {
case .upc(let numberSystem, let manufacturer, 
          let product, let check):
    print("UPC : \(numberSystem), \(manufacturer), 
           \(product), \(check).")
case .qrCode(let productCode):
    print("QR code: \(productCode).")
}
// prints "QR code: ABCDEFGHIJKLMNOP."


// 튜플 자체를 한번에 let 또는 var로 받아도 된다
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
    print("UPC : \(numberSystem), \(manufacturer),
          \(product), \(check).")
case let .qrCode(productCode):
    print("QR code: \(productCode).")
}
// prints "QR code: ABCDEFGHIJKLMNOP."
// <참고> 만약 case let .upc(values): 로 받으면
// values는 4개 요소로 구성된 튜플이다



Raw Values

C에서 했던 것처럼 Default value(raw value)로 ENUM의 케이스를 채울 수도 있다. 이 경우에는 모든 케이스의 값의 타입이 일치해야 한다.

enum ASCIIControlCharacter: Character {
    // raw value의 type을 필수로 명시해주어야 한다!
    // 여기서는 Character 라고 정의
    
    case Tab = "\t"
    case LineFeed = "\n" // raw value의 타입은 모든 케이스가 같아야
    case CarriageReturn = "\r" // 그리고 케이스 별로 유니크해야
}

print(ASCIIControlCharacter.Tab.rawValue) // "\t"

raw value는 꼭 Integer 값이 아니라 String, Character, Integer, Floating-Point 등 여러 타입을 사용할 수 있다.

<note> Raw Value는 ENUM을 처음 정의한 순간부터 들어가있는 디폴트 값이고 Associated Value는 ENUM의 케이스를 가지고 변수/상수를 만들 때마다 셋팅이 되는 값이다. 이 두개는 다른 것이다.

<참고> enum 선언 시 rawValue의 타입 지정을 하지 않은 경우에는 케이스에 대고 rawValue를 부르지 못한다. 그런 멤버가 없다는 오류가 난다.


<Implicitly Assigned Raw Values>

명시적으로 전부 raw value를 할당하지 않더라도, raw value의 type만 명시해준다면 컴파일러가 알아서 raw value를 할당해준다.

enum Planet: Int {
    case mercury, venus, earth, mars, jupiter
   // 0, 1, 2, 3, 4
}

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter
   // 1, 2, 3, 4, 5
}

enum Planet: Int {
    case mercury=1, venus, earth, mars=10, jupiter
    // 1, 2, 3, 10, 11
}

enum Planet: Int {
    case mercury, venus = 6, earth, mars, jupiter
   // 0, 6, 7, 8, 9
}

enum Planet: Int {
    case mercury, venus = 0, earth, mars, jupiter
    // 컴파일 에러 (raw value가 유니크하지 않다며)
    // 0, 0, 1, 2, 3 이 되니까
}

enum Planet: Int {
    case mercury = 6, venus = 5, earth, mars, jupiter
    // 컴파일 에러 (raw value가 유니크하지 않다며)
    // 6, 5, 6, 7, 8, ... 이 되니까
}


raw value 타입이 String 인 경우에는 default Raw Value 값이 Case 이름 자체이다.

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

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


// <추가설명>
enum CompassPoint: String {
    case north, south = "north", east, west
    // raw value가 unique하지 않기 때문에 컴파일 에러
}


<Initializing from a Raw Value>

rawValue가 있는 ENUM은 rawValue를 파라미터로 넘기는 이니셜라이저를 사용할 수 있다.

let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet 의 타입은 Planet?

// rawValue가 7인 Type이 정의되어 있지 않다면 nil이 반환된다
// 이렇게 nil이 될 수 있는 이니셜라이저를 failable initializer 라고 한다

//  옵셔널 바인딩을 함께 활용
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)")
}
// prints "There isn't a planet at position 11"



Recursive Enumerations

recursive enumeration 이란 한 개 이상 케이스의 associated value로 ENUM의 인스턴스를 받는 ENUM을 말한다. indirect 라는 키워드를 사용하여 만든다.

 // 다음과 같이 필요 케이스 앞에 indirect 키워드를 붙여도 되고
enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

// 이렇게 enum 옆에 indirect 를 명시하면 모든 case에 적용
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}


이것을 이용하는 계산기 예제를 보자.

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)
    }
}

// evaluate (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))

print(evaluate(product))
// prints "18"


'Swift 공식 가이드 > Swift 3' 카테고리의 다른 글

Properties  (0) 2017.02.27
Classes and Structures  (0) 2017.02.27
Closures  (0) 2017.02.21
Functions  (0) 2017.02.19
Control Flow  (0) 2017.02.15