본문 바로가기

학습 노트/Swift (2021)

098 ~ 103. Enumeration (열거형)

Enumeration Types (열거형)

연관된 상수들을 하나의 그룹으로 묶은 자료형을 열거형이라고 한다.
이때 열거형에 포함되게 된 상수들은 상수라고 표현하지 않고 Enumeration Case혹은 줄여서 case라고 부른다.
열거형과 열거형에 포함된 케이스는 독립적인 이름을 가진다.
열거형은 코드의 가독성과 안정성을 증가하기 위해 사용한다.

워드 프로세서를 만든다고 가정했을 때 문단 정렬을 위해 다음과 같이 선언한다.
왼쪽 정렬은 0, 가운데 정렬은 1, 오른쪽 정렬은 2이다.

let left = 0
let center = 1
let right = 2

var alignment = center1
결과

1

가운데 정렬을 선택했을 때 동작은 정상적으로 가능하지만 결과인 1이 정확히 무엇을 정의하는지 알기 어렵다.
또한 각각의 상수에 다른 값을 저장해도 오류가 발생하지 않고 그대로 잘못된 값을 전달한다.

let left = 0
let center = 30
let right = 2

var alignment = center
결과

30

코드 안정성이 나쁘다고 평가할 수 있다.
이 코드를 조금씩 개선해 보도록 하자

let left2 = "left"
let center2 = "center"
let right2 = "right"

var alignment = center
결과

center

결과를 보고 어떤 기능을 하는지 직관적으로 알 수 있다.
하지만 오타의 위험성이 있다.

let left2 = "left"
let center2 = "center"
let right2 = "right"

var alignment = center

if alignment == "Center" {
}
결과

Center

또한 문자열은 대소문자를 구별하기 때문에 사전에 약속이 되어 있지 않다면 비교에 부적합하다.

 

Syntax

enum TypeName {
    case caseName
    case caseNamecaseName
}

 

enum Alignment {
	case left
	case right
	case center
}

Alignment.center
결과

center

Alignment를 입력하면 자동완성에 속성처럼 포함된 케이스를 알려준다.
또한 결과에서도 center를 출력해 주므로 용도가 명확해진다.

케이스의 이름을 정할 때는 열거형과 관련된 중복된 단어를 제거하고 최대한 단순하게 작성한다.
또한 열거형의 이름은 UpperCamelCase, 케이스의 이름은 lowerCamelCase로 작성하는 것이 관례이다.

enum Alignment {
	case left
	case right
	case center
}

//-----

var textAlignment = Alignment.center

textAlignment = .left
결과

center
left

열거형은 '값;의 취급을 받는다.
따라서 코드와 같이 textAlignment에 한 번 저장된 열거형을 통해 이후 다른 케이스로 전환할 수도 있다.
이때 열거형의 이름은 생략해도 괜찮지만 '.'을 생략해서는 안된다.

enum Alignment {
	case left
	case right
	case center
}

var textAlignment = Alignment.center

textAlignment = .left

//-----

textAlignment = .textleft

//-----

textAlignment = .Left
결과

//error

//-----

//error

존재하지 않는 다른 속성을 입력해도, 대문자로 입력해도 error를 출력한다.
따라서 원래의 코드에 비해 안정성이 증가했다.

열거형을 비교하는 방법으로 if문과 switch문을 활용할 수 있다.

enum Alignment {
	case left
	case right
	case center
}

var textAlignment = Alignment.center

switch textAlignment {
	case .left:
		print("left")
	case .right:
		print("right")
	case .center:
		print("center")
}

if textAlignment == .center {
	print("center")
} else if textAlignment == .right {
	print("right")
} else {
	print("left")
}
결과

center
center

열거형의 값에 따라 분기하여 원하는 동작을 수행할 수 있다.

 

Raw Values (원시값)

열거형의 케이스들은 각각 독립적인 이름을 갖지만 이 안에 값을 저장할 수 있다.
이를 raw value라고 하며 원시 값이라고 부른다.

직접 생성하는 열거형에 원시값이 포함되는 경우는 드물지만,
이미 구현되어 있는 프레임워크에선 흔하게 보이기도 한다.

 

Syntax

enum TypeNameRawValueType {
    case caseName = Value
}

 

enum Alignmnet1 {
	case left
	case right
	case center
}

enum Alignmnet2: Int {
	case left
	case right = 100
	case center
}

