Swift 특이점①, 열거형(Enum)
열거형(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가지 선택(Int, String) / (2)연관값 ===> 튜플의 형태로 형식 제한 없음
- 값의 저장 시점: (원시값: 선언시점 / 연관값: 새로운 열거형 값을 생성할때 )
- 서로 배타적: 하나의 열거형에서 원시값과 연관값을 함께 사용하는 것은 불가능 함
열거형과 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) 열거형 케이스 패턴 (앞에서 배운)
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("그 외의 모든 경우")
}