열거형(Enumerations)

타입 자체를 한정된 사례(case)안에서 정의할 수 있는 타입

enum Weekday {
    case monday
    case tuesday
    case wednesday
    case thursday
    case friday
    case saturday
    case sunday
}


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

한정된 사례(정해진 갯수) 안에서 정의할 수 있을때

  • 월 / 화 / 수 / 목 / 금 / 토 / 일
  • 동 / 서 / 남 / 북
  • 좌로 정렬 / 가운데 정렬 / 우측 정렬
  • 초등학교 / 중학교 / 고등학교 / 대학교
  • 남 / 여
  • 가위 / 바위 / 보

열거형을 사용하면 코드의 가독성과 안정성이 높아짐 ===> 명확한 분기 처리 가능

var today: Weekday = Weekday.monday

today = .tuesday




// 일요일인 경우만 특정한 처리를 하고 싶을때

if today == .sunday {
   
}


// 모든 각각의 경우를 처리하고 싶을때

if today == .monday {
   print("오늘은 월요일입니다.")
} else if today == .tuesday {
   print("오늘은 화요일입니다.")
} else if today == .wednesday{
   print("오늘은 수요일입니다.")
} else if today == .thursday{
   print("오늘은 목요일입니다.")
} else if today == .friday{
   print("오늘은 금요일입니다.")
} else if today == .saturday{
   print("오늘은 토요일입니다.")
} else {
   print("오늘은 일요일입니다.")
}

/**=====================================**/

//항상 switch문과 연결해서 사용됨

switch today {
case .monday:
   print("오늘은 월요일입니다.")
case .tuesday:
   print("오늘은 화요일입니다.")
case .wednesday:
   print("오늘은 수요일입니다.")
case .thursday:
   print("오늘은 목요일입니다.")
case .friday:
   print("오늘은 금요일입니다.")
case .saturday:
   print("오늘은 토요일입니다.")
case .sunday:
   print("오늘은 일요일입니다.")
}

열거형의 원시값과 연관값

열거형의 원시값은 매칭되는 기본값(정수/문자열)을 정해, 열거형을 좀 더 쉽게 활용 가능 → Int나 String 형태가 보통 많이 쓰이긴 한다.

  • 원시값 입력안하면 0, 1, 2 이렇게 자동으로 저장됨 (정수의 경우 숫자가 하나씩 증가)
  • 1(설정시), 2, 3
  • 0, 2(설정시), 3
enum Alignment1: String {
    case left = "L"
    case center = "C"
    case right = "R"
}


let align = Alignment(rawValue: 0)   // 인스턴스 생성시 - 옵셔널타입으로 리턴 (실패가능)
let leftValue = Alignment.center.rawValue    // 접근연산자를 통해 원시값 자체에도 접근가능



Alignment1(rawValue: "C")     // (가능하지만) 문자열 방식으로는 잘 사용하지는 않음
let centerValue = Alignment1.center.rawValue

원시값의 활용

숫자 또는 문자열과 매칭시켜 자유롭게 활용 가능 (숫자 ←→ 열거형 타입)

  • 논리적으로 nil이 될 수 없다면, 출력은 하지만 문자열은 아님에 주의
enum RpsGame: Int {
    case rock
    case paper
    case scissors
}

RpsGame(rawValue: 0)!
RpsGame(rawValue: 1)
RpsGame(rawValue: 2)


let number = Int.random(in: 0...100) % 3 
print(RpsGame(rawValue: number)!)      // 출력은 하지만 문자열은 아님에 주의




// 옵셔널값을 벗겨서 사용
if let r = RpsGame(rawValue: 0) {
    print(r)       // 출력은 하지만 문자열은 아님에 주의
}
  • 여기서도 optional 값 조심해서 벗겨서 활용하기

열거형의 연관값 (Associated Values)

열거형의 연관값은 구체적인 추가정보를 저장하기 위해 사용

enum Computer {
    case cpu(core: Int, ghz: Double)
    case ram(Int, String)
    case hardDisk(gb: Int)
}

let myChip1 = Computer.cpu(core: 8, ghz: 3.5)
let myChip2 = Computer.cpu(core: 4, ghz: 2.0)

let myChip3 = Computer.ram(16, "DRAM")
let myChip4 = Computer.ram(4, "SRAM")
let myChip5 = Computer.ram(32, "DRAM")

let myChip6 = Computer.hardDisk(gb: 128)
let myChip7 = Computer.hardDisk(gb: 512)

