Goal
- Swift의 Enum을 이해한다.
- Enum을 활용할 수 있다.
들어가기에 앞서
- 본 글은 Swift의 언어가이드를 따릅니다.[1]
- 문법작성규칙은 아래와 같이 작성합니다.[2]
- lower camel case: method, variable, constant
- upper camel case: enum, struct, class, extension
Enum
Enumerations, 열거형이라고 불리우며 공통된 유형의 값 항목을 묶어 표현할 수 있게 해주는 타입입니다.
덕분에, 코드 내에서 type-safe 방식의 값으로 작업할 수 있도록 합니다.
C언어에 익숙하신 분은 C의 열거형이 정수 값 한정으로 관련 이름을 할당한다는 것을 아실 겁니다.
스위프트의 열거형은 훨씬 더 유연하며, 열거의 각 경우에 값을 제공할 필요가 없습니다. 각 열거 항목에 대해 값(원시 값)이 제공된 경우 값은 문자열, 문자, 정수 또는 부동소수점 유형의 값이 될 수 있습니다.
열거형 구문 (Enum Syntax)
enum
키워드와 중괄호로 묶어 열거형을 선언할 수 있습니다.
1
2
3
enum School {
}
다음은 나침반의 네 방향으로 열거형을 정의하는 예시입니다.
이렇게 각 비슷한 항목들을 열거형으로 정의하여 보다 깔끔하게 코드를 작성할 수 있습니다.
Point: 열거형 형식은 복수명이 아닌 단수형으로 만드는 것이 좋습니다.[3]
1
2
3
4
5
6
enum CompassPoint {
case north
case south
case east
case west
}
case를 여러 번 사용하지 않고 한 줄로 처리할 수도 있습니다.
1
2
3
enum School {
case elementary, middle, high
}
enum의 인스턴스를 생성할 때 Enum.항목명
형식으로 사용할 수 있습니다.
또는, 자료형을 미리 선언하게 되면 타입추론으로 .항목명
형식으로 사용할 수 있습니다(Enum을 반복하여 쓰지 않아도 됩니다).
1
2
3
4
5
6
7
8
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
var planet = Planet.earth
var mars: Planet = .mars // 타입 추론
planet = .venus // earth를 venus로 변경, 타입이 추론됨
Switch 문법으로 열거형 값 매칭하기
각각의 열거형 항목을 switch 구문을 이용해 상태를 나누어 코드를 작성할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum School {
case elementary, middle, high
}
let school: School = .high
switch school {
case .elementary:
print("Elementary school")
case .middle:
print("Middle school")
case .high:
print("High school")
}
// Prints "High school"
또는, default 구문을 사용하여 특정 항목 외 나머지들을 처리해 줄 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
enum School {
case elementary, middle, high, college
}
let school: School = .high
switch school {
case .college:
print("Adult")
default:
print("teenager")
}
// Prints "High school"
각 항목의 반복(Iterating over Enumeration Cases)
열거형을 사용할 때, 각 항목에 대한 모든 케이스를 이용할 때가 있습니다. 그럴 때 열거형 이름 다음에 : CaseIterable
를 작성하면 allCases속성을 이용하여 각 항목을 순회할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
enum School: CaseIterable {
case elementary, middle, high
}
let school: [School] = School.allCases
print(school)
// Prints "[.elementary, .middle, .high]"
print(school.count)
// Prints "3"
이를 for-in
구문을 이용하여 반복할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
enum School : CaseIterable {
case elementary, middle, high
}
for school in School.allCases {
print(school)
}
// elementary
// middle
// high
Point: allCases를 사용하기 위해서는 CaseIterable을 준수(conform)해야 합니다.[4]
연관 값(Associated Values)
열거형은 단순히 자신의 항목만 사용할 뿐 아니라, 각 항목에 대한 다른 유형의 값을 추가로 저장하여 사용할 수 있습니다.
만약 앱이 서버와 통신하고 있고, 어떠한 요청을 보내고 응답을 기다린다고 가정해봅시다. 이를 연관 값을 이용하여 다음과 같이 구현할 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
enum Error {
case decoding
case encoding
case response
}
enum ProcessResult {
case success(value: String)
case error(problem: Error)
}
// some codes..
// HELLO가 응답으로 들어옴
var result: ProcessResult = .success(value: "HELLO")
// 응답 에러가 뜸
var otherResult = ProcessResult.error(problem: .response)
// decoding 에러가 뜸
var anotherResult: ProcessResult = .error(problem: .decoding)
원시 값(Raw Values)
원시값은 연관 값(Associated Values)과는 다르게 항목에 대해 동일한 유형의 값으로 선언해줄 수 있습니다.
원시 값을 적용하고 싶을 때 열거형 이름 다음에 : (Type)
을 작성하면 됩니다. 원시 값은 문자열, 문자 또는 정수 또는 실수 타입일 수 있습니다.
다음은 항목에 대해 ASCII 값을 저장하는 예시입니다.
1
2
3
4
5
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
여기서 ASCIIControlCharacter라는 열거형의 원시 값은 Character 타입으로 정의되었습니다.
그리고 일반적인 ASCII 제어 문자를 각 항목의 이름에 맞게 설정했습니다. 문자열 및 문자에 대한 설명은 여기를 참조해주세요.
암시적 할당
원시 값이 정수형이거나 문자열일 경우 각 항목마다 명시적으로 선언할 필요는 없습니다. 선언하지 않으면, 스위프트가 자동으로 값을 할당합니다.
예를 들어, 정수형의 원시 값을 사용하는 경우, 각 항목의 원시 값은 이전 항목의 값에서 1씩 증가한 값이 됩니다. 첫 번째 항목에 대한 선언이 없다면 자동으로 0이 할당됩니다.
아래 예시는 태양으로부터 각 행성의 순서를 나타내는 열거형입니다.
1
2
3
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
위 예시에서 mercury의 초기값을 1로 두었기 때문에, 그 이후의 항목들에 대해서는 자동으로 1씩 증가한 값으로 할당됩니다.
Planet.venus의 원시 값은 2가 되는 것이죠.
만약 원시 값이 문자열일 경우, 각 항목의 원시 값은 항목의 이름으로 할당됩니다. 예를 들어, 다음 예시에서 원시 값이 문자이기 때문에, 각 항목의 원시 값은 항목의 이름으로 할당됩니다.
1
2
3
enum CompassPoint: String {
case north, south, east, west
}
위 예시에서 CompassPoint.north의 원시 값은 “north”입니다.
원시 값을 rawValue
속성을 이용하여 접근할 수 있습니다.
1
2
3
4
5
let earthsOrder = Planet.earth.rawValue
// earthsOrder is 3
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"
원시 값으로 초기화
원시 값을 정의했다면, 원시 값으로 변수나 상수를 초기화할 수 있습니다. 원시 값으로 초기화하는 것이기 때문에, 주어진 원시 값을 찾지 못할 경우도 있어 Optional 형태로 반환됩니다.
따라서 아래의 예시는 Optional(Planet.uranus)를 반환합니다. 타입은 Planet?
입니다.
1
2
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus
만약 원시 값으로 11을 주었을 때, nil을 반환할 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
let positionToFind = 11
if let somePlanet = Planet(rawValue: positionToFind) {
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
} else {
print("There isn't a planet at position \(positionToFind)")
}
// Prints "There isn't a planet at position 11"
위 예시에서 행성에 접근하기 위해 if-let 구문(옵셔널 바인딩)[5]을 사용하였습니다.
Planet에서 11을 갖는 항목은 없기 때문에 else 분기가 실행됩니다.
순환 열거형
순환 열거형은 연관 값으로 자기 자신을 갖는 열거형을 뜻합니다. case 앞에 indirect
로 선언하여 순환 열거형을 정의할 수 있습니다.
아래 예시는 산술 연산을 저장하는 순환 열거형입니다.
1
2
3
4
5
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
또는 열거형 앞에 indirect
를 선언하여 연관 값을 갖는 모든 열거 항목에 대해 순환 열거형을 적용할 수 있습니다.
1
2
3
4
5
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
위 코드는 숫자와 덧셈, 곱셈에 관련한 항목들을 가지고 있습니다. 덧셈과 곱셈 항목의 경우 열거형을 연관 값으로 갖고 있으며 이러한 연관 값을 사용하면 식을 중첩할 수 있습니다.
예를 들어, 식 (5 + 4) * 2는 곱셈의 오른쪽에 숫자가 있고 곱셈의 왼쪽에 다른 식이 있습니다. 데이터가 중첩되기 때문에 데이터를 저장하는 데 사용되는 열거도 순환 열거형이어야 합니다. 즉, 열거가 재귀적이어야 합니다. 아래 코드는 (5 + 4) * 2에 대해 만들어지고 있는 산술 표현식 재귀 열거를 보여줍니다.
1
2
3
4
5
6
let five = ArithmeticExpression.number(5) // 5
let four = ArithmeticExpression.number(4) // 4
let sum = ArithmeticExpression.addition(five, four) // 5 + 4
// (5 + 4) * 2
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))
재귀 함수는 재귀 구조를 가진 데이터를 쉽게 사용할 수 있는 방법입니다. 예를 들어, 산술계산하는 함수는 다음과 같습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
func evaluate(_ expression: ArithmeticExpression) -> Int {
switch expression {
case let .number(value):
return value
case let .addition(left, right):
return evaluate(left) + evaluate(right)
case let .multiplication(left, right):
return evaluate(left) * evaluate(right)
}
}
print(evaluate(product))
// Prints "18"
이 함수는 숫자일 경우 숫자 그대로 반환하며, 그렇지 않을 경우 각 연산자에 따라 왼쪽 식과 오른쪽 식의 반환값을 계산하여 반환합니다.
Reference
- The Basics — The Swift Programming Language (Swift 5.6)
- Camel Case - 위키피디아
- Each enumeration definition defines a new type. Like other types in Swift, their names (such as CompassPoint and Planet) start with a capital letter. Give enumeration types singular rather than plural names, so that they read as self-evident: Enumerations — The Swift Programming Language (Swift 5.6)
- Protocols — The Swift Programming Language (Swift 5.6)
- Optional Chaining — The Swift Programming Language (Swift 5.6)