본문 바로가기

문법

[Swift]Enumerations 개념정리 및 활용법 알아보기

주의 : 영문서 번역 중 오역이 있을 수 있으며 정확하지않는 정보가 있을 수 있습니다.

 

카테고리
- 열거형의 선언 및 변수생성과 값변경
- 열거형의 원시값 지정과 사용
- 원시값을 통한 열거형 초기화
- 연관 값을 갖는 열거형
- 항목순회
- 플랫폼별로 사용하는 조건 추가
- 연관값을 갖는 열거형의 항목 순회
- 순환 열거형
- 비교가능한 열거형
- 포인트정리 및 궁금증

 

열거형

 

연관된 항목들을 묶어 표현할 수 있는 타입이다. 열거형은 배열이나 딕셔너리 같은 타입과 다르게 프로그래머가 정해준 항목 값 외에는 추가/ 수정이 불가하다. 고로 딱 정해진 값만 열거형 값에 속할 수 있다.

 

열거형은 다음과 같은 경우에 요긴하게 사용할 수 있다.

- 제한된 선택지를 주고 싶을때

- 정해진 값 외에는 입력받고 싶지않을때

- 예상된 입력값이 한정되어 있을때

 

생활에서도 열거형으로 묶을 수 있는 항목들을 발견해 볼 수 있다.

 

무선통신방식 : WiFi, 블루투스, LTE, 3G, 기타

학생 : 초등학생, 중학생, 고등학생, 대학생

지역 : 강원도, 경기도, 경상도, 전라도, 제주도, 총청도

색상 : 빨간색, 노란색, 주황색, 파란색, 보라색, 검은색, 흰색

 

기존 C언어 등에서 열거형은 주로 정수 타입값의 별칭 형태로 사용이 될 뿐이었다. 그렇기때문에 모든 열거형의 데이터 타입은 같은 타입(주로정수타입) 으로 취급한다. 이는 열거형 각각이 고유한 타입으로 인식될 수 없다는 문제 때문에 여러 열거형을 사용할때 프로그래머의 실수로 인한 버그가 생길 수도 있었다. 그러나 스위프트의 열거형은 각 열거형의 고유의 타입으로 인정되기때문에 실수로 버그가 일어날 가능성을 원천 봉쇄할 수 있다.

 

물론 열거형 각 항목이 원시값(rawValue)이라는 형태로 실제 값을 가질 수도 있다. 또는 연관값(Associated Value)을 사용하여 공용체로 불리는 값의 묶음도 구현할 수 있다.

 

열거형은 switch구문과 만났을때 멋지게 활용해 볼 수 있다. 이번 열거형 파트에서는 열거형에 관한 기본문법을 알아보자.

 

열거형의 선언 및 변수생성과 값변경

// MARK: 열거형 선언 및 변수생성과 값변경
enum School {
    case primary
    case elementary
    case middle
    case high
    case college
    case university
    case graduate
}
// 위 코드와 정확히 같은 표현이다.
enum SecondSchool{
    case primary, elementary, middle, high, college, university, graduate
}

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

// 같은 타입인 School 내부의 항복으로만 highestEducationLevel의 값을 변경해 줄 수 있습니다.
highestEducationLevel = .graduate

 

열거형의 원시값 지정과 사용

 

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

// MARK: 얼거형의 원시값 지정 과 사용

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

let highestEducationLevel: 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 = .university
print("저의 최종학력은 \(highestEducationLevel.rawValue) 입니다.")
// 저의 최종학력은 university 입니다.

print(School.university.rawValue)
// university, (rawValue가 없기때문에 연관값을 출력함)

enum Numbers: Int {
    case zero, one, two, eight = 8
}

print("\(Numbers.zero.rawValue), \(Numbers.one), \(Numbers.two.rawValue), \(Numbers.eight)")
// 0, one, 2, eight

 

중간 포인트 정리 

- enum은 원시값(rawValue)이라는 프로퍼티를 가질 수 있다. 타입지정시 enum옆에 : String 이렇게 지정해줄 수 있다.

- 원시값을 반드시 다 지정하지 않아도 된다.

- enum은 인스턴스 생성하지않고도 Numbers.zero.rawValue이렇게 값에 접근 할 수 있다.

- Numbers case내부에서 eight만 원시값을 8지정해주었는데 컴파일러가 자동으로 case 순서대로 0부터 적용한다.

   ex) zero = 0, one = 1, two = 2, eight = 8

   만일 eight뒤에 case nine이 존재하고 원시값을 없을경우엔 ? 그 전의 수를 8로 지정해줬으니 nine은 자동으로 9라는 원시값을 갖게된     다. 

 

 

원시값을 통한 열거형 초기화

// MARK: 원시값을 통한 열거형 초기화
let primary = School(rawValue: "유치원")
let graduate = School(rawValue: "대학원")
print("\(primary?.rawValue)\n\(graduate?.rawValue)")
// 유치원은 값이 있지만 대학원이라는 원시값은 존재하지않기때문에 'Optional("유치원")'과 'nil'결과를 출력한다.
let one = Numbers(rawValue: 8)
let three = Numbers(rawValue: 0)