각 케이스별로 상이한 특징이 있고, 그것을 저장 / 활용할 필요가 있을 때

  • 개별케이스마다 저장할 형식을 따로 정의(자료형에 제한이 없음 / 튜플의 형태)
  • 하나의 케이스에 서로다른 연관값을 저장할 수 있음 ===> 선언시점이 아니라, 새로운 열거형 값을 생성할때 저장
switch chip {
case .cpu(core: 8, ghz: 3.1):
    print("CPU 8코어 3.1GHz입니다.")
case .cpu(core: 8, ghz: 2.6):
    print("CPU 8코어 2.6GHz입니다.")
case .ram(32, _):
    print("32기가램 입니다.")
default:
    print("그 이외의 칩에는 관심이 없습니다.")
}
// 연관값을 가진 케이스를 패턴 매칭시키기


switch chip {
case let .cpu(a, b):    // let a = 연관값, let b = 연관값
    print("CPU \(a)코어 \(b)GHz입니다.")
case let .ram(a, _):
    print("램 \(a)기가램 입니다.")
case let .hardDisk(a) :
    print("하드디스크 \(a)기가 용량입니다.")
}

원시값(Raw Values)과 연관값(Associated Values)의 차이

  1. 자료형 선언 방식: 선언하는 위치가 다름
  2. 선언 형식: (1) 원시값 ===> 2가지중 1가지 선택(Int, String) / (2)연관값 ===> 튜플의 형태로 형식 제한 없음
  3. 값의 저장 시점: (원시값: 선언시점 / 연관값: 새로운 열거형 값을 생성할때 )
  4. 서로 배타적: 하나의 열거형에서 원시값과 연관값을 함께 사용하는 것은 불가능 함

열거형과 switch문

열거형에 대한 구체적인 처리는 스위치(switch)문과 함께 쓸 때, 사용성이 높아짐.

// 로그인 타입을 정의

enum LoginProvider: String {      // 3가지로 정의
    case email
    case facebook
    case google
}

let userLogin = LoginProvider.facebook


// 열거형은 한정된 사례로 만든 타입이고,
// 스위치문은 표현식에 대한 분기처리에 최적화

switch userLogin {                   // 3가지로 분기처리
case .email:
    print("이메일 로그인")
case .facebook:
    print("페이스북 로그인")
case .google:
    print("구글 로그인")
}


// 물론 특정한 경우도 처리 가능
if LoginProvider.email == userLogin {
    print("이메일 로그인")
}

옵셔널 타입에 대한 정확한 이해

enum Optional<Wrapped> {     // 제네릭 문법
    case some(Wrapped)
    case none
}

var num: Int? = 7  // 열거형(한정된 사례)
//Optional.some(7)
//Optional.none
  • 열거형 case 패턴을 활용해서, 내부 연관값을 꺼냄
  • print(num!) 아니면 이런식으로 꺼내 써야함.
  • .none과 nil은 완전히 동일 ===> .none은 명시적인 열거형으로 표현한 것이고, 일반적으로는 값이 없다는 의미의 nil키워드를 사용할뿐
switch num {
case .some(let a):  // let a = 7 
    print(a)
case .none:
    print("nil")
}

switch num {
case Optional.some(let a):
    print(a)
case Optional.none:
    print("nil")
}
  • 옵셔널에서는 에러가 나지 않도록 Enum(임시적인) 타입을 담아주는 개념이다.

열거형에 (연관값이 없고), 옵셔널 열거형일 경우

switch문의 편의성 (열거형 case 패턴) - Enumeration Case Pattern

enum SomeEnum {
    case left
    case right
}

// 타입을 다시 옵셔널 열거형으로 선언한다며?
let x: SomeEnum? = .left

SomeEnum?의 의미 [옵셔널 열거형]

  • 값이 있는 경우 .some ===> 내부에 다시 열거형 (1).left / (2).right
  • 값이 없는 경우 .none ===> nil
// 원칙 ===> 이거 말고
switch x {
case .some(let value):      // Optional.some(let value) = Optional.some(SomeEnum.left)  ⭐️
    switch value {
    case .left:
        print("왼쪽으로 돌기")
    case .right:
        print("오른쪽으로 돌기")
    }
case .none:
    print("계속 전진")
}

// 편의적 기능 제공 ===> 이렇게 처리됨!!
switch x {
case .some(.left):
    print("왼쪽으로 돌기")
case .some(.right):
    print("오른쪽으로 돌기")
case .none:
    print("계속 전진")
}

스위치문의 편의성

switch문에서 옵셔널 열거형 타입을 사용할때, 벗기지 않아도 내부값 접근가능

switch x {     // 예전에는 x! 라고 써줘야 했음 (스위치문에서 옵셔널 타입도 ok)
case .left:
    print("왼쪽으로 돌기")
case .right:
    print("오른쪽으로 돌기")
case nil:
    print("계속 전진")
}

