공식 문서로 공부하는 Swift (7) - 열거형

ci·2020년 5월 27일
1

Enumerations

열거형(enumeration)은 관련된 값으로 이루어진 그룹을 공통의 타입으로 선언해 타입 안전성(type-safety)을 보장하는 방법으로 코드를 다룰 수 있게 해준다.

C나 Objective-C가 Integer 값들로 열거형을 구성한 것에 반해, Swift에서는 케이스 값이 String, Character, Integer, Floting-point인 값들도 사용할 수 있다.

열거형은 1급 클래스 타입(first-class types)이다. 열거형은 전통적으로 클래스에 의해서만 지원됐던 많은 기능을 갖고 있다. 현재 값에 대한 추가적인 정보를 제공하는 계산된 프로퍼티(computed properties), 또는 열거형을 대표하는 값과 연관된 기능성을 제공하는 인스턴스 메소드를 지원한다. 또한 초기화를 지정하거나, 초기 선언을 확장해 사용할 수 있다.



열거형 문법

enum 키워드를 선언하고 코드 블록 안에 전체적인 정의를 넣음으로써 열거형을 작성한다.

enum SomeEnumeration {
    // enumeration definition goes here
}
enum CompassPoint {
    case north
    case south
    case east
    case west
}

열거형에 정의된 값들을 그것의 열거 케이스(enumeration case)라 부른다. 새로운 열거 케이스를 추가하기 위해 case 키워드를 사용한다.

Swift의 열거형은 C나 Objective-C와 달리 정수를 기본 값으로 갖지는 않는다. north, south, east, west는 각각 암시적으로 0, 1, 2, 3값을 갖지 않는다. 대신 Swift에서 열거형의 각 case는 CompassPoint으로 선언된 온전한 값이다.


여러 케이스를 한 줄로 표현할 수 있다

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

각각의 열거형은 새로운 타입을 정의한다. 타입의 이름은 Swift의 다른 타입처럼 대문자로 시작한다. 열거형 타입은 복수형보다는 단수형으로 작성한다.

var directionToHead = CompassPoint.west

directionToHead의 타입은 CompassPoint의 가능한 값 중 하나로 초기화될 때 추론된다. 한번 directionToHeadCompassPoint로 선언됐다면 CompassPoint의 다른 값을 축약해서 설정할 수 있다.

directionToHead = .east


Switch 구문에서 열거 값 매칭하기

switch 구문으로 열거형의 개별 값을 매칭할 수 있다.

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"

switch문은 반드시 열거형의 모든 경우(cases)를 완전히 포함해야 한다. 만약 위에서 case .west가 생략되었다면 코드는 컴파일 되지 않는다. 만약 열거형의 모든 cases의 처리를 기술하는게 적당하지 않다면 기본(default) case를 제공함으로써 처리되지 않는 case를 피할 수 있다.

let somePlanet = Planet.earth
switch somePlanet {
case .earth:
    print("Mostly harmless")
default:
    print("Not a safe place for humans")
}
// Prints "Mostly harmless"


열거 케이스 순회하기

열거형의 모든 케이스가 있는 컬렉션을 쓰는 게 유용할 때가 있다. : CaseIterable을 열거형 이름 뒤에 작성함으로써 이를 가능케 한다. Swift는 모든 케이스의 컬렉션을 allCases 프로퍼티를 사용해 접근할 수 있다.

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

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

연관 값 (Associated Value)

열거형의 케이스에 상수나 변수를 설정하고 나중에 이 값을 체크할 수도 있다. 하지만 때때로 이 케이스 값에 다른 타입의 값을 저장하는 게 유용할 수 있다. 이러한 추가적인 정보를 연관 값(associated value)라 부른다.

Swift의 열거형을 어떤 타입의 관련 값이든 저장하도록 정의할 수 있다. 이 값의 타입은 열거형이 필요할 때마다 달라질 수 있다.

두 가지 종류의 바코드를 예로 들어 보자. 어떤 물건은 1줄의 UPC 포맷 바코드가 부여된다. 각각의 바코드는 5자리 제조사 코드 번호와 5자리 제품 번호를 갖고 있다. 스캔이 정확하게 됐는지 확인하기 위한 체크 숫자도 있다.

