으니의 개발로그

[Swift] 데이터 타입 고급(3) - 열거형 본문

Swift/책 정리

[Swift] 데이터 타입 고급(3) - 열거형

아잉으니야 2021. 1. 12. 22:33

[Swift] 데이터 타입 고급(3) - 열거형

이 글은 Swift 프로그래밍 책을 읽고 요약한 내용입니다.

 

열거형 : 연관된 항목들을 묶어서 표현할 수 있는 타입

배열이나 딕셔너리 같은 타입과 다르게 프로그래머가 정의해준 항목 값 외에는 추가/수정이 불가함. 그렇기 때문에 딱 정해진 값만 열거형 값에 속할 수 있음

 

  • 열거형이 요긴하게 사용되는 경우
    • 제한된 선택지를 주고 싶을 때
    • 정해진 값 외에는 입력받고 싶지 않을 때
    • 예상된 입력 값이 한정되어 있을 때
  • 열거형으로 묶을 수 있는 항목의 예
    • 무선통신 방식 : WiFi, 블루투스, LTE, 3G, 기타
    • 학생 : 초등학생, 중학생, 고등학생, 대학생, 대학원생, 기타
    • 지역 : 강원도, 경기도, 경상도, 전라도, 제주도, 충청도

 

 

1. 기본 열거형

스위프트의 열거형은 enum 이라는 키워드로 선언할 수 있음

 

School 열거형의 선언

enum School {
    case primary        // 유치원
    case elementary     // 초등
    case middle         // 중등
    case high           // 고등
    case college        // 대학
    case university     // 대학교
    case graduate       // 대학원
}

각 항목은 그 자체가 고유의 값이며, 항목이 여러 가지라서 나열하기 귀찮거나 어렵다면 한 줄에 모두 표현해 줄 수 있음

enum School {
    case primary, elementary, middle, high, college, university, graduate
}

 

School 열거형 변수의 생성 및 값 변경

var highestEducationLevel1: School = School.university

// 위 코드와 정확히 같은 표현
var highestEducationLevel2: School = .university

// 같은 타입인 School 내부의 항목으로만 highestEducationLevel의 값을 변경해줄수 있음
highestEducationLevel2 = .graduate

 

 

2. 원시 값

열거형의 강 항목은 자체로도 하나의 값이지만 항목의 원시 값(Raw Value)도 가질 수 있음. 즉, 특정 타입으로 지정된 값을 가질 수 있다는 뜻. 특정 타입의 값을 원시 값으로 가지고 싶으면 열거형 이름 오른쪽에 타입을 명시해주면 됨. 또, 원시 값을 사용하고 싶다면 rawValue라는 프로퍼티를 통해 가져올 수 있음

enum School: String {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college = "대학"
    case university = "대학교"
    case graduate = "대학원"
}

let highestEducationLevel: School = School.university
print("저의 최종학력은 \(highestEducationLevel.rawValue) 졸업입니다.")
/* 저의 최종학력은 대학교 졸업입니다. */

enum WeekDays: Character {
    case mon = "월", tue = "화", wed = "수", thu = "목", fri = "금", sat = "토", sun = "일"
}

let today: WeekDays = WeekDays.fri
print("오늘은 \(today.rawValue)요일입니다.")
/* 오늘은 금요일입니다. */

 

일부 항목만 원시 값을 주는 것도 가능. 문자열 형식의 원시 값을 지정해줬다면 각 항목 이름을 그대로 원시 값으로 갖게 되고, 정수 타입이라면 첫 항목을 기준으로 0부터 1씩 늘어난 값을 갖게 됨

enum School: String {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college
    case university
    case graduate
}

let highestEducationLevel: School =  School.university
print("저의 최종학력은 \(highestEducationLevel.rawValue) 졸업입니다.")
/* */

print(School.elementary.rawValue)
/* 초등학교 */

enum Numbers: Int {
    case zero
    case one
    case two
    case ten = 10
}

