본문 바로가기
프로그래밍/Swift

[Swift] Enumerations (열거형, 열거자)

by 별준 2021. 12. 22.

References

Contents

  • Enumeration Syntax
  • Maching Enumeration Values with a Switch Statement
  • Iterating over Enumeration Cases
  • Associated Values
  • Raw Values
  • Recursive Enumerations

Enumeration(열거형)은 관련된 값들의 그룹에 대한 공통적인 유형을 정의하고 코드 내에서 type-safe한 방식으로 해당 값으로 작업할 수 있도록 합니다. C에 익숙하다면 C 열거형이 정수 값들의 집합에 이름을 할당한다는 것을 알고 있습니다. Swift의 열거형은 훨씬 더 유연하고 열거형의 각 케이스에 대해 값을 할당할 필요가 없습니다. 만약 값을 할당해야한다면, 그 값은 문자열, 문자, 또는 어떤 정수나 부동소수점의 값이 될 수 있습니다.

 

또한, 열거형의 케이스로 다른 케이스 값과 함께 저장되는 어떠한 타입의 관련된 값들을 지정할 수 있습니다. 관련된 공통 집합을 하나의 열거형의 일부로 정의할 수 있고, 각 열거형에는 서로 다른 값의 집합들을 가지고 있을 수 있습니다.

(하나의 열거형 케이스가 여러 개의 값들의 집합을 가리킬 수 있습니다.)

 

Swift의 열거형은 그 자체로 first-class 타입입니다. 이들은 오직 클래스에 의해서만 지원되는 전통적인 기능들을 가지고 있습니다(열거형의 현재 값에 대한 추가 정보를 제공하는 계산 속성, 관련된 기능을 제공하는 인스턴스 메소드 등). 또한 각 케이스의 초기 값을 제공하는 이니셜라이저를 정의할 수 있고, 기능을 확장할 수 있으며 표준 기능을 제공하기 위해 프로토콜을 따를 수도 있습니다.

 


Enumeration Syntax

enum 키워드를 사용하여 열거형을 정의할 수 있습니다.

enum SomeEnumeration {
    // enumeration definition goes here
}

아래는 4가지 방향을 갖는 CompassPoint 열거형 선언의 예입니다.

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

열거형에서 정의된 각 값(north, south, east, west)들은 열거형 케이스(enumeration cases)라고 합니다. case 키워드를 사용하여 새로운 열거형 케이스를 추가할 수 있습니다.

Swift의 열거형 케이스는 C나 Objective-C와는 달리 기본적으로 설정되는 정수값이 없습니다. 위 CompassPoint 열거형에서 north, south, east, west는 암시적으로 0,1,2,3의 값이 할당되지 않습니다. 열거형 케이스들은 명시적으로 정의된 타입을 가지는 자체적인 값입니다.

 

여러 케이스들을 콤마로 구분하여 한 줄에 표현할 수도 있습니다.

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

 

각 열거형 정의는 새로운 타입을 정의합니다. Swift의 다른 타입과 마찬가지로 열거형의 이름은 보통 대문자로 시작하고, 열거형 타입 이름은 단수로 명명하여 의미를 명확하게 합니다.

var directionToHead = CompassPoint.west

위의 directionToHead의 타입은 CompassPoint에서 가능한 값 중의 하나로 초기화될 때 추론됩니다. directionToHead가 일단 CompassPoint로 선언되면, 더 짧은 dot 문법으로 다른 CompassPoint 값을 설정할 수 있습니다.

directionToHead = .east

 

 


Matching Enumeration Values with a 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"

 

 

switch문에 열거형을 사용할 때, 열거형의 모든 케이스를 포함해야 합니다. 만약 case에 .west가 생략되었다면, 컴파일 에러가 발생합니다. 만약 열거형의 모든 케이스의 처리를 기술하는 것이 적절하지 않다면 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

일부 열거형에서는 그 열거형의 모든 케이스들의 집합을 포함하는 것이 유용합니다. 이는 열거형 이름 뒤에 CaseIterable을 작성하면 가능합니다. CaseIterable을 사용하면 열거형의 allCases 속성을 사용하여 모든 케이스의 콜렉션을 나타낼 수 있습니다.

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

위 예제 코드에서 Beverage.allCases는 Beverage 열거형의 모든 케이스를 포함하는 콜렉션에 액세스합니다. allCases는 다른 콜렉션처럼 사용할 수 있는데, 이 콜렉션의 요소는 열거형 타입의 인스턴스들입니다. 