따라서 위와 같이 원시 값이 없어도 열거형은 정상적으로 만들어지며, 사용할 수도 있다.
열거형의 이름 뒤에 자료형을 명시해 원시 값의 자료형을 선언한다.
이때 위의 코드와 같이 명시적으로 저장하지 않는다면, 컴파일러가 임의의 값으로 자동으로 저장한다.

enum Alignmnet1 {
	case left
	case right
	case center
}

enum Alignmnet2: Int {
	case left
	case right = 100
	case center
}

//-----

Alignmnet2.left.rawValue
Alignmnet2.right.rawValue
Alignmnet2.center.rawValue
결과

0
100
101

가장 먼저 생성된 left는 0으로 자동 저장되었고,
100 이후로 생성된 center는 순차적으로 101이 저장되었다.

이런 원시 값은 선언 이후엔 변경할 수 없다는 것을 주의하자.

enum Alignmnet2: Int {
	case left
	case right = 100
	case center
}

Alignmnet2.left.rawValue = 10
결과

//error

열거형의 생성자를 통해 원시 값을 전달하면 해당 케이스를 반환할 수 있다.

enum Alignmnet2: Int {
	case left
	case right = 100
	case center
}

Alignmnet2(rawValue: 0)
Alignmnet2(rawValue: 200)
결과

left
nil

이때 존재하지 않는 원시 값을 전달하는 경우 nil이 반환되는 것으로 생성자의 반환 값이 optional 형태인 것을 알 수 있다.