print("\(Numbers.zero.rawValue), \(Numbers.one.rawValue), \(Numbers.two.rawValue), \(Numbers.ten.rawValue)")
/* 0, 1, 2, 10 */

열거형이 원시 값을 갖는 열거형일 때, 열거형의 원시 값 정보를 안다면 원시 값을 통해 열거형 변수 또는 상수를 생서해줄 수 있음. 만약 올바르지 않은 원시 값을 통해 생성하려고 한다면 nil 반환

let primary = School2(rawValue: "유치원")  // primary
let graduate = School2(rawValue: "석박사") // nil

let one = Numbers(rawValue: 1)      // one
let three = Numbers(rawValue: 3)    // nil

 

 

3. 연관 값

열거형 내의 항목(case)이 자신과 연관된 값을 가질 수 있음. 연관 값은 각 항목 옆에 소괄호로 묶어서 표현. 다른 항목이 연관 값을 갖는다고 모든 항목이 연관 갖을 가질 필요는 없음

// 연관 값을 갖는 열거형
enum MainDish {
    case pasta(taste: String)
    case pizza(dough: String, topping: String)
    case chicken(withSauce: Bool)
    case rice
}

var dinner: MainDish = MainDish.pasta(taste: "크림")     // 크림 파스타
dinner = .pizza(dough: "치즈크러스트", topping: "불고기")    // 불고기 치즈크러스트 피자
dinner = .chicken(withSauce: true)  // 양념 통닭
dinner = .rice  // 밥

 

여러 열거형의 응용

enum PastaTaste {
    case cream, tomato
}

enum PizzaDough {
    case cheeseCrust, thin, original
}

enum PizzaTopping {
    case peperoni, cheese, bacon
}

enum MainDish {
    case pasta(taste: PastaTaste)
    case pizza(dough: PizzaDough, topping: PizzaTopping)
    case chicken(withSauce: Bool)
    case rice
}

var dinner: MainDish = MainDish.pasta(taste: PastaTaste.tomato)
dinner = MainDish.pizza(dough: PizzaDough.cheeseCrust, topping: PizzaTopping.bacon)

 

 

4. 항목 순회

열거형에 포함된 모든 케이스를 알아야 할 때 열거형의 이름 뒤에 콜론(:) 을 작성하고 한칸 띄운 뒤 CaseIterable 프로토콜을 채택해준다. 그러면 열거형에 allCases 라는 이름의 타입 프로퍼티를 통해 모든 케이스의 컬렉션을 생성해줌

enum School: CaseIterable {
    case primary
    case elementary
    case middle
    case high
    case college
    case university
    case graduate
}

let allCases: [School] = School.allCases
print(allCases)
/* [School.primary, School.elementary, School.middle, School.high, School.college, School.university, School.graduate */

 

원시값을 갖는 열거형이라면 원시값 타입 다음에 쉼표(,) 를 쓰고 띄어쓰기를 한 후 CaseIterable 프로토콜을 채택해 주면 됨.

enum School: String, CaseIterable {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college = "대학"
    case university = "대학교"
    case graduate = "대학원"
}

let allCases: [School] = School.allCases
print(allCases)
/* [School.primary, School.elementary, School.middle, School.high, School.college, School.university, School.graduate */

 

단순한 열거형에는 CaseIterable 프로토콜을 채택해 주는 것만으로 allCases 프로퍼티를 사용할 수 있다. 그렇지만 조금 복잡해지는 열거형은 그렇지 않을 수도 있다. 그 대표적인 예가 플랫폼별로 사용 조건을 추가하는 경우임

enum School: String, CaseIterable {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college = "대학"
    case university = "대학교"
    @available(iOS, obsoleted: 12.0)
    case graduate = "대학원"

    static var allCases: [School] {
        let all: [School] = [.primary, .elementary, .middle, .high, .college, .university]

        #if os(iOS)
        return all
        #else
        return all + [.graduate]
        #endif
    }
}

