티스토리 뷰

반응형

안녕하세요 Pingu입니다

 

이번 글에서는 Swift 공식 문서의 8단원인 Enumerations를 읽고 정리한 글을 쓰려고 합니다.

 

Apple Swift 공식 문서 8단원 Enumerations

Enumerations

Enumeration(열거형)은 연관성이 있는 값들을 모아놓은 것을 말합니다. C언어에서의 열거형과 비슷하게 열거형 속 각각의 값에 Int 타입 값을 줄 수 있습니다. Swift에서의 열거형은 좀 더 융통성이 있어서 열거의 각 경우에 값을 꼭 제공할 필요는 없는데요, raw value라고 알려진 각 케이스의 값은 String, Character, Int, Float, Double과 같은 값일 수 있습니다.

 

Swift의 열거형은 일급 객체입니다. 또한 클래스와 비슷하게 프로퍼티를 계산하고 추가적인 정보를 처리하고 관련된 기능을 제공하는 인스턴스 메서드를 지원합니다. 생성자를 정의할 수도 있고 원래 구현된 기능 이상의 것을 추가할 수 있도록 확장 기능도 제공합니다. 프로토콜을 채택해서 표준 기능을 사용할 수도 있습니다.

 

참고로 열거형은 값타입입니다. 이는 다음글인 Structures and Classes에서 자세히 알아보도록 하겠습니다.


Enumeration Syntax

열거형을 선언하기 위해서는 enum 키워드를 사용하면 됩니다.

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

열거형은 위의 코드와 같이 선언될 수 있습니다. 여기서 보이는 north, south, east, west가 enumeration cases(열거 케이스)입니다. 더 추가하고 싶다면 case 키워드를 사용해서 추가해 주면 됩니다. Swift 열거형에서는 C언어와 Objective-C와 다르게 기본적으로 정숫값이 설정되어 있지 않습니다.

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

위의 코드와 같이 케이스들을 한 줄에 나열할 수도 있습니다.

 

열거형을 만들게 되면 하나의 새로운 타입처럼 사용할 수 있습니다. 그렇기 때문에 열거형은 Swift의 이름 규칙에 따라 이름을 대문자로 시작해야 합니다.

var directionToHead = CompassPoint.west

위의 코드와 같이 열거형의 하나의 케이스에 접근할 수도 있습니다. 또한 이제 directionToHead는 선언이 되었기 때문에 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"

위의 코드를 보면 directionToHead는 아까 만든 열거형의 .south 케이스입니다. 따라서 switch 구문의 케이스 중 맞는 것으로 명령을 수행한 결과를 볼 수 있습니다. Switch 구문의 케이스가 CompassPoint 열거형의 케이스를 모두 포함하기 때문에 default는 사용하지 않아도 되고 만약 하나라도 빼먹으면 아래와 같이 default를 사용해야 합니다.

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

열거형을 사용할 때 모든 케이스에 바로 접근할 수 있으면 편리할 것 같지 않나요? 그래서 Swift의 열거형에서는 이러한 기능을 제공합니다! 하지만 열거형에서 기본적으로 제공하진 않고 CaseIterable 프로토콜을 사용해야 이 기능을 사용할 수 있습니다.

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

위의 코드와 같이 열거형에서 CaseIterable 프로토콜을 채택하고 allCases 프로퍼티를 사용하면 케이스들이 Array로 반환되게 됩니다. Array 타입은 count 프로퍼티로 값의 개수를 구할 수 있기 때문에 이렇게 열거형의 케이스 개수를 구할 수 있게 됩니다.

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

물론 개수 말고도 위와 같이 for-in 구문과 함께 사용하면 case의 종류에도 접근할 수 있습니다. 프로토콜에 관한 내용은 후에 Protocols 단원에서 살펴보도록 하겠습니다.


Associated Values

지금까지의 예는 모두 열거형의 케이스가 명시적으로 정의되는 것이었습니다. 이렇게 정의된 열거형을 상수나 변수에 Planet.earth와 같이 저장하고 나중에 이 값을 확인했습니다. 하지만 가끔은 다른 타입의 값과 열거형의 케이스 값을 함께 저장할 수 있는 것이 유용할 수 있습니다. 이렇게 열거형의 케이스와 저장되는 다른 값을 Associated Value라고 합니다.

 

어떠한 타입의 값도 열거형과 함께 사용될 수 있고 각각의 열거 케이스마다 다른 타입을 사용할 수도 있습니다. 이러한 열거형을 discriminated unions, tagged unions, variants in other programming language라고 합니다.

 

예를 들어 재고를 관리하는 시스템이 서로 다른 두 타입의 바코드로 재고를 관리한다고 생각해보겠습니다. 일부 제품에는 UPC 형태의 1D 바코드가 있고 0~9까지의 숫자를 사용하여 제조업체 코드 숫자와 제품 코드 숫자를 나타낸다고 가정하겠습니다.