print("\(one?.rawValue)\n\(three?.rawValue)")
// 'nil', 'Optional(0)' 값을 출력

 

중간 포인트 정리 

 

- enum을 인스턴스 초기화하면 값이 옵셔널 랩핑이된다. 이유는 추측해보기론..(enum자체가 옵셔널 유형으로 설계돼있어 그런것같음), 고로 출력이 optional(0), optional("유치원") 이렇게 나오는것. 

- 존재하지 않는 rawValue값으로 초기화를 할경우 nil을 반환한다.

- case는 항목이라고 칭한다.

- primary? 에서 물음표는 값이 있어 ? 라고 물으는 옵셔널체이닝인데 값이 존재할경우 rawValue를 반환하고 존재하지 않으면 nil을 반환함

 

 

연관 값을 갖는 열거형

스위프트의 열거형 각 항목이 연관값을 가지게되면 기존 프로그래밍 언어의 공용체 형태를 띌 수 도 있다.

열거형 내 항목(case)이 자신과 연관된 값을 가질 수 있다. 연관 값은 각 항목 옆에 소괄호로 묶어 표현

할 수 있다. 다른 항목이 연관값을 갖는다고 모든항목이 연관값을 가질 필요는 없다.

 

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

var dinner: MainDish = .pasta(taste: "크림")
dinner = .pizza(dough: "치즈크러스트", topping: "불고기")
print("\(dinner)")
// pizza(dough: "치즈크러스트", topping: "불고기")
dinner = .chicken(withSauce: true)
print("\(dinner)")
// chicken(withSauce: true)
dinner = .rice
print("\(dinner)")
// rice
print(MainDish.pizza(dough: "치즈크러스트", topping: "불고기"))
// pizza(dough: "치즈크러스트", topping: "불고기")

 

식당의 재료가 한정적이라 파스타의 맛과 피자의 도우, 토핑 등을 특정 메뉴로 한정 지으려면 열거형으로 바꾸면 된다.

 

enum PastaTaste {
    case cream, tomato
}

enum PizzaDough {
    case cheeseCrust, thin, original
}

enum PizzaTopping {
    case pepperoni, cheease, bacon
}

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

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

var dinner2: MainDish = .pasta(taste: .tomato)
dinner2 = .pizza(dough: .cheeseCrust, topping: .bacon)


dinner1 과 dinner2는 표현방식의 차이지 값은 똑같다. 개인적으로 줄이는걸 선호하지만 타입을 명시함으로써
코드를 읽을때 어느타입에 속해있는지 명확히 알 수 있으니 타입을 명시해주는게 더 가독성에 좋을 수 있겠다고 생각한다.

 

 

항목순회

우리는 때때로 열거형에 포함된 모든 케이스를 알아야할 때가 있다. 그럴때 열거형의 이름뒤에 콜론 (:) 을 작성하고 한칸을 띄운뒤

CaseIterable 프로토콜을 채택하면 열거형에 allCases라는 이름의 타입 프로퍼티를 통해 모든케이스의 컬렉션을 생성해준다.

(case들의 항목들을 배열컬렉션으로 만들어줌)

 

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

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

allCases.forEach({ print($0) })
//primary
//elementary
//middle
//high
//college
//university
//graduate

allCases.forEach({ print($0.rawValue) })
//유치원
//초등학교
//중학교
//고등학교
//대학
//대학교
//대학원

궁금증

- allCases 상수를 바로 print하면 배열 전체가 출력이되는데 옵셔널 랲핑이되어있다고 해도 파일명을 출력할 필요는 없는것같다.

   현재 teset이라는 파일을 생성해서 그안에 CommandLine Tool에서 코드를 작성한건데 왜 코드를 출력하다말고 파일명을 출력하는것     일까 ?

- forEach는 반복문을 도와주는 고차함수인데 옵셔널 랩핑을 벗겨주고 반복의 기능이있기때문에 allCases에 rawValue와 case들을        출  력해줄 수 있는것같다.

 

 

플랫폼별로 사용하는 조건 추가

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)

//[teset.School.primary, teset.School.elementary, teset.School.middle,
//teset.School.high, teset.School.college, teset.School.university, 
//teset.School.graduate]

위 처럼 @available  속성을 통해 특정 케이스를 플랫폼에 따라 사용할 수 있거나 없는 경우 가 생기면 CaseIterable프로토콜을

채택하는것 만으로는 allCases프로퍼티를 사용할 수 없다. 그럴때는 직접 allCases 프로퍼티를 구현해 주어야 합니다. 이렇게 또

있는데, 바로 열거형의 케이스가 연관값을 갖는 경우이다.

 

 

연관값을 갖는 열거형의 항목 순회

enum PastaTaste: CaseIterable {
    case cream, tomato
}

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

enum PizzaTopping: CaseIterable {
    case pepperoni, cheease, bacon
}