열거형에 연관값이 있는 경우

연관값(Associated Values)이 있는 경우와 switch문 (열거형 case 패턴) - Enumeration Case Pattern

  • 연관값이 있는 열거형의 활용 (열거형 case 패턴)
  • 구체적 정보(연관값)를 변수에 바인딩(let/var가능)하는 패턴

열거형 case 패턴

  • case Enum.case(let 변수이름):
  • case let Enum.case(변수이름):

스위치문 뿐만 아니라, 조건문/반복문에서도 활용가능 switch문 (대부분) / if / guard / for-in / while (필요한 경우)


연관값(Associated Values)이 있는 경우 if / guard / for-in / while 문 (열거형 case 패턴)

값 하나를 사용하기 위해서, 스위치문을 전체를 다 써야하는 불편함이 있었음 → 특정 케이스만 다루기 위해서 if문이나 반복문(for문) 사용 가능

chip = Computer.hardDisk(gb: 128)    // 256

// if문에서도 스위치문에서 사용하는 case문과 같이 사용 가능
if case Computer.hardDisk(gb: let gB) = chip {
    print("\(gB)기가 바이트 하드디스크임")
}

if case Computer.hardDisk(gb: let gB) = chip, gB == 256 {    // 처리를 다양하게 활용 가능
    print("256기가 바이트 하드디스크임")
}


//for문
let chiplists: [Computer] = [
    .cpu(core: 4, ghz: 3.0),
    .cpu(core: 8, ghz: 3.5),
    .ram(16, "SRAM"),
    .ram(32, "DRAM"),
    .cpu(core: 8, ghz: 3.5),
    .hardDisk(gb: 500),
    .hardDisk(gb: 256)
]

for case let .cpu(core: c, ghz: h) in chiplists {    // 배열중에서 특정 케이스만 뽑아서 활용 가능 ⭐️
    print("CPU칩: \(c)코어, \(h)헤르츠")
}

// 일반 for문과 비교 =====> 모든 경우
for chip in chiplists {
    print("\(chip)")
}

// 옵셔널 타입을 포함하는 배열에서 반복문을 사용하는 경우
let arrays: [Int?] = [nil, 2, 3, nil, 5]

for case .some(let number) in arrays {
    print("Found a \(number)")
}

옵셔널 패턴(Optional Pattern)

옵셔널 타입에서 열거형 케이스 패턴을 더 간소화한 옵셔널 패턴(Optional Pattern) → 열거형 내부에 “연관값”을 사용시

  1. 열거형 케이스 패턴
  2. 옵셔널 패턴
// 1) 열거형 케이스 패턴 (앞에서 배운)

switch a {
case .some(let z):
    print(z)
case .none:  // nil이라고 써도됨
    print("nil")
}


// 2) 옵셔널 패턴

switch a {
case let z?:      // .some을 조금 더 간소화하는 문법
    print(z)
case nil:         // .none 이라고 써도됨
    print("nil")
}

If문에서도?

if case .some(let x) = num {
    print(x)
}


//옵셔널 패턴 (.some을 ? 물음표로 대체 가능한 패턴)
if case let x? = num {        // "옵셔널(?) 물음표를 띄어내고, x 상수로 보자"의 의미라고 생각하면 됨
    print(x)
}

For문에서도?

// 옵셔널 타입을 포함하는 배열에서 반복문을 사용하는 경우, 옵셔널 패턴을 사용하면 편리함
let arrays: [Int?] = [nil, 2, 3, nil, 5]


// 1) 열거형 케이스 패턴
for case .some(let number) in arrays {
    print("Found a \(number)")
}


// 2) 옵셔널 패턴
for case let number? in arrays {
    print("Found a \(number)")
}

@Unknown 키워드

만약, 열거형의 케이스가 늘어난다면 항상 올바른 처리를 하고 있다고 말할 수 있을까?
=> 처리하는 로직이 항상 옳다는 보장을 하지 못함

  • @unknown 키워드를 default블럭에 추가해둠
  • switch문에서 열거형의 모든 케이스를 다루지 않는 경우, 스위치문에서 모든 열거형의 케이스를 다루지 않았다고 경고를 통해 알려줌 ===> 개발자의 실수 가능성을 컴파일 시점에 알려줌
  • “Switch must be exhaustive”로 알려줌
switch userLogin {
case .email:
    print("이메일 로그인")
case .facebook:
    print("페이스북 로그인")
case .google:
    print("구글 로그인")
@unknown default:
    print("그 외의 모든 경우")
}