또 어떠한 제품인 QR코드 타입의 2D 바코드로 표시되며 이는 ISO 8859-1 문자를 사용할 수 있고 최대 2953길이의 문자열을 인코딩 가능하다고 가정하겠습니다.

즉 이와 같이 다른 두 가지 타입의 케이스를 열거형에서 한 번에 다루려면 다음과 같이 나타낼 수 있습니다.

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

upc 바코드 케이스는 Int 타입을 4개 사용하는 튜플의 형태로, QR코드 케이스는 String 타입으로 열거 케이스를 나눠줄 수 있습니다. 이렇게 해서 각각의 타입마다 새로운 값을 생성할 수 있습니다.

var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("https://icksw.tistory.com")

이렇게 되면 productBarcode 변수에는 한 시점에 하나의 값만 저장할 수 있게 됩니다. 즉 upc 바코드 타입이나 QR코드 타입 중 하나만 저장할 수 있다는 것이죠!

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: https://icksw.tistory.com."

즉 위와 같이 switch와 함께 사용해서 해당 값에 있는 값을 추출할 수 있습니다. 또한 여러값이 들어있는 associated value를 위해 값들마다 변수 혹은 상수로 정해 이름을 붙여 사용할 수도 있습니다.


Raw Value

이젠 열거형의 raw Value에 대해 알아보겠습니다. 위에서 본 Associated Value가 열거형 타입이 여러 타입으로 이루어진 값을 저장하는 방법이었다면 raw Value는 기본값으로 미리 선언해 둘 수 있는 타입입니다.

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

위의 코드에서 ASCIIControlCharacter 열거형은 Character 타입으로 정의됩니다. Raw Value는 위와 같이 Character 일 수도 있고 Int, Float, Double, String 타입이 될 수도 있습니다. 하지만 각각의 케이스마다 raw Value는 고유한 값이어야 합니다.

 

Raw Value는 Associated Value와는 다릅니다. Raw Value는 열거형을 처음 정의할 때 미리 채워진 값으로 설정하는 것이고 Associated Value는 타입만 정해두고 나중에 채우는 것입니다. 즉 Raw Value는 새로운 인스턴스가 모두 같은 값을 가지지만 Associated Value는 변할 수 있습니다.

Implicitly Assigned Raw Value

Int, String으로 열거형의 Raw Value를 선언하면 모든 케이스에 대해 Raw Value를 선언하지 않아도 됩니다. 물론 선언하면 그 값이 해당 케이스의 Raw Value가 되겠지만 선언하지 않으면 Swift가 알아서 값을 할당하게 됩니다.

enum Planet: Int {
    case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
let earthsOrder = Planet.earth.rawValue
// earthsOrder is 3

예를 들어 위의 코드에서 보면 Raw Value를 mercury에만 1이라고 선언해 줬습니다. 그 뒤에 케이스들에는 Raw Value를 선언하지 않았지만 Swift에서 알아서 venus에는 2, earth에는 3 과같이 할당을 해둡니다.

enum CompassPoint: String {
    case north, south, east, west
}
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"

만약 위의 코드처럼 아무것도 선언해 주지 않으면 케이스의 이름이 String 타입으로 Raw Value가 됩니다.

Initializing from a Raw Value

열거형의 새로운 인스턴스를 생성할 때 변수나 상수에 rawValue라는 매개변수로 초기화할 수 있는데 이렇게 되면 반환값으로 해당 Raw Value가 존재하면 해당 케이스의 값이 나오고 존재하지 않는 Raw Value라면 nil이 반환됩니다. 즉 옵셔널 타입으로 반환이 된다는 말이죠.

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

위와 같이 값이 있다면 다행이지만 없는 경우 nil이 반환되기 때문에 이는 실패 가능한 초기화 방법입니다. 따라서 if let 구문을 이용한 옵셔널 바인딩을 사용해 안전하게 추출해야 합니다.

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"

위의 코드와 같이 11이라는 Raw Value는 없기 때문에 if let 구문에서 else에 있는 명령을 수행하게 됩니다.


Recursive Enumerations

Recursive Enumerations는 재귀 열거형으로 어떤 열거형의 케이스에 Associated value의 타입으로 자신의 열거형 타입이 들어간 경우를 말합니다. 이렇게 자신의 열거형 타입을 사용하게 되면 해당 케이스 앞에 indirect라는 키워드를 사용해야 하는데 예를 보면 다음과 같습니다.

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

이렇게 여러 번 쓰는 것이 귀찮다면 아래와 같이 아예 제일 처음에 써주는 방법도 있습니다.

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

위의 예제에서 addition, multiplication 케이스에서 recursive enumeration이 사용됐고 해당 케이스에서는 associated value 타입으로 자신의 열거형 타입을 가지고 있습니다.

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"

감사합니다

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함