enum MainDish: CaseIterable {
    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)
print(MainDish.allCases)

출력결과

처음 열거형을 정의하고 allCases를 구현한 이후에 케이스를 추가할 일이 생겼다면 꼭 잊지말고 allCases를 다시 살펴봐야한다.

 

 

순환 열거형

순환 열거형은 열거형 항목의 연관값이 열거형 자신의 값이고자 할때 사용한다.

순환 열거형을 명시하고 싶다면 indirect 키워드를 사용하면된다. 특정항목에만 한정하고싶다면

case 키워드 앞에 indirect를 붙이면 되고, 열거형 전체에 적용하고 싶다면 enum 키워드 앞에 indirect를 붙이면 됩니다.

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

 

열거형에는 정수를 연관 값으로 갖는 number라는 항목이 있고 덧셈을 위한 addition이라는 항목, 곱셈을 위한 multiplication 항목이 있습니다. ArithmeticExpression 열거형을 사용하여 (5+4)x2연산을 구현해보는 예제를 시도해보자.

 

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 = 11

중간 포인트 정리 

 

- switch case에서 상수를 생성할 수 있었던건 ArithmeticExpression내 case number가 (Int타입을 가지고 있었기때문)?

 

 

비교가능한 열거형

Comparable 프로포콜을 준수하는 연관값만 갖거나 연관값이 없는 열거형은 Comparable프로토콜을 채택하면 각 케이스를 비교할 수 있다.

point - 앞에 위치한 케이스가 더 작은 값이 됩니다.

 

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)

//[WhatIsTheEnumeration.Device.iPhone(version: "14.3"), 
// WhatIsTheEnumeration.Device.iPhone(version: "6.1"), 
// WhatIsTheEnumeration.Device.iPad(version: "10.3"), 
// WhatIsTheEnumeration.Device.macBook, 
// WhatIsTheEnumeration.Device.iMac]

 

포인트 정리 종합 

- enum은 case의 고유한 값과 연관값을 가질 수 있고 추가로 원시값(rawValue)이라는 프로퍼티를 가질 수 있다. 원시값의 타입을 지정      할  때는 enum옆에 : String 이렇게 지정해줄 수 있다.

- 항목들에 대한 원시값을 반드시 다 지정하지 않아도 된다.

- enum은 인스턴스 생성하지않고도 Numbers.zero.rawValue이렇게 값에 접근 할 수 있다.

- Numbers case내부에서 eight만 원시값을 8지정해주었는데 컴파일러가 자동으로 case 순서대로 0부터 적용한다.

   ex) case zero = 0, one = 1, two = 2, eight = 8

   만일 eight뒤에 case nine이 존재하고 원시값을 없을경우엔 ? 그 전의 수를 8로 지정해줬으니 nine은 자동으로 9라는 원시값을 갖게된     다. 

- enum을 인스턴스 초기화하면 값이 옵셔널 랩핑이된다. 이유는 추측해보기론..(enum자체가 옵셔널 유형으로 설계돼있어 그런것같음), 고로 출력이 optional(0), optional("유치원") 이렇게 나오는것. 

- 존재하지 않는 rawValue값으로 초기화를 할경우 nil을 반환한다.

- case는 항목이라고 칭하고 case name(let String)이렇게 괄호안에 변수나 타입이들어간결 연관값이라고 할 수 있다

- primary? 에서 물음표는 값이 있어 ? 라고 물으는 옵셔널체이닝인데 값이 존재할경우 rawValue를 반환하고 존재하지 않으면 nil을 반환함

 

 

궁금증

- allCases 상수를 바로 print하면 배열 전체가 출력이되는데 옵셔널 랲핑이되어있다고 해도 파일명을 출력할 필요는 없는것같다.

   현재 teset이라는 파일을 생성해서 그안에 CommandLine Tool에서 코드를 작성한건데 왜 코드를 출력하다말고 파일명을 출력하는것     일까 ?

 

[수정전] forEach는 반복문을 도와주는 고차함수인데 옵셔널 랩핑을 벗겨주고 반복의 기능이있기때문에 allCases에 rawValue와 case들을  출력해줄 수 있는것같다.

[수정 후] forEach에는 옵셔널래핑을 벗겨주는 기능은없고 enum rawValue로 인스턴스화를 할때 옵셔널랩핑이 되며 그외 인스턴스화 하지않고 값을 사용하게될 경우에는 옵셔널랩핑이 되지않는다.

 

[수정 전] 순환열거형에서,, switch case에서 상수를 생성할 수 있었던건 ArithmeticExpression내 case number가 (Int타입을 가지고 있었기때문)?

[수정 후] case의 연관값에 (Int)타입이 지정되야만 실제로 switch문을 활용할때 .number(let value)이렇게 연관값을 생성하여 사용할 수 있는것이고 연관 타입이 존재하지않고  case만존재한다면 별도의 연관값 let value같은건 사용할 수 없다.

 

참고책 - 야곰 Swift프로그래밍 3판