아래 코드는 for-in 루프를 사용하여 모든 케이스들을 반복하는 것을 보여줍니다.

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

 

위 예제 코드에서 사용된 문법은 CaseIterable 프로토콜을 따르는 열거형에서 사용됩니다.

 


Associated Values

앞서 살펴본 예제들은 열거형의 케이스들이 그 자체의 값으로 정의되는 방법을 보여주었습니다. 상수나 변수에 Planet.earth를 설정하고 이를 값으로 확인할 수 있습니다. 그러나, 때때로는 다른 타입의 값들을 저장하는 것이 유용할 수 있습니다. 이러한 추가적인 정보를 associated value라고 하며, 코드에서 이 케이스의 값들은 매번 사용할 때마다 다를 수 있습니다.

 

Swift 열거형은 주어진 타입의 관련된 값들을 저장할 수 있으며, 필요한 경우에 각 케이스에 대한 값 타입을 다르게 할 수도 있습니다. 이러한 열거형은 다른 프로그래밍 언어에서 discriminated unions, tagged unions, 또는 variants으로 알려져 있습니다.

 

예를 들어, 물품 추적 시스템에서 두 가지 다른 타입의 바코드를 사용하여 제품을 추적해야 한다고 가정해보겠습니다. 일부 제품에서는 0에서 9까지의 숫자를 사용하는 UPC 포맷의 1D 바코드가 표시되어 있습니다. 각 바코드는 시스템 숫자 하나, 5자리의 제조사 코드, 5자리의 제품 코드, 그리고 올바르게 스캔되었는지 확인하기 위한 숫자 하나로 구성되어 있습니다. 

다른 제품에는 QR 코드 포맷의 2D 바코드가 표시되어 있습니다. 이 포맷은 ISO 8859-1 문자를 사용하고, 최대 2953개의 문자로 구성된 문자열을 인코딩할 수 있습니다.

물품 추적 시스템에서 UPC 바코드는 4개의 정수로 구성된 튜플로 저장하고, QR 코드 바코드는 임의의 길이의 문자열로 저장하는 것이 편리합니다. 따라서, 이를 표현하는 열거형은 다음과 같을 수 있습니다.

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

위 코드는 Barcode라는 열거형 타입을 정의하고 있습니다. 이 열거형은 (Int, Int, Int, Int) 타입의 associated value들로 구성된 upc의 값과 String 타입의 associated value들로 구성된 qrCode의 값을 취할 수 있습니다. 이 정의에서는 실제 Int 또는 String 값은 제공하지 않으며 Barcode 타입의 상수나 변수가 저장할 수 있는 associated value의 타입만을 정의합니다.

 

정의한 다음에 Barcode 타입 중 하나를 사용하여 새로운 바코드를 생성할 수 있습니다.

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

productBarcode라는 새로운 변수를 생성하고, Barcode.upc의 값인 associated tuple value인 (8, 85909, 51226, 3)를 productBarcode에 할당하고 있습니다.

아래와 같이 동일한 제품에 다른 타입의 바코드를 할당할 수도 있습니다.

productBarcode = .qrCode("ABCDEFGHIJKLMNOP")

여기서, 원래 Barcode.upc와 정수 값들은 새로운 Barcode.qrCode와 문자열 값들로 대체되었습니다. Barcode 타입의 상수와 변수는 둘 다 .upc 또는 .qrCode를 저장할 수 있지만, 한 번에 하나의 타입만을 저장할 수 있습니다.

 

Matching Enumeration Values with a Switch Statement의 예시처럼 Switch문을 사용하여 다양한 바코드 타입을 확인할 수 있습니다. 그러나 이번에는 switch문의 일부로 associated value들이 추출됩니다. switch 케이스 본문 내에서 사용할 각 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."

 

만약 열거형 케이스에서 모든 associated value가 상수 또는 변수로 추출된다면, 하나의 var 또는 let을 케이스 이름 앞에 사용하여 간략하게 작성할 수 있습니다.

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

associated value의 대안으로 열거형의 케이스가 모두 동일한 타입의 기본값으로 미리 채워질 수 있습니다.

다음 코드는 열거형 케이스들에 기본 ASCII 값을 저장하는 예제입니다.

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

ASCIIControlCharacter라는 열거형에서 raw값은 Character 타입으로 정의되고, 몇몇의 일반적인 ASCII 제어 문자로 설정되었습니다.

 