let allCases: [School] = School.allCases
print(allCases)     // 실행환경에 따라 다른 결과
/* [School.primary, School.elementary, School.middle, School.high, School.college, School.university */

 

available 속성을 통해 특정 케이스를 플랫폼에 따라 사용할 수 있거나 없는 경우가 생기면 CaseIterable 프로토콜을 채택하는 것만으로는 allCases 프로퍼티를 사용할 수 없기 때문에 직접 allCases 프로퍼티를 구현해 주어야 함. 이렇게 CaseIterable 프로토콜을 채택하여도 allCases 프로퍼티를 바로 사용할 수 없는 경우가 또 있는데, 바로 열거형의 케이스가 연관 값을 갖는 경우임

enum PastaTaste: CaseIterable {
    case cream, tomato
}

enum PizzaDough: CaseIterable {
    case cheeseCrust, thin, original
}

enum PizzaTopping: CaseIterable {
    case peperoni, cheese, bacon
}

enum MainDish {
    case pasta(taste: PastaTaste)
    case pizza(dough: PizzaDough, topping: PizzaTopping)
    case chicken(withSauce: Bool)
    case rice

    static var allCases: [MainDish] {
        return PastaTaste.allCases.map(MainDish.pasta) + PizzaDough.allCases.reduce([]) { (result, dough) -> [MainDish] in result + PizzaTopping.allCases.map { (topping) -> MainDish in MainDish.pizza(dough: dough, topping: topping)}}
            + [true, false].map(MainDish.chicken)
            + [MainDish.rice]
    }
}

print(MainDish.allCases.count)
/* 14 */
print(MainDish.allCases)    // 모든 경우의 연관 값을 갖는 케이스 컬렉션

 

 

5. 순환 열거형

순환 열거형은 열거형 항목의 연관 값이 열거형 자신의 값이고자 할 때 사용. 순환 열거형을 명시하고 싶다면 indirect 키워드를 사용하면 됨. 특정 항목에만 한정하고 싶다면 case 키워드 앞에 indirect 를 붙이면 되고, 열거형 전체에 적용하고 싶다면 enum 키워드 앞에 indirect 키워드를 붙이면 됨.

 

// 특정 항목에 순환 열거형 항목 명시
enum ArithmeticExpression1 {
    case number(Int)
    indirect case addition(ArithmeticExpression1, ArithmeticExpression1)
    indirect case multiplication(ArithmeticExpression1, ArithmeticExpression1)
}

// 열거형 전체에 순환 열거형 명시
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

 

ArithmeticExpression 열거형을 사용하여 (5 + 4) * 2 연산 구해보기

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let final = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

func evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

let result: Int = evaluate(final)
print("(5 + 4) * 2 = \(result)")
/* (5 + 4) * 2 = 18 */

 

 

6. 비교 가능한 열거형

Comparable 프로토콜을 준수하는 연관 값만 갖거나 연관 값이 없는 열거형은 Comparable 프로토콜을 채택하면 각 케이스를 비교할 수 있음. 앞에 위치한 케이스가 더 작은 값이 됨

 

enum Condition: Comparable {
    case terrible
    case bad
    case good
    case great
}

let myCondition: Condition = Condition.great
let yourCondition: Condition = Condition.bad

if myCondition >= yourCondition {
    print("제 상태가 더 좋군요")
} else {
    print("당신의 상태가 더 좋아요")
}
/* 제 상태가 더 좋군요 */

enum Device: Comparable {
    case iPhone(version: String)
    case iPad(version: String)
    case macBook
    case iMac
}

var devices: [Device] = []
devices.append(Device.iMac)
devices.append(Device.iPhone(version: "14.3"))
devices.append(Device.iPhone(version: "6.1"))
devices.append(Device.iPad(version: "10.3"))
devices.append(Device.macBook)

let sortedDevices: [Device] = devices.sorted()
print(sortedDevices)
/* [Device.iPhone(version: "14.3"), Device.iPhone(version: "6.1"), Device.iPad(version: "10.3"), Device.iMac] */