enum Weekday: String {
	case sunday
	case monday = "MON"
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

Weekday.sunday.rawValue
Weekday.monday.rawValue
Weekday.tuesday.rawValue
결과

sunday
MON
tuesday

만약 원시 값의 자료형을 문자열로 생성하고, 명시적으로 지정하지 않는다면,
케이스의 이름과 동일하게 원시 값을 자동 생성한다.

enum ControlChar: Character {
	case tab = "t"
	case newLine
}
결과

//error

하지만 이는 캐릭터에는 적용되지 않는다.
캐릭터를 원시 값의 자료형으로 생성한 경우, 원시 값을 생략할 수 없음에 주의하자.

 

Associated Values (연관값)

연관 값은 열거형의 케이스에 연관된 값을 함께 저장한다.

 

Syntaxt

enum TypeName {
    case caseName(Type)
    case caseName(TypeType, ...)
}

 

enum VideoInterface1: String {
	case dvi = "1028x768"
	case hdmi = "2048x1536"
	case displayPort = "2840x2160"
}

enum VideoInterface2 {
	case dvi(width: Int, height: Int)
	case hdmi(Int, Int, Double, Bool)
	case displayPort(CGSize)
}

이러한 특징은 앞서 정리한 원시 값과 비슷한데 결정적으로 값의 형식에 제한이 없다는 점이다.

원시값 연관값
Int
String
Character
제한 없음
추출용 코드가 필요함 필요 없음
하나만 저장해야 됨 복수로 저장할 수 있음
enum VideoInterface {
	case dvi(width: Int, height: Int)
	case hdmi(Int, Int, Double, Bool)
	case displayPort(CGSize)
}

//-----

var input = VideoInterface.dvi(width: 2048, height: 1536)

switch input{
case .dvi(2048, 1536):
	print("dvi 2048 x 1536")
case .dvi(2048, _):
	print("dvi 2048 x any")
case .dvi:
	print("dvi")
case .hdmi(let width, let height, let version, var audioEnabled):
	print("hdmi \(width)x\(height)")
case let .displayPort(size):
	print("dp")
}

 

결과

dvi 2048 x 1536

모든 연관 값을 비교하거나, 연관 값 중 일부와 비교하거나, 케이스로만 비교하거나, value binding 하거나, 열거형 케이스 매칭을 하는 등
원시 값과는 비교가 안 될 정도로 다양한 경우에 대처하는 것이 가능하다.
만약 dvi로 2048*1536 해상도의 모니터를 연결했다고 가정하면 코드와 같이 dvi 케이스의 여러 경우중 하나로 분기할 수 있다.

 

Enumeration Case Pattern

switch-case 문과 같은 조건문에서 나열형의 연관 값을 사용하는 방법이다.

 

Syntax

case Enum.case(let name):
case Enum.case(var name):
case let Enum.case(name):
case var Enum.case(name):

 

enum Transportation {
	case bus(number: Int)
	case taxi(company: String, number: String)
	case subway(lineNumber: Int, express: Bool)
}

교통수단을 나타내는 열거형을 생성한다.
bus에는 노선 번호를, taxi에는 사명과 번호를, subway에는 노선 번호와 급행 여부를 전달한다.

enum Transportation {
	case bus(number: Int)
	case taxi(company: String, number: String)
	case subway(lineNumber: Int, express: Bool)
}

//-----

var tpt = Transportation.taxi(company: Kakao, number: 3388)

switch tpt {
case .bus(let n):
	print(n)
case .taxi(let c, var n):
	print(c, n)
case let .subway(l, e):
	print(l, e)
}
결과

Kakao 3388

변수, 상수의 선언은 전달되는 모든 연관 값이 같을 경우 괄호의 밖에서 선언하는 것이 효율적이다.

enum Transportation {
	case bus(number: Int)
	case taxi(company: String, number: String)
	case subway(lineNumber: Int, express: Bool)
}

//-----

var tpt = Transportation.subway(lineNumber: 1, express: true)

if case let .subway(1, express) = tpt {
	if express {
		print("express")
	} else {
		print("wrong")
	}
}
결과

express

if문에서는 case를 먼저 써 주고, let이나 var가 사용되는데 밸류 바인딩을 사용하지 않는다면 생략한다.
위의 경우 급행 여부를 나타내는 express를 밸류바인딩 하므로 let을 사용했다.
따라서 완성된 코드는 1호선인지 확인하고, 1호선이라면 if문 안에서 급행여부를 판단하는 코드가 된다.

만약 복수의 연관 값 중 하나만 비교하고자 한다면 wildcard pattern을 사용한다.

enum Transportation {
	case bus(number: Int)
	case taxi(company: String, number: String)
	case subway(lineNumber: Int, express: Bool)
}

//-----

var tpt = Transportation.subway(lineNumber: 9, express: true)

if case .subway(_, true) = tpt {
	print("express")
}
결과

express

노선 번호를 wildcard로 무시해 노선에 관계없이 급행 여부만 판단하도록 했다.
또한 밸류 바인딩을 사용하지 않아 let, var 키워드도 생략됐다.

enum Transportation {
	case bus(number: Int)
	case taxi(company: String, number: String)
	case subway(lineNumber: Int, express: Bool)
}

//-----

let list = [
	Transportation.subway(lineNumber: 2, express: false),
	Transportation.bus(number: 4412),
	Transportation.subway(lineNumber: 7, express: true),
	Transportation.taxi(company: "SeoulTaxi", number: "1234")
]

for case let .subway(n, _) in list {
	print("subway \(n)")
}

for case let .subway(n, true) in list {
	print("express train \(n)")
}

for case let .subway(n, true) in list where n == 7 {
	print("subway \(n)")
}

 

결과

subway 2
subway 7
express train 7
subway 7

for-in 반복문에서도 wildcard pattern으로 특정 연관 값을 무시할 수 있다.
또한 where 절을 붙임으로써 추가적인 조건을 추가할 수 있다.

 

Caselterable

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

요일을 저장한 열거형을 선언한다.
원시 값의 자료형은 Int이다.

무작위로 하루를 고르는 코드가 필요하다고 가정한다.

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

//-----

let rnd = Int.random(in: 0...6)
Weekday(rawValue: rnd)
결과

wednesday

코드를 반복 실행 시 결과는 계속 달라진다.
배열의 경우 random() method에 count를 전달하면 되는 문제지만, 열거형은 그러한 기능을 제공하지 않기 때문에 위와 같이 범위를 직접 전달한다.
열거형의 범위와 method의 범위가 일치하기 때문에 코드는 정상적으로 작동이 되지만, 잘못된 범위를 전달할 경우 rnd의 값이 얼마건 간에 nil을 반환한다.

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

//-----

let rnd = Int.random(in: 0...7)
Weekday(rawValue: rnd)
결과

4
nil
enum Weekday: Int {
	case sunday = 100
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

//-----

let rnd = Int.random(in: 0...7)
Weekday(rawValue: rnd)
결과

6
nil
enum Weekday2: Int, CaseIterable {
	case sunday
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

 

위와 같이 CaseIterable 속성을 사용하면 모든 케이스를 배열로 반환하는 속성이 자동으로 적용된다.

enum Weekday2: Int, CaseIterable {
	case sunday
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

//-----

let rnd2 = Int.random(in: 0...Weekday2.allCases.count)
Weekday2(rawValue: rnd)
결과

7
nil

따라서 위에서 사용할 수 없었던 배열의 count 기능을 사용할 수 있다.
이에는 몇 가지 이점이 있는데, 원시 값의 수를 정확히 해아리지 않아도 된다는 점과 이에 따른 유지보수의 용이성 증가가 있다.
하지만 케이스의 범위가 원시값의 범위와 일치하지 않을 수 있다.

enum Weekday2: Int, CaseIterable {
	case sunday = 100
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

이렇게 케이스의 범위는 0~6이지만 원시값의 범위는 0~100으로 큰 차이가 난다.

enum Weekday2: Int, CaseIterable {
	case sunday = 100
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

//-----

let rnd2 = Int.random(in: 0...Weekday2.allCases.count)
Weekday2(rawValue: rnd)
결과

0

이 때는 범위를 잘못 전달했을 때와 같은 결과를 반환한다.
CaseIterable 속성을 사용한 열거형에 배열의 count method를 사용할 수 있었던 것처럼 여러 속성을 사용할 수 있다.

enum Weekday2: Int, CaseIterable {
	case sunday = 100
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

//-----

Weekday2.allCases.randomElement()
결과

saturday

코드도 훨씬 단순해지고, 동작도 의도한 대로 하는 것을 확인할 수 있다.
또한 열거형의 케이스 확인에도 용이하다.

enum Weekday2: Int, CaseIterable {
	case sunday = 100
	case monday
	case tuesday
	case wednesday
	case thursday
	case friday
	case saturday
}

//-----

for w in Weekday2.allCases {
	print(w)
}
결과

sunday
monday
tuesday
wednesday
thursday
friday saturday

 

Nonfrozen Enumeration

enum ServiceType {
	case onlineCourse
	case offlineCamp
}

let selectedType = ServiceType.onlineCourse

switch selectedType {
case .onlineCourse:
	print("send online course email")
case .offlineCamp:
	print("send offline camp email")
}
결과

send online course email

ServiceType 열거형에는 onlineCourse, offlineCamp 두 가지 코스가 등록되어있다.
누군가가 선택했다고 가정하는 selectedType에는 onlineCourse가 저장되어 있다.
가장 아래의 switch 문에는 사용자의 선택에 따른 분기가 정의되어 있다.

지금은 열거형의 모든 케이스의 가능성을 고려하여 switch문에서 모든 분기가 정의되어 있다.
하지만 아래와 같이 모두 고려하지 않는다면 코드는 동작하지 않는다.

enum ServiceType {
	case onlineCourse
	case offlineCamp
}

let selectedType = ServiceType.onlineCourse

switch selectedType {
case .onlineCourse:
	print("send online course email")
}
결과

//error

따라서 Switch문은 열거형이 가진 모든 케이스에 대응해야 한다는 결론을 내릴 수 있다.

이러한 문제는 유지보수 간에 쉽게 발생한다.
이후에 새로운 선택지를 추가했다고 가정해 보자.

enum ServiceType {
	case onlineCourse
	case offlineCamp
	case onlineCamp
	case seminar
}

let selectedType = ServiceType.onlineCourse

switch selectedType {
case .onlineCourse:
	print("send online course email")
case .offlineCamp:
	print("send offline camp email")
}
결과

//error

이런 식으로 열거형을 생성한 이후 케이스를 추가할 수 있는 열거형을 Nonfrozen Enumeration이라고 부른다.
반대의 경우인 Frozen Enumeration도 존재하지만 다음에 언급한다.

이렇게 새로운 케이스가 추가된 경우 switch문에서도 똑같이 대응하거나

enum ServiceType {
	case onlineCourse
	case offlineCamp
	case onlineCamp
	case seminar
}

let selectedType = ServiceType.onlineCourse

switch selectedType {
case .onlineCourse:
	print("send online course email")
case .offlineCamp:
	print("send offline camp email")
case .onlineCamp:
	print("send online camp email")
case .seminar:
	print("send seminar email")
}
결과

send online course email

기본값을 설정해 위와 같은 오류를 미리 방지할 수 있다.

enum ServiceType {
	case onlineCourse
	case offlineCamp
	case onlineCamp
	case seminar
}

let selectedType = ServiceType.onlineCourse

switch selectedType {
case .onlineCourse:
	print("send online course email")
case .offlineCamp:
	print("send offline camp email")
case .onlineCamp:
	print("send online camp email")
@unknown default:
	break
}
결과

send online course email

switch문이 모든 케이스에 대응하지 않았다는 경고를 표시해 편리해진다.

 


Log

2021.09.05.
블로그 이전으로 인한 글 옮김 및 수정