[Programming Language]/[Swift]

[Swift] 열거형(enum: Enumerations) 총 정리

Semincolon 2024. 7. 22. 19:06

1. 열거형(enum)

열거형이란 성격(종류)가 비슷한 값들의 그룹 개념으로 비슷한 데이터를 나타내는 값들을 모아서 정의한 것이다. 이는 다른 프로그래밍 언어에서도 종종 본 적이 있을 것이다. 예를들어 나라에 대한 열거형이라 한다면 아래와 같이 나타낼 수 있다.

enum Countries {
    case korea
    case japan
    case america
    case canada
}

// 각 값을 쉼표(,)로 구분하여 한 줄로 작성하는 것도 가능
enum Countries {
    case korea, japan, america, canada
}

열거형을 정의할 땐 위와 같이 열거형의 이름은 첫글자를 대문자로, 각각의 case소문자로 작성한다. 추가로 열거형은 각각의 값을 case로 나눠서 정의할 수도 있지만 한 줄로 작성하는 것도 가능하다.

 

1-1. 열거형 값 사용 방법

위에서 선언한 열거형은 각 case만 가지고 있을 뿐 특정 수치를 가지고 있지는 않다. 이를 저장하는 것은 잠시 후에 하도록 하고 일단 위 열거형 데이터를 사용하는 방법은 다음과 같다.

enum Countries {
    case korea
    case japan
    case america
    case canada
}

let countryA = Countries.korea
let countryB: Countries = Countries.japan
let countryC: Countries = .america // 타입을 지정했으므로 Countries 를 생략할 수 있음

print(type(of: countryA), countryA) // Countries korea
print(type(of: countryB), countryB) // Countries japan
print(type(of: countryC), countryC) // Countries america

countryA~C는 모두 Countries 라는 열거형 값이므로 타입이 모두 동일한 것을 확인할 수 있다. 열거형 데이터를 할당할 때 타입에 열거형을 지정해주면 countryC의 경우처럼 열거형의 이름을 생략할 수 있다.

 

1-2. 초깃값이 할당된 열거형

이번엔 초깃값을 갖는 열거형을 선언해보자. 열거형이 초깃값을 갖기 위해서는 열거형의 이름 뒤에 타입을 지정해주어야 한다. 타입을 지정하지 않고 초깃값을 할당하려 하면 할당할 수 없다는 오류가 발생한다. 아래 예제는 정수형(Int) 초깃값을 갖는 열거형을 선언한 것이다.

enum Years: Int {
    case a = 2000
    case b = 2003
    case c = 2024
}

print(type(of: Years.a)) // Years
print(Years.a) // a
print(type(of: Years.a.rawValue)) // Int
print(Years.a.rawValue) // 2000

 

 

Years라는 열거형은 Int형 초깃값을 갖는다. 해당 열거형의 초깃값에 접근하려면 rawValue 속성을 사용하면 된다.

Question. 열거형의 초깃값은 수정이 가능할까?
Answer. 열거형의 초깃값은 수정할 수 없다. 이를 수정하려 하면 다음과 같은 오류가 발생한다.
Cannot assign to property: 'rawValue' is immutable

 

1-2-1. 타입만 지정하고 초깃값은 생략한다면 어떻게 될까?

열거형의 타입만 지정하고 초깃값을 생략한다면 정수형은 기본값 0, 실수형은 기본값 0.0, 문자열은 기본값으로 case 그 자체의 이름을 갖는다.

enum IntEnum: Int {
    case a
}

enum FloatEnum: Float {
    case b
}

enum DoubleEnum: Double {
    case c
}

enum StringEnum: String {
    case d
}

print(IntEnum.a.rawValue) // 0
print(FloatEnum.b.rawValue) // 0.0
print(DoubleEnum.c.rawValue) // 0.0
print(StringEnum.d.rawValue) // d

이처럼 타입만 지정하고 초깃값을 생략하면 지정한 타입에 따라 각각의 초깃값을 기본값으로 갖는 것을 확인할 수 있다.

 

1-2-2. 초깃값을 여러개 생략한다면 어떻게 될까?

문자열(String)의 경우 초깃값을 생략하면 항상 case 그 자체의 값을 기본값으로 갖는다.

enum StringEnum: String {
    case hello
    case swift
}

print(StringEnum.hello.rawValue) // hello
print(StringEnum.swift.rawValue) // swift

 

그러나 정수형(Int) 경우 초깃값을 생략하면 바로 앞의 값에 1을 더한 값을 기본값으로 갖는다. 그렇기 때문에 맨 첫 번째 case의 값을 생략한다면 앞의 값이 없으므로 기본으로 0에서 시작하는 것이다.

enum IntEnum: Int {
    case year = 2024
    case nextYear // 2024 + 1
}

print(IntEnum.year.rawValue) // 2024
print(IntEnum.nextYear.rawValue) // 2025

 

실수형(Float) 역시 바로 앞의 값에 1을 더한 값을 갖는데 이는 앞의 값이 정수 형태로 저장된 경우만 해당된다. 만약 바로 앞의 값이 1.0, 1.3처럼 실수 형태로 저장되었다면 그 다음 값은 생략할 수 없다.

enum FloatEnum: Double {
    case f1 = 1
    case f2 // f1이 정수 형태이므로 f1 + 1 값인 2가 실수 형태로 저장됨 (2.0)
    case f3 = 2.5
//    case f4 // f3이 실수 형태이므로 그 다음 값인 f4는 기본 값을 생략하면 오류가 발생함
    case f5 = 3
    case f6 // f5가 정수 형태이므로 f6는 다시 기본 값을 생략할 수 있음
}