다른 제품은 QR코드라 불리는 2D 바코드가 부여된다. 이는 ISO 8859-1 캐릭터를 사용하고 2,953개의 문자로 구성된다.

UPC 바코드는 정수 네 개의 튜플로 저장하는 게 유용하다. QR코드는 임의의 문자열로 저장한다.

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

이 정의는 실제 Int값이나 String값을 제공하지는 않는다. Barcide.upc거나 Barcode.qrCode와 같을 때 Barcode 상수와 변수가 저장할 수 있는 관련 값의 타입을 정의한다.


다음과 같이 새로운 바코드를 만들 수 있다.

var productBarcode = Barcode.upc(8, 85909, 51226, 3)

이 예시는 새로운 변수인 productBarcode에 연관 튜플의 값인 (8, 85909, 51226, 3)을 갖는 Barcode.upc를 할당한다.


같은 제품에 다른 바코드 타입을 할당할 수 있다.

productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

기존 Barcode.upc와 그것의 정수 값은 새로운 Barcode.qrCode와 문자열 값으로 대체된다. Barcode 타입의 상수/변수는 .upc.qrCode를 모두 저장할 수 있다. 단, 한 번에 하나의 값만 가능하다.


서로 다른 바코드 타입을 switch문을 이용하여 확인할 수 있다. 하지만 이때 연관 값은 switch문의 일부분으로써 추출된다. 각각의 연관 값을 상수 혹은 변수로 추출하고 switch문의 블록 안에서 사용할 수 있다.

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."


Raw Values

연관 값의 대안으로써, 열거 케이스는 raw values로 불리는 기본값으로 미리 채워질 수 있다. 이 기본 값들은 모두 같은 타입이어야 한다.

enum ASCIIControlCharacter: Character {
    case tab = "\t"
    case lineFeed = "\n"
    case carriageReturn = "\r"
}

raw values는 문자열, 문자, 정수, 실수가 될 수 있다. 각각의 값은 열거형 선언 안에서 고유해야만 한다.

raw values는 연관 값과 같지 않다. raw values는 코드에서 열거형을 처음 선언할 때 미리 채워진다. 특정한 열거 케이스의 raw values는 항상 같다. 연관 값은 새로운 상수나 변수를 만들 때마다 새로 정해진다.


암시적으로 할당된 Raw Values

정수나 문자열 raw values를 저장하는 열거형을 사용할 때, 각 케이스의 raw values를 명시적으로 할당할 필요가 없다.

예를 들어 raw values에 정수를 사용할 경우, 각 케이스의 암시적 값은 이전 케이스의 값보다 하나 크다. 만약 첫 번째 케이스가 값을 갖고 있지 않다면, 값은 0이 된다.


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

위 예시에서 Planet.mercury는 명시적 raw value인 1을 갖는다. Planet.venus는 암시적 raw value`인 2를 갖게 된다.


문자열을 사용할 때는 각 케이스 이름이 암시적 값으로 정해진다.

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

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

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

Raw Value로 초기화 하기

만약 raw value 타입과 함께 열거형을 초기화 한다면, 열거형은 raw value 값의 타입(rawValue라 불리는 매개 변수)을 취하는 initializer를 자동적으로 받게 된다. 이는 열거 케이스 또는 nil을 반환한다.

let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus

하지만 모든 정수 값이 케이스를 찾지는 못한다. 이 때문에 raw value initializer는 항상 옵셔널 열거 케이스를 반환한다.

raw value initializer는 실패할 가능성이 있다. 모든 raw value가 열거 케이스를 반환하지는 않기 때문이다.


만약 11로 행성을 찾고자 한다면 nil을 반환할 것이다.

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 enumeration)은 다른 열거 인스턴스를 관계 값으로 갖는 열거형이다. 재귀 열거자 case는 앞에 indirect키워드를 붙여 표시한다. 즉 특정한 사례의 연관 값의 멤버중에 자기 자신을 넣는 것이 가능하다.

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

연관 값을 갖는 모든 열거 케이스에 indirect를 표시하고 싶으면 열거형의 시작 부분에 붙일 수 있다.

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

additionmultiplication 케이스 역시 ArithmeticExpression을 연관 값으로 가진다. 이러한 연관 값은 중첩된 표현을 가능케 한다.


다음 코드는 ArithmeticExpression 재귀적 열거형으로 (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))
// Prints "18"

0개의 댓글