Raw value는 문자열, 문자, 어떤 정수나 부동소수점 타입도 가능합니다. 각 raw value는 열거형의 정의 내에서 유일해야합니다.

Raw values는 associated value와 같지 않습니다. Raw values는 열거형을 정의할 때 미리 채워진 값으로 설정됩니다. 특정 열거형 케이스에 대한 raw value는 항상 동일합니다. 반면 associated value는 새로운 상수나 변수를 열거형의 케이스 중의 하나로 생성할 때 설정되며, 그 값은 생성할 때마다 달라질 수 있습니다.

 

Implicitly Assigned Raw Values

정수나 문자열 raw values를 열거형에 저장하도록 작업할 때, 각 케이스의 raw value는 명시적으로 할당할 필요는 없습니다. 명시적으로 할당하지 않는다면, Swift는 자동으로 그 값을 할당합니다.

 

예를 들어, 정수를 raw value로 사용하고자 할 때, 각 케이스에 대한 암시적인 값은 이전 케이스의 값보다 1큰 값으로 설정됩니다. 만약 첫 번째 케이스의 값이 설정되지 않았다면 이 값은 0으로 설정됩니다.

 

Planet 열거형을 아래처럼 정의해봅시다.

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

위 코드에서 Planet.mercury는 1이 명시적으로 설정됩니다. 그리고 Planet.venus는 암시적으로 2로 설정됩니다.

 

raw value로 문자열을 사용할 때, 각 케이스의 암시적인 값은 바로 그 케이스의 이름입니다.

CompassPoint 열거형을 아래처럼 다시 정의해봅시다.

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

이 코드에서 CompassPoint.south의 값은 암시적으로 "south"로 설정됩니다.

 

각 케이스의 raw value는 rawValue 속성으로 액세스할 수 있습니다.

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

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

 

Initializing from a Raw Value

만약 raw-value 타입의 열거형을 정의하는 경우, 그 열거형은 자동으로 raw value 타입의 값을 파라미터(called rawValue)로 받는 이니셜라이저를 가지고, 열거형의 케이스나 nil를 반환합니다. 열거형의 새로운 인스턴스를 생성하는데 이 이니셜라이저를 사용할 수 있습니다.

 

아래 코드는 7이라는 raw value로부터 Uranus를 식별하는 예제입니다.

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

 

그러나 가능한 Int값 중에 일부는 일치하는 행성에 매칭되지 않습니다. 이 때문에 raw value 이니셜라이저는 항상 optional 열거형 케이스를 리턴합니다. 위 예제 코드에서 possiblePlanet의 타입은 Planet? (optional 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)")
}
// Prints "There isn't a planet at position 11"

위 코드에서는 optional binding을 사용하여 11이라는 raw value에 매칭되는 planet에 액세스하도록 시도합니다. line 2에서는 optional Planet을 생성하고, 이 값을 somePlanet에 설정합니다. 이 경우, 11에 해당하는 케이스가 없기 때문에 else 분기가 수행됩니다.

 


Recursive Enumerations

재귀 열거형(recursive enumerations)는 하나 이상의 케이스들을 associated value로 갖는 열거형의 다른 인스턴스들을 갖는 열거형을 의미합니다. 열거형의 케이스는 indirect 를 사용하여 재귀 열거 케이스를 추가할 수 있습니다.

 

예를 들어, 간단한 산술 표현식을 저장하는 열거형을 살펴보겠습니다.

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

또한, 열거형를 정의할 때 enum 앞에 indirect 키워드를 작성하여 associated value를 갖는 모든 열거형의 케이스들이 indirection이 되도록 할 수 있습니다.

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

위 열거형은 3종류의 산술 표현식(일반 숫자, 덧셈, 곱셈)을 저장합니다. 덧셈과 곱셈의 경우, 산술 표현식인 associated value를 가지고 있으며, 이 값들을 사용하여 표현식을 중첩할 수 있습니다. 예를 들어, (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"

 

'프로그래밍 > Swift' 카테고리의 다른 글

[Swift] Properties  (0) 2021.12.25
[Swift] Structures and Classes (구조체와 클래스)  (0) 2021.12.24
[Swift] Closures (클로저)  (0) 2021.12.22
[Swift] Functions (함수)  (0) 2021.12.18
[Swift] Control Flow (제어문)  (0) 2021.12.16

댓글