Swift/Swift 가이드

[Swift] 공식 문서 - 열거형 (Enumerations)

유정주 2022. 1. 1. 00:10
반응형

안녕하세요. 개발하는 정주입니다.

 

오늘은 열거형 (Enumerations)를 정리해보겠습니다.

 

* 완벽한 번역이 아닌 내용을 한 줄 한 줄 읽는 것에 의의를 두었습니다.

* 파파고의 힘을 빌려 번역했으며 잘못된 번역이 있다면 댓글로 알려주세요.

 


새로 배운 점

  • 열거형 타입은 ‘복수형 (plural) 보단 단수형 (singular)’ 이름을 부여해야 명확하다는 점
  • CaseIterable를 채택함으로서 모든 케이스 집합체를 가질 수 있다는 점
  • Associated Values에 대한 내용
  • case 이름 앞에 var 나 let annotation을 하나만 적어도 된다는 점
  • 원시 값으로 문자열을 사용할 땐 그 case 이름에 있는 문장이 각 case 의 암시적인 값이 된다는 점
  • 열거형 initializer는 nil을 return할 수 있는 옵셔널형이라는 점
  • 재귀 열거형(Recursive Enumerations)에 대한 내용

목차


    서론

    열거형(enumerations)는 서로 관련된 값 그룹에 공통 타입을 정의하여 코드 안에서 이 값들과 type-safe하게 작업할 수 있게 합니다.

    type-safe라 함은 타입 추론과 타입 검사를 사용할 수 있음을 의미합니다.

     

    C 와 익숙하다면, C 열거체가 서로 관련된 이름을 정수 값 집합에 할당한다는 걸 알고 있을 것입니다. Swift 열거체는 훨씬 더 유연해서 열거체의 각 case 마다 값을 제공하지 않아도 됩니다. 각 열거체 case 마다 값(원시(raw) 값)을 제공하는 경우, 이 값은 string이나 character, 아니면 integer 또는 floating-point 타입의 값이든 다 될 수 있습니다.

     

    대안으로, 다른 언어에서 ‘공용체(unions)나 가변체(variants)’ 가 많이 하는 것처럼, 열거체 case 는 Any 타입의 결합(associated) 값이든 서로 다른 각 case 값과 나란히 저장하도록 지정할 수 있습니다. 한 열거형에서 서로 관련된 case 들의 공통 집합을 정의할 수 있는데, 제각각 자신과 결합된 적절한 타입의 서로 다른 값 집합을 가집니다.

     

    Swift 열거형은 그 자체로 '일급(first-class) 객체' 입니다. 

    이는 열거형의 현재 값에 대한 추가 정보를 제공하는 computed properties와, 열거형이 표현하는 값과 관련한 기능을 제공하는 인스턴스 메소드 같이, 전통적으로 클래스만 지원하던 수많은 특징을 채택합니다. 열거형은 초기 case 값을 제공하는 생성자(initializers)도 정의할 수 있고 원본 구현 너머로 자신의 기능을 늘리도록 확장(extend)할 수도 표준 기능을 제공하도록 프로토콜을 준수할 수도 있습니다.

     


    열거형 구문 (Enumeration Syntax)

     

    열거형은 enum 키워드와 중괄호 안에 모든 정의를 나타냅니다.

    enum SomeEnumeration {
        // enumeration definition goes here
    }

     

    다음 예제는 나침반의 4개의 주 방위(points) 입니다.

    enum CompassPoint {
        case north
        case south
        case east
        case west
    }

    열거형 안에 정의된 값(north, south, east, west)은 열거형 케이스(enumeration cases) 입니다. 새로운 열거형 케이스를 나타내기 위해 case 키워드를 사용합니다.

     

    Swift의 열거형 case 는 C 및 Objective-C 같은 언어와 달리 정수 값을 기본 설정하지 않습니다. 위 CompassPoint 예제의 north, south, east, west는 암시적으로 0, 1, 2, 3과 같지 않습니다. 서로 다른 열거형 case들은 그 자체로 값이며, 명시적으로 정의한 CompassPoint 라는 타입을 가집니다.

     

    여러 개의 case 들을 쉼표로 구분하여 한 줄로 나타낼 수 있습니다.

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

     

    각각의 열거형 정의는 새로운 타입을 정의합니다. Swift의 다른 타입(CompassPoint 와 Planet) 같이 이름은 대문자로 시작합니다. 열거형 타입은 ‘복수형(plural) 보단 단수형(singular)’ 이름을 부여해야 명확해집니다.

    var directionToHead = CompassPoint.west

     

    하나의 가능한 CompassPoint 값으로 초기화할 때 directionToHead의 타입을 추론합니다. 한 번 directionToHead를 CompassPoint 로 선언하고 나면 다른 CompassPoint 값을 더 짧은 점 구문으로 설정할 수 있습니다.

    directionToHead = .east

     

    directionToHead 타입을 이미 알고 있으므로, 값 설정 때 타입을 생략할 수 있습니다. 이는 명시적으로 타입을 지정한 열거형 값과 작업할 때 아주 이해하기 쉬운 코드를 만듭니다.

     


    Switch 구문에서 열거형 값 일치 (Matching Enumeration Values with Switch Statement)

    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"

    이 코드는 다음처럼 이해할 수 있습니다.

    “directionToHead 값을 검토합니다. .north와 같은 경우, "Lots of planets have a north" 를 print 합니다. .south 와 같은 경우, "Watch out for penguins" 을 print 합니다.”

     

    Control Flow(제어 흐름)에서 설명한 것처럼 열거형의 case 를 검토할 땐 반드시 switch 문을 ‘완전 소진 (exhaustive)’ 해야 합니다. .west 이라는 case 를 생략하면 CompassPoint의 완전한 case 목록을 검토한 게 아니기 때문에, 코드를 컴파일하지 않습니다. 완전 소진을 요구하는 건 열거형 case 를 생략하는 사고가 없도록 보장합니다.

     

    모든 열거형 case 마다 case 절을 제공하는 게 적절하지 않을 때 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"

     


    열거형 케이스 반복 (Iterating over Enumeration Cases)

    일부 열거형에서는 그 열거형의 ‘모든 case 를 집합체(collection)’ 로 가지는 게 유용합니다. 열거형 이름 뒤에 : CaseIterable 을 작성하면 이렇게 할 수 있습니다. Swift는 모든 case 의 집합체를 ‘열거형 타입의 allCases 속성’ 으로 노출합니다. 다음은 예제입니다.

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

    위 예제에서는 Beverage.allCases 라고 작성하여 Beverage 열거형의 모든 case 를 담은 집합체에 접근합니다. allCase 는 다른 어떤 집합체인 것 같이 사용할 수 있습니다. 집합체 원소가 열거형 타입의 인스턴스이므로 이 경우엔 Beverage 값입니다. 위 예제는 case의 개수를 세며 아래 예제는 for 반복문을 사용하여 모든 case 에 동작을 반복합니다.

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

    위 예제에서 사용한 구문은 열거형이 CaseIterable 프로토콜을 준수한다고 표시합니다. 

     


    결합 값(Associated Values)

    이전 부분에 있는 예제는 열거형 case 가 그 자체로 (타입을 지정한) 정의 값이라는 걸 보여줍니다. 상수나 변수에 Planet.earth 를 설정할 수도 있고 이 값을 나중에 검사할 수도 있습니다. 하지만 이 case 값과 나란하게 다른 타입의 값을 저장할 수 있는 게 유용할 때가 있습니다. 이런 추가 정보를 결합 값(associated value)이라고 하며 이는 코드에서 그 case 를 값으로 사용할 때마다 변합니다.

     

    Swift 열거형은 주어진 어떤 타입의 결합 값이든 저장할 수 있으며, 필요하다면 각각의 열거형의 case 마다 값 타입이 서로 다를 수도 있습니다. 이와 비슷한 열거형을 다른 프로그래밍 언어에서는 차별화된 공용체(discriminated unions), 꼬리표 단 공용체(tagged unions), 또는 가변체(variants) 라고 합니다.

     

    예를 들어, 재고 추적 시스템이 서로 다른 두 가지 타입의 바코드로 제품을 추적할 필요가 있다고 가정해 봅시다. 

    일부 제품은 0 부터 9 까지의 숫자를 쓴 UPC 양식의 1-차원 바코드를 가지고 이름표를 답니다. 각각의 바코드에는 ‘한 자리 시스템 코드’ 와, 그 뒤의 ‘다섯 자리 제조 회사 코드’ 및 ‘다섯 자리 물품 코드’ 가 있습니다. 그 뒤엔 코드를 올바로 스캔했는 지 증명하는 ‘한 자리 검사 코드’ 를 붙입니다.

    다른 제품은, 어떤 ISO 8859-1 문자든 사용할 수 있고 문자열을 2,953 개의 문자만큼 부호화 (encode) 할 수 있는 QR 코드 양식의 2-차원 바코드를 가지고 이름표를 답니다.

     

    재고 추적 시스템이 'UPC 바코드는 네 정수를 가진 튜플로 저장' 하고, 'QR 코드 바코드는 임의 길이의 문자열로 저장' 하는 게 편리합니다.

     

    Swift에서 두 타입의 바코드를 정의하는 열거형은 다음과 같습니다.

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

    이는 다음처럼 이해할 수 있습니다.

    "(Int, Int, Int, Int) 타입의 연관된 값을 가진 upc 또는 String 타입의 연관된 값을 가진 qrCode 를 취할 수 있는 Barcode 라는 열거형 타입을 정의합니다."

    이 정의는 어떠한 실질적인 Int 나 String 값도 제공하지 않습니다. 그냥 Barcode 상수 및 변수가 Barcode.upc 나 Barcode.qrCode 와 같을 때 저장할 수 있는 결합 값의 타입 (type) 만 정의합니다.

     

    어느 타입을 사용하는 바코드든 새로 생성할 수 있습니다.

    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 문 case 절 본문 안에서 사용할 (let 접두사의) 상수나 (var 접두사의) 변수로 각각의 결합 값을 뽑아냅니다

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

     

    열거형 case 의 모든 결합 값을 상수로 뽑아내거나 변수로 뽑아낼 경우, 간결함을 위해 case 이름 앞에 var 나 let 보조 설명 (annotation) 하나만 둘 수도 있습니다.

    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)

    위에 있는 바코드 예제는 열거형 case가 서로 다른 타입의 결합 값을 저장한다고 선언할 수 있는 방법을 보여줍니다. 

    결합 값의 대안으로써 원시 값(raw values)이라는 모두 동일한 타입의 기본 값을 가지고 열거체 case 를 미리 채울 수 있습니다.

     

    다음은 원시 ASCII 값을 이름 붙인 열거체 case 와 나란하게 저장하는 예제입니다.

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

    여기선, ASCIIControlCharacter 라는 열거형의 원시 값을 Character 타입으로 정의하며, 좀 더 흔한 일부의 ASCII 제어 문자들로 설정합니다.

     

    원시 값은 string이나 character, 아니면 integer 또는 floating-point 타입일 수 있습니다. 각각의 원시 값은 자신의 열거형 선언 안에서 반드시 유일해야 합니다.

     

    원시값은 결합 값(associated values)과 같지 않습니다. 원시값은 위의 3개의 ASCII 코드처럼 코드에서 열거형을 처음 정의할 때 미리 설정하는 값입니다. 특정 열거형 케이스를 위한 원시값은 항상 같습니다. 결합 값은 열거형 케이스 중 하나를 기반으로 새로운 상수 또는 변수를 생성할 때 설정하고 달라질 수 있습니다.

     

    암시적으로 할당된 원시값 (Implicitly Assigned Raw Values)

    정수나 문자열 원시 값을 저장한 열거형와 작업할 때는 각 case 의 원시 값을 명시적으로 할당하지 않아도 됩니다. 할당을 안 할 땐 Swift가 자동으로 값을 할당합니다. 예를 들어, 원시 값으로 정수를 사용할 땐 각 case 값이 이전 case 보다 암시적으로 하나 커집니다. 첫 번째 case 에 값을 설정하지 않으면 그 값은 0 입니다.

     

    아래 열거체는, 태양으로부터 각 행성의 순서를 나타내는 정수 원시 값을 갖도록 앞선 Planet 열거체의 변형된 버전입니다.

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

    위 예제에서 Planet.mercury 는 1 이라는 명시적인 원시 값을 가지고 Planet.venus 는 2 라는 암시적인 원시 값을 가지며 등등 그렇게 계속됩니다.

     

    원시 값으로 문자열을 사용할 땐 그 case 이름에 있는 문장이 각 case 의 암시적인 값입니다.
    아래 열거형은 각 방향의 이름을 나타내는 문자열 원시 값을 갖도록 앞선 CompassPoint 열거체를 개량한 것입니다

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

    위 예제에서, CompassPoint.south 는 "south" 라는 암시적인 원시 값을 가지며, 등등 그렇게 계속됩니다.

     

    열거형 case의 원시 값엔 rawValue 속성으로 접근합니다.

    let earthsOrder = Planet.earth.rawValue
    // earthsOrder is 3
    
    let sunsetDirection = CompassPoint.west.rawValue
    // sunsetDirection is "west"

     

     

    원시값으로 초기화 (Initializing from a Raw Value)

    원시값 타입을 가진 열거형을 정의하면 열거형이 ‘원시 값 타입의 값을 rawValue 라는 매개 변수로 취하고 열거형 case나 nil을 반환하는 초기자(initializer)’ 를 자동으로 받습니다. 이 초기자를 사용하면 새로운 열거형 인스턴스를 생성할 수 있습니다.

     

    이 예제는 7 원시값으로 천왕성을 식별합니다.

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

     

    하지만 모든 Int 값이 일치하는 행성을 찾을 수 있는 건 아닙니다. 이 때문에 원시 값 초기자는 항상 옵셔널(optional) 열거형 case 를 반환합니다. 위 예제에서, possiblePlanet 은 Planet? 타입, 또는 “옵셔널 (optional) Planet” 타입입니다. 원시 값 초기자는 모든 원시 값이 열거형 case 를 반환하는 건 아니기 때문에 ‘실패 가능 (failable) 초기자’ 입니다. 

     

    11 번 째 위치의 행성을 찾으려고 하면 원시 값 초기자가 반환할 옵셔널 Planet 값이 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"

    이 예제는 ‘옵셔널 연결(optional binding)’ 을 사용하여 원시 값 11 을 가진 행성에 접근하려고 합니다. 구문 if let somePlanet = Planet(rawValue : 11)은 옵셔널 Planet 을 생성하고, 옵셔널 Planet 값을 가져올 수 있다면 somePlanet 에 그 값을 설정합니다. 이 경우 11 번 째 위치의 행성을 가져오는 건 불가능하므로 대신 else 분기를 실행합니다.

     


    재귀 열거형 (Recursive Enumerations)

    재귀 열거체(recursive enumeration)는 ‘하나 이상의 열거형 case가 결합 값으로 또 다른 열거형 인스턴스를 가지는 열거형’ 입니다. 열거형 case 가 재귀적이라고 지시하려면 필요한 ‘간접 계층(layer of indirection)’을 집어 넣어야 함을 컴파일러에게 알리고자 그 앞에 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)
    }

     

    이 열거형은 평범한 수, 두 표현식의 덧셈, 그리고 두 표현식의 곱셈이라는 세 가지 종류의 산술 표현식을 저장할 수 있습니다. 

    addition 과 multiplication case 는 또 다시 산술 표현식인 결합 값을 가집니다. 이러한 결합 값은 표현식을 중첩 가능하게 합니다. 

    예를 들어, 표현식 (5 + 4) * 2 는 곱셈 오른-쪽엔 수가 있고 곱셈 왼-쪽엔 또 다른 표현식이 있습니다. 자료를 중첩하기 때문에 자료를 저장하는데 사용하는 열거형도 중첩을 지원할 필요가 있습니다. 이는 열거형이 재귀적일(recursive) 필요가 있다는 의미입니다. 아래 코드는 (5 + 4) * 2 를 생성하고 있는 재귀 열거체인 ArithmeticExpression 을 보여줍니다.

    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"

    이 함수는 단순하게 관련된 값을 반환하여 숫자를 판단합니다. 좌항의 식을 판단하고 우항의 식을 판단한 다음에 이를 더하거나 곱하여 덧셈 또는 곱셈을 판단합니다.


    참조

    https://docs.swift.org/swift-book/

    https://bbiguduk.gitbook.io/swift/

     

     

    반응형