print(FloatEnum.f1.rawValue) // 1.0
print(FloatEnum.f2.rawValue) // 2.0

print(FloatEnum.f3.rawValue) // 2.5

print(FloatEnum.f5.rawValue) // 3.0
print(FloatEnum.f6.rawValue) // 4.0

 

1-3. Switch 구문에서 열거형의 사용

Switch 구문에서 열거형을 사용할 수 있다. 단, 주의할 점은 switch 구문은 완벽해야 하므로 열거형의 모든 경우에 대한 case를 매치시켜야 한다는 것이다. 다시 말해서 모든 값에 대해 각각의 case로 매칭하거나 일부 값만 매칭하고 나머지 값에 대해서는 default로 매칭되도록 작성해야 한다는 것이다.

enum Countries {
    case korea, japan, america, canada
}

let country = Countries.korea

switch country {
case .korea:
    print("KOREA")
case .japan:
    print("JAPAN")
case .america:
    print("AMERICA")
case .canada:
    print("CANADA")
}

위 예제처럼 모든 값이 매칭될 수 있도록 하거나 아니면 아래처럼 default로 처리해주어야 한다.

...
switch country {
case .korea:
    print("KOREA")
default:
    print("OTHER")
}

 

1-4. 열거형 전체 데이터 및 전체 개수 (CaseIterable, allCases, count)

열거형의 전체 데이터를 한번에 접근하기 위해선 열거형의 이름 뒤에 CaseIterable 타입을 지정해줘야 한다. 이를 지정하면 열거형이름.allCases 로 전체 데이터를 한 번에 접근할 수 있고, 열거형이름.allCases.count 로 전체 개수를 얻을 수 있다.

enum Countries: CaseIterable {
    case korea, japan, america, canada
}

print(Countries.allCases) // [XmlParser.Countries.korea, XmlParser.Countries.japan, XmlParser.Countries.america, XmlParser.Countries.canada]
print(Countries.allCases.count) // 4

 

CaseIterable을 사용하게 되면 반복문을 통해 각 요소에 접근하는 것 역시 가능하다.

enum Countries: CaseIterable {
    case korea, japan, america, canada
}

for country in Countries.allCases {
    print(country)
}

/*
 korea
 japan
 america
 canada
 */

 

1-5. 각 case 마다 여러 값을 갖는 열거형

열거형의 각 case는 여러 값을 가질 수 있다. 예를 들어 아래는 Student 라는 열거형을 선언한 것인데 birth 를 사용하면 학생의 생년월일을 저장할 수 있고, name 을 사용하면 학생의 이름을 저장할 수 있다.

enum Student {
    case birth(Int, Int, Int)
    case name(String)
}

그러나 birth와 name 둘다 값을 저장하는 것은 불가능하다. birth 또는 name 중 하나의 case 만을 사용할 수 있다.

var student = Student.birth(2000, 11, 27)
print(student) // birth(2000, 11, 27)

student = .name("Semin Park")
print(student) // name("Semin Park")

 

1-6. 초깃값을 갖는 열거형은 그 값 자체로 열거형 데이터에 접근할 수 있다

앞서 열거형의 이름 뒤에 타입을 지정하면 열거형은 초깃값을 가질 수 있다고 했다. 이 열거형에 접근할 때 해당 값을 기준으로 접근할 수 있다. 그러나 Swift는 안전을 추구하는 언어이므로 열거형에 해당 값이 없는 경우를 고려하기 때문에 반환되는 값의 타입은 옵셔널(Optional)이다. 즉, 찾고자 하는 값이 없는 경우에는 nil이 반환된다는 것이다.

enum Countries: String {
    case korea, japan, america, canada
}

let countryA = Countries(rawValue: "korea")
print(countryA) // Optional(XmlParser.Countries.korea)
print(countryA!) // korea
print(countryA?.rawValue) // Optional("korea")

let countryB = Countries(rawValue: "china")
print(countryB) // nil

 

1-7. 재귀 열거형 (Recursive Enum)

열거형은 자기 자신을 값으로 가질 수 있다. 이를 위해서는 indirect 키워드를 사용해야 하며 이는 각 case 앞에 사용하거나 enum 키워드 앞에 사용하면 된다. 과연 재귀 열거형이 실제로 자주 사용될까..?

// 자기 자신을 값으로 갖는 각 case 앞에 indirect 를 사용해도 되고,
enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

// enum 선언 앞부분에 indirect 를 사용해도 된다.
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

 

위 재귀 열거형 타입의 변수 2개를 선언해보겠다.

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)

 

fivefour ArithmeticExpressionadditionmultiplication에 저장될 수 있다. 또한 addtionmultiplication은 서로를 저장할 수도 있다.

let sum = ArithmeticExpression.addition(five, four)
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

 

이제 ArithmeticExpression 타입을 매개변수로 갖는 함수를 선언하여 이 재귀 열거형을 사용해보자.

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)
    }
}

함수 evalutateswitch문의 각 case에서는 재귀 호출이 일어나게 되고, 이를 통해 각 경우에 따라 연산이 이뤄지게 된다.

print(evaluate(product))
// Prints "18"

열거형은 아직 다루지 않은 구조체와 비슷하게 같은 유형의 값을 저장한다는 특성이 있으니 실제로 코딩할 때 자주 사용될 것 같다...

특정 데이터에 대한 데이터 구조를 만드는 상황에서 유용하게 사용될 수 있으니 익혀두고 사용할 수 있도록 하자.

 

끝!