본문 바로가기
iOS/SWIFT

[Swift] Chapter 04. 데이터 타입 고급

by 원만사 2021. 10. 8.
반응형

 

 

 

4.1 데이터 타입 안심


스위프트는 타입에 굉장히 민감하고 엄격하다. 서로 다른 타입끼리의 데이터 교환은 꼭 타입 캐스팅(Type Casting)을 거쳐야 한다. 스위프트에서 값 타입의 데이터 교환은 엄밀히 말하면 타입캐스팅이 아닌 새로운 인스턴스를 생성하여 할당하는 것이다.

 

4.1.1 데이터 타입 안심이란

스위프트는 데이터 타입을 안심하고 사용할 수 있는(Type-Safe) 언어이다. 이 말은 그만큼 실수를 줄일 수 있다는 의미이다. 예를 들어 Int 타입 변수에 할당 하려는 값이 Character 타입이라면 컴파일 오류가 발생한다. 이런 오류는 프로그래밍 도중에 눈치채기 어려워 컴파일러가 알려주지 않으면 나중에 오류를 찾아내기 쉽지 않다. 그렇지만 스위프트는 컴파일 오류로 알려주므로 서로 다른 타입의 값을 할당하는 실수를 줄일 수 있다. 이렇게 스위프트가 컴파일 시 타입을 확인하는 것을 타입 확인이라고 한다.

 

4.1.2 타입 추론

변수나 상수를 선언할 때 특정 타입을 명시하지 않아도 컴파일러가 할당된 값을 기준으로 변수나 상수의 타입을 결정한다.

/*
	코드 4-1. 타입 안심과 타입 추론
*/

// 타입을 지정하지 않았으나 타입 추론을 통하여 name은 String 타입으로 선언된다.
var name = "wondae"

// 앞서 타입 추론에 의해 name은 String 타입의 변수로 지정되었기 때문에
// 정수를 할당하려고 시도하면 오류가 발생한다.
name = 100

 

4.2 타입 별칭


스위프트에서 기본으로 제공하는 데이터 타입이든, 사용자가 임의로 만든 데이터 타입이든 이미 존재하는 데이터 타입에 임의로 다른 이름(별칭)을 부여할 수 있다. 기존에 사용하던 데이터 타입의 이름과 프로그래머가 만들어준 이름 모두 사용할 수 있다.

/*
	코드 4-2. 타입 별칭
*/

typealias MyInt = Int
typealias YourInt = Int
typealias MyDouble = Double

let age: MyInt = 100  // MyInt는 Int의 또 다른 이름이다.
var year: YourInt = 2080  // YourInt도 Int의 또 다른 이름이다.

// MyInt도, YourInt도 Int이기 때문에 같은 타입으로 취급한다.
year = age

let month: Int = 7 // 물론 기존의 Int도 사용 가능하다.
let percentage: MyDouble = 99.9 // Int 외에 다른 자료형도 모두 별칭 사용이 가능하다.

 

4.3 튜플


튜플은 타입의 이름이 따로 지정되어 있지 않은, 프로그래머 마음대로 만드는 타입이다. '지정된 데이터의 묶음' 이라고 표현할 수 있다.

스위프트의 튜플은 파이썬의 튜플과 유사하다. 튜플은 타입 이름이 따로 없으므로 일정 타입의 나열 만으로 튜플 타입을 생성해줄 수 있다. 튜플에 포함될 데이터의 개수는 자유롭게 정할 수 있다.

 

/*
	코드 4-3. 튜플 기본
*/

// String, Int, Double 타입을 갖는 튜플
var person: (String, Int, Double) = ("choi", 100, 182.5)

// 인덱스를 통해서 값을 빼올 수 있다.
print("이름 : \(person.0), 나이 : \(person.1), 신장 : \(person.2)")

person.1 = 99 // 인덱스를 통해 값을 할당할 수 있다.
person.2 = 178.5

print("이름 : \(person.0), 나이 : \(person.1), 신장 : \(person.2)")

[코드 4-3]에서는 튜플의 각 요소를 이름 대신 숫자로 표현하기 때문에 간편해 보일 수 있지만, 차후에 다른 프로그래머가 코드를 볼 때 각 요소가 어떤 의미가 있는지 유추하기가 어렵다. 그래서 튜플의 요소마다 이름을 붙여줄 수도 있다. [코드 4-4]에서는 튜플의 요소마다 이름을 붙여봤다.

 

/*
	코드 4-4. 튜플 요소 이름 지정
*/

// String, Int, Double 타입을 갖는 튜플
var person: (name: String, age: Int, height: Double) = ("choi", 100, 182.5)

// 요소 이름을 통해서 값을 빼올 수 있다.
print("이름 : \(person.name), 나이 : \(person.age), 신장 : \(person.height)")

person.age = 99 // 요소 이름을 통해 값을 할당할 수 있다.
person.2 = 178.5 // 인덱스를 통해서도 값을 할당할 수 있다.

// 기존처럼 인덱스를 이용하여 값을 빼올 수도 있다.
print("이름 : \(person.0), 나이 : \(person.1), 신장 : \(person.2)")

또, 튜플에는 타입 이름에 해당하는 키워드가 따로 없다 보니 사용에 불편함을 겪기도 한다. 매번 같은 모양의 튜플을 사용하고 싶은데 선언해줄 때마다 긴 튜플 타입을 모두 써줘야 하는 불편함이 생길 수도 있기 때문이다. 이럴 때는 타입 별칭을 사용하여 조금 더 깔끔하고 안전하게 코드를 작성할 수 있다.

 

/*
	코드 4-5. 튜플 별칭 지정
*/

typealias PersonTuple = (name: String, age: Int, height: Double)

let yagom: PersonTuple = ("yagom", 100, 178.5)
let eric: PersonTuple = ("eric", 150, 183.5)

print("이름 : \(yagom.name), 나이 : \(yagom.age), 신장 : \(yagom.height)")
print("이름 : \(eric.name), 나이 : \(eric.age), 신장 : \(eric.height)")

 

4.4 컬렉션형


스위프트는 튜플 외에도 많은 수의 데이터를 묶어서 저장하고 관리할 수 있는 컬렉션 타입을 제공한다. 컬렉션 타입에는 배열(Array), 딕셔너리(Dictionary), 세트(Set)등이 있다.

 

4.4.1 배열

배열은

같은 타입

의 데이터를 일렬로 나열한 후 순서대로 저장하는 형태의 컬렉션 타입이다. 각기 다른 위치에 같은 값이 들어갈 수도 있다.

 

배열 타입을 선언해줄 방법은 다양하다. let 키워드를 사용해 상수로 선언하면 변경할 수 없는 배열이 되고, var 키워드를 사용해 변수로 선언해주면 변경 가능한 배열이 된다. 실제로 배열을 사용할 때는 Array라는 키워드와 타입 이름의 조합으로 사용한다. 또, 대괄호로 값을 묶어 Array 타입임을 표현할 수도 있다. 빈 배열은 이니셜라이저 또는 리터럴 문법을 통해 생성해줄 수 있는데 isEmpty 프로퍼티로 비어있는 배열인지 확인해 볼 수 있다. 그리고 배열에 몇 개의 요소가 존재하는지 알고 싶으면 count 프로퍼티를 확인하면 된다.

 

NOTE_스위프트의 Array 스위프트의 Array는 C 언어의 배열처럼 버퍼(Buffer)이다. 단, C 언어처럼 한 번 선언하면 크기가 고정되던 버퍼가 아니라, 필요에 따라 자동으로 버퍼의 크기를 조절해주므로 요소의 삽입 및 삭제가 자유롭다.

 

/*
	코드 4-6. 배열의 선언과 생성
*/

// 대괄호를 사용하여 배열임을 표시한다.
var names: Array<String> = ["yagom", "chulsoo", "younghee", "yagom"]

// 위 선언과 정확히 동일한 표현이다. [String]은 Array<String>의 축약 표현이다.
var names2: [String] = ["yagom", "chulsoo", "younghee", "yagom"]

var emptyArray: [Any] = [Any]() // Any 데이터를 요소로 갖는 빈 배열을 생성한다.
var emptyArray2: [Any] = Array<Any>() // 위 선언과 정확히 같은 동작을 하는 코드이다.

// 배열의 타입을 정확히 명시해줬다면 []만으로도 빈 배열을 생성할 수 있다.
var emptyArray3: [Any] = []
print(emptyArray.isEmpty) // true
print(names.count) // 4

 

배열은 각 요소에 인덱스를 통해 접근할 수 있다. 인덱스는 0부터 시작한다. 잘못된 인덱스로 접근하려고 하면 익셉션 오류가 발생한다. 또, 맨 처음과 맨 마지막 요소는 first와 last 프로퍼티를 통해 가져올 수 있다. firstIndex(of:) 메서드를 사용하면 해당 요소의 인덱스를 알아낼 수도 있다. 만약 중복된 요소가 있다면 제일 먼저 발견된 요소의 인덱스를 반환한다. 맨 뒤에 요소를 추가하고 싶다면 append(_:) 메서드를 사용한다.

중간에 요소를 삽입하고 싶다면 insert(at:) 메서드를 사용하면 된다. 요소를 삭제하고 싶다면 remove(_:) 메서드를 사용하게 되는데, 메서드를 사용하면 해당 요소가 삭제된 후 반환된다.

 

/*
	코드 4-7. 배열의 사용
*/

print(names[2]) // younghee
names[2] = "jenny"
print(names[2]) // jenny
//print(names[4]) // 인덱스의 범위를 벗어났기 때문에 오류가 발생함.

//names[4] = "elsa" // 인덱스의 범위를 벗어났기 때문에 오류가 발생함.
names.append("elsa") // 마지막에 elsa가 추가된다.
names.append(contentsOf: ["john", "max"]) // 맨 마지막에 john과 max가 추가된다.
names.insert("happy", at: 2) // 인덱스 2에 삽입된다.
// 인덱스 5의 위치에 jinhee와 minsoo가 삽입된다.
names.insert(contentsOf: ["jinhee", "minsoo"], at: 5)


print(names[4])         // yagom
print(names.firstIndex(of: "yagom")) // 0
print(names.firstIndex(of: "christal")) // nil
print(names.first) // yagom
print(names.last) // max

let firstItem: String = names.removeFirst()
let lastItem: String = names.removeLast()
let indexZeroItem: String = names.remove(at: 0)

print(firstItem) // yagom
print(lastItem)  // max
print(indexZeroItem) // chulsoo
print(names[1 ... 3]) // ["jenny", "yagom", "jinhee"]

 

4.4.2 딕셔너리

딕셔너리는 요소들이 순서 없이 키와 값의 쌍으로 구성되는 컬렉션 타입이다. 딕셔너리에 저장되는 값은 항상 키와 쌍을 이루게 되는데, 딕셔너리 안에는 키가 하나이거나 여러 개일 수 있다. 단, 하나의 딕셔너리 안의 키는 같은 이름을 중복해서 사용할 수 없다. 즉, 딕셔너리에서 키는 값을 대변하는 유일한 식별자가 되는 것이다.

 

딕셔너리는 Dictionary라는 키워드와 키의 타입과 값의 타입 이름의 조합으로 써준다. 대괄호로 키와 값의 타입 이름의 쌍을 묶어 딕셔너리 타입임을 표현한다. 빈 딕셔너리는 이니셜라이저 또는 리터럴 문법을 통해 생성할 수 있다. isEmpty 프로퍼티를 통해 비어있는 딕셔너리인지 확인할 수 있다. 그리고 count 프로퍼티로 딕셔너리의 요소 개수를 확인할 수 있다.

 

/*
	코드 4-8. 딕셔너리의 선언과 생성
*/

// typealias를 통해 조금 더 단순하게 표현할 수 있다.
typealias StringIntDictionary = [String: Int]

// 키는 String, 값은 Int 타입인 빈 딕셔너리를 생성한다.
var numberForName: Dictionary<String, Int> = Dictionary<String, Int>()

// 위 선언과 같은 표현이다. [String: Int]는 Dictionary<String, Int>의 축약 표현이다.
var numberForName2: [String: Int] = [String: Int]()

// 위 코드와 같은 동작을 한다.
var numberForName3: StringIntDictionary = StringIntDictionary()

// 딕셔너리의 키와 값 타입을 정확히 명시해줬다면 [:]만으로도 빈 딕셔너리를 생성할 수 있다.
var numberForName4: [String: Int] = [:]

// 초깃값을 주어 생성해줄 수도 있다.
var numberForName5: [String: Int] = ["yagom": 100, "chulsoo": 200, "jenny": 300]

print(numberForName5.isEmpty) // false
print(numberForName5.count) // 3

 

딕셔너리는 각 값에 키로 접근할 수 있다. 딕셔너리 내부에서 키는 유일해야 하며, 값은 유일하지 않다. 딕셔너리는 배열과 다르게 딕셔너리 내부에 없는 키로 접근해도 오류가 발생하지 않는다. 다만 nil을 반환한다. 특정 키에 해당하는 값을 제거하려면 removeValue(forKey:) 메서드를 사용한다. 키에 해당하는 값이 제거된 후 반환된다.

 

/*
	코드 4-9. 딕셔너리의 사용
*/

print(numberForName["chulsoo"]) // 200
print(numberForName["minji"]) // nil

numberForName["chulsoo"] = 150
print(numberForName["chulsoo"]) // 150

numberForName["max"] = 999 // max라는 키로 999라는 값을 추가해준다.
print(numberForName["max"]) // 999

print(numberForName.removeValue(forKey: "yagom")) // 100

// 위에서 yagom 키에 해당하는 값이 이미 삭제되었으므로 nil이 반환된다.
// 키에 해당하는 값이 없으면 기본값을 돌려주도록 할 수도 있다.
print(numberForName.removeValue(forKey: "yagom"))


// yagom 키에 해당하는 값이 없으면 기본으로 0이 반환된다.
print(numberForName["yagom", default: 0]) // 0

 

4.4.3 세트

세트는 같은 타입의 데이터를 순서 없이 하나의 묶음으로 저장하는 형태의 컬렉션 타입이다. 세트 내의 값은 모두 유일한 값, 즉 중복된 값이 존재하지 않는다. 그래서 세트는 보통 순서가 중요하지 않거나 각 요소가 유일한 값이어야 하는 경우에 사용한다. 또, 세트의 요소로는 해시 가능한 값이 들어와야 한다.

 

세트는 Set 키워드와 타입 이름의 조합으로 써준다. 또, 배열과 마찬가지로 대괄호로 값들을 묶어 세트 타입임을 표현한다. 배열과 달리 줄여서 표현할 수 있는 축약형이 없다. let 키워드와 var 키워드를 사용 가능하다. 빈 세트는 이니셜라이저 또는 리터럴 문법을 통해 생성할 수 있다. isEmpty 프로퍼티를 통해 비어있는 세트인지 확인해볼 수 있다. 그리고 세트에 몇 개의 요소가 존재하는지 알고 싶으면 count 프로퍼티를 확인하면 된다.

 

/*
	코드 4-10. 세트의 선언과 생성
*/

var names3: Set<String> = Set<String>() // 빈 세트 생성
var names2: Set<String> = [] // 빈 세트 생성

// Array와 마찬가지로 대괄호를 사용한다.
var names: Set<String> = ["yagom", "chulsoo", "younghee", "yagom"]

// 그렇기 때문에 타입 추론을 사용하게 되면 컴파일러는 Set가 아닌 Array로 타입을 지정한다.
var numbers = [100, 200, 300]
print(type(of: numbers)) // Array

print(names.isEmpty) // false
print(names.count)   // 3 - 중복된 값은 허용되지 않아 yagom은 1개만 남는다.

 

세트에 요소를 추가하고 싶다면 insert(_:) 메서드를 사용한다. 요소를 삭제하고 싶다면 remove(_:) 메서드를 사용하는데, 메서드를 사용하면 해당 요소가 삭제된 후 반환된다.

 

/*
	코드 4-11. 세트의 사용
*/

print(names.count) // 3
names.insert("jenny")
print(names.count) // 4

print(names.remove("chulsoo")) // chulsoo
print(names.remove("john"))    // nil

 

세트는 자신 내부의 값들이 모두 유일함을 보장하므로, 집합관계를 표현하고자 할 때 유용하게 쓰일 수 있으며, 두 세트의 교집합, 합집합 등을 연산하기에 매우 용이하다. 또한 sorted() 메서드를 통하여 정렬된 배열을 반환해줄 수도 있다.

 

/*
	코드 4-12. 세트의 활용 - 집합연산
*/

let englishClassStudents: Set<String> = ["john" ,"chulsoo", "yagom"]
let koreanClassStudents: Set<String> = ["jenny", "yagom", "chulsoo", 
		"hana", "minsoo"]


// 교집합 {"yagom", "chulsoo"}
let intersectSet: Set<String> = 
		englishClassStudents.intersection(koreanClassStudents)

// 여집합의 합(배타적 논리합) {"john", "jenny", "hana", "minsoo"}
let symmetricDiffSet: Set<String> = 
		englishClassStudents.symmetricDifference(koreanClassStudents)

// 합집합 {"minsoo", "jenny", "john", "yagom", "chulsoo", "hana"}
let unionSet: Set<String> = e
		nglishClassStudents.union(koreanClassStudents)

// 차집합 {"john"}
let subtractSet: Set<String> = 
		englishClassStudents.subtracting(koreanClassStudents)

print(unionSet.sorted()) // ["chulsoo", "hana", "jenny", "john", "minsoo", "yagom"]

 

세트는 포함 관계를 연산할 수 있는 메서드로 구현되어 있다.

 

let 새: Set<String> = ["비둘기", "닭", "기러기"]
let 포유류: Set<String> = ["사자", "호랑이", "곰"]
let 동물: Set<String> = 새.union(포유류) // 새와 포유류의 합집합

print(새.isDisjoint(with: 포유류)) // 서로 배타적인지 - true
print(새.isSubset(of: 동물)) // 새가 동물의 부분집합? - true
print(동물.isSuperset(of: 포유류)) // 동물은 포유류의 전체집합? - true
print(동물.isSuperset(of: 새)) // 동물은 새의 전체집합? - true

 

4.5 열거형

열거형은 연관된 항목들을 묶어서 표현할 수 있는 타입이다. 열거형은 배열이나 딕셔너리 같은 타입과 다르게 프로그래머가 정의해준 항목 값 외에는 추가/수정이 불가능하다. 그렇기 때문에 딱 정해진 값만 열거형 값에 속할 수 있다.

열거형은 다음 같은 경우에 요긴하게 사용할 수 있다.

  • 제한된 선택지를 주고 싶을 때
  • 정해진 값 외에는 입력받고 싶지 않을 때
  • 예상된 입력 값이 한정되어 있을 때

 

스위프트의 열거형은 항목별로 값을 가질 수도, 가지지 않을 수도 있다. 예를 들어 C 언어는 열거형의 각 항목 값이 정수 타입으로 기본 지정되지만, 스위프트의 열거형은 각 항목이 그 자체로 고유의 값이 될 수 있다. 스위프트의 열거형은 각 열거형이 고유의 타입으로 인정되기 때문에 실수로 버그가 일어날 가능성을 원천 봉쇄할 수 있다.

물론 열거형 각 항목이 원시 값(raw value)이라는 형태로 (정수, 실수, 문자 타입 등의) 실제 값을 가질 수도 있다. 또는 연관 값(Associated Values)을 사용하여 다른 언어에서 공용체라고 불리는 값의 묶음을 구현할 수 있다.

 

4.5.1 기본 열거형

enum 키워드로 선언할 수 있다.

/*
	코드 4-14. School 열거형의 선언
*/

enum School {
    case primary // 유치원
    case elementary // 초등
    case middle // 중등
    case high // 고등
    case college // 대학
    case university // 대학교
    case graduate // 대학원
}

각 항목은 그 자체가 고유의 값이며, 항목이 여러 가지라서 나열하기 귀찮거나 어렵다면 한 줄에 모두 표현해줄 수도 있다.

/*
	코드 4-15. School 열거형의 선언
*/

enum School {
    case primary, elementary, middle, high, college, university, graduate
}

 

/*
	코드 4-16. School 열거형 변수의 생성 및 값 변경
*/

var highestEducationLevel: School = School.university

// 위 코드와 정확히 같은 표현
var highestEducationalLevel2: School = .university

// 같은 타입인 School 내부의 항목으로만 highestEducationalLevel의 값을 변경해줄 수 있다.
highestEducationLevel = .graduate

 

4.5.2 원시 값

열거형의 각 항목은 자체로도 하나의 값이지만 항목의 원시 값(Raw Value)도 가질 수 있다. 즉, 특정 타입으로 지정된 값을 가질 수 있다는 뜻이다. 특정 타입의 값을 원시 값으로 가지고 싶다면 열거형 이름 오른쪽에 타입을 명시해주면 된다. 또, 원시 값을 사용하고 싶다면 rawValue라는 프로퍼티를 통해 가져올 수 있다.

 

/*
	코드 4-17. 열거형의 원시 값 지정과 사용
*/

enum School: String {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college = "대학"
    case university = "대학교"
    case graduate = "대학원"
}

let highestEducationLevel: School = .university
print("저의 최종학력은 \(highestEducationLevel.rawValue) 졸업입니다.") 
// 저의 최종학력은 대학교 졸업입니다.

enum WeekDays: Character {
    case mon = "월", tue = "화", wed = "수", thu = "목", fri = "금", 
		sat = "토", sun = "일"
}

let today: WeekDays = WeekDays.fri
print("오늘은 \(today.rawValue)요일입니다.") // 오늘은 금요일입니다.

 

만약 일부 항목만 원시 값을 주고 싶다면 그렇게 해도 된다. 나머지는 스위프트가 알아서 처리해준다. 문자열 형식의 원시 값을 지정해줬다면 각 항목 이름을 그대로 원시 값으로 갖게 되고, 정수 타입이라면 첫 항목을 기준으로 0부터 1씩 늘어난 값을 갖게 된다.

/*
 코드 4-18. 열거형의 원시 값 일부 지정 및 자동 처리
*/

enum School: String {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college
    case university
    case graduate
}

let highestEducationLevel: School = School.university
print("저의 최종학력은 \(highestEducationLevel.rawValue) 졸업입니다.")
// 저의 최종학력은 university 졸업입니다.

print(School.elementary.rawValue) // 초등학교

enum Numbers: Int {
    case zero
    case one
    case two
    case ten = 10
}

print("\(Numbers.zero.rawValue), \(Numbers.one.rawValue), \(Numbers.two.rawValue), \(Numbers.ten.rawValue)")
// 0, 1, 2, 10

 

열거형이 원시 값을 갖는 열거형일 때, 열거형의 원시 값 정보를 안다면 원시 값을 통해 열거형 변수 또는 상수를 생성해줄 수도 있다. 만약 올바르지 않은 원시 값을 통해 생성하려고 한다면 nil을 반환한다. 이는 실패 가능한 이니셜라이저 기능이다.

/*
	코드 4-19. 원시 값을 통한 열거형 초기화
*/

let primary = School(rawValue: "유치원") // primary
let graduate = School(rawValue: "석박사") // nil

 

4.5.3 연관 값

스위프트의 열거형 각 항목이 연관 값을 가지게 되면, 기존 프로그래밍 언어의 공용체 형태를 띌 수도 있다. 열거형 내의 항목(case)이 자신과 연관된 값을 가질 수 있다. 연관 값은 각 항목 옆에 소괄호로 묶어 표현할 수 있다. 다른 항목이 연관 값을 갖는다고 모든 항목이 연관 값을 가질 필요는 없다.

enum MainDish {
    case pasta(taste: String)
    case pizza(dough: String, topping: String)
    case chicken(withSauce: Bool)
    case rice
}

var dinner: MainDish = MainDish.pasta(taste: "크림") // 크림 파스타
dinner = .pizza(dough: "치즈크러스트", topping: "불고기") // 불고기 치즈크러스트 피자
dinner = .chicken(withSauce: true) // 양념 통닭
dinner = .rice // 밥

 

식당의 재료가 한정적이라 파스타의 맛과 피자의 도우, 토핑 등을 특정 메뉴로 한정 지으려면 [코드 4-21]처럼 열거형으로 바꾸면 된다.

/*
	코드 4-21. 여러 열거형의 응용
*/

enum PastaTaste {
    case cream, tomato
}

enum PizzaDough {
    case cheeseCrust, thin, original
}

enum PizzaTopping {
    case pepperoni, cheese, bacon
}

enum MainDish {
    case pasta(taste: PastaTaste)
    case pizza(dough: PizzaDough, topping: PizzaTopping)
    case chicken(withSauce: Bool)
    case rice
}

var dinner: MainDish = MainDish.pasta(taste: PastaTaste.tomato)
dinner = .pizza(dough: PizzaDough.cheeseCrust, topping: PizzaTopping.bacon)

 

4.5.4 항목 순회

우리는 때때로 열거형에 포함된 모든 케이스를 알아야 할 때가 있다. 그럴 때 열거형의 이름 뒤에 콜론(:)을 작성하고 한 칸 띄운 뒤 CaseIterable 프로토콜을 채택한다. 그러면 열거형에 allCases라는 이름의 타입 프로퍼티를 통해 모든 케이스의 컬렉션을 생성해준다.

/*
	코드 4-22. CaseIterable 프로토콜을 활용한 열거형의 항목 순회
*/

enum School: CaseIterable {
    case primary
    case elementary
    case middle
    case high
    case college
    case university
    case graduate
}

let allCases: [School] = School.allCases
print(allCases)
// [School.primary, School.elementary, School.middle, School.high, 
//   School.college, School.university, School.graduate]

 

원시값을 갖는 열거형이라면 원시값의 타입 다음에 쉼표(, )를 쓰고 띄어쓰기를 한 후 CaseIterable 프로토콜을 채택해주면 된다.

조금 복잡해지는 열거형은 CaseIterable 프로토콜을 채택해 주는 것만으로 allCases 프로퍼티를 사용하지 못할 수도 있다. 그 대표적인 예가 플랫폼별로 사용 조건을 추가하는 경우이다.

/*
 코드 4-24. available 속성을 갖는 열거형의 항목 순회
*/

enum School: String, CaseIterable {
    case primary = "유치원"
    case elementary = "초등학교"
    case middle = "중학교"
    case high = "고등학교"
    case college = "대학"
    case university = "대학교"
    @available(iOS, obsoleted: 12.0)
    case graduate = "대학원"
    
    static var allCases: [School] {
        let all: [School] = [.primary,
                             .elementary,
                             .middle,
                             .high,
                             .college,
                             .university ]
        
        #if os(iOS)
        return all
        #else
        return all + [.graduate]
        #endif
    }
}

let allCases: [School] = School.allCases
print(allCases)
// 실행환경에 따라 다른 결과

 

[코드 4-24]처럼 avilable 속성을 통해 특정 케이스를 플랫폼에 따라 사용할 수 있거나 없는 경우가 생기면 CaseIterable 프로토콜을 채택하는 것만으로는 allCases 프로퍼티를 사용할 수 없다. 그럴 때는 직접 allCases 프로퍼티를 구현해 주어야 한다. 이렇게 CaseIterable 프로토콜을 채택하여도 allCases 프로퍼티를 바로 사용할 수 없는 경우가 또 있는데, 바로 열거형의 케이스가 연관 값을 갖는 경우이다.

/*
    코드 4-25. 연관 값을 갖는 열거형의 항목 순회
*/

enum PastaTaste: CaseIterable {
    case cream, tomato
}

enum PizzaDough: CaseIterable {
    case cheeseCrust, thin, original
}

enum PizzaToping: CaseIterable {
    case pepperoni, cheese, bacon
}

enum MainDish: CaseIterable {
    case pasta(taste: PastaTaste)
    case pizza(dough: PizzaDough, topping: PizzaToping)
    case chicken(withSauce: Bool)
    case rice
    
    static var allCases: [MainDish] {
        return PastaTaste.allCases.map(MainDish.pasta)
        + PizzaDough.allCases.reduce([]) { (result, dough) -> [MainDish] in
            result + PizzaToping.allCases.map { (topping) -> MainDish in
                MainDish.pizza(dough: dough, topping: topping)
            }
        }
        + [true, false].map(MainDish.chicken)
        + [MainDish.rice]
    }
}

print(MainDish.allCases.count) // 14
print(MainDish.allCases) // 모든 경우의 연관 값을 갖는 케이스 컬렉션

 

4.5.5 순환 열거형

순환 열거형은 열거형 항목의 연관 값이 열거형 자신의 값이고자 할 때 사용한다. 순환 열거형을 명시하고 싶다면 indirect 키워드를 사용하면 된다. 특정 항목에만 한정하고 싶다면 case 키워드 앞에 indirect를 붙이면 되고, 열거형 전체에 적용하고 싶다면 enum 키워드 앞에 indirect 키워드를 붙이면 된다.

다음은 산술 연산을 위해 정의한 열거형이다.

/*
	코드 4-26. 특정 항목에 순환 열거형 항목 명시
*/

enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}

 

/*
	코드 4-27. 열거형 전체에 순환 열거형 명시
*/

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

 

[코드 4-28]은 ArithmeticExpression 열거형을 사용하여 (5+4) X 2 연산을 구현해보는 예제이다. evaluate는 ArithmeticExpression 열거형의 계산을 도와 주는 순환 함수이다.

/*
	코드 4-28. 순환 열거형의 사용
*/

indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression, ArithmeticExpression)
    case multiplication(ArithmeticExpression, ArithmeticExpression)
}

let five = ArithmeticExpression.number(5)
let four = ArithmeticExpression.number(4)
let sum = ArithmeticExpression.addition(five, four)
let final = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2))

func evalute(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evalute(left) + evalute(right)
    case .multiplication(let left, let right):
        return evalute(left) * evalute(right)
    }
}


let result: Int = evalute(final)
print("(5+4) * 2 = \(result)")

indirect 키워드는 [코드 4-28]의 예제뿐만 아니라, 이진 탐색 트리 등의 순환 알고리즘을 구현할 때 유용하게 사용할 수 있다.

 

4.5.6 비교 가능한 열거형

Comparable 프로토콜을 준수하는 연관 값만 갖거나 연관 값이 없는 열거형은 Comparable 프로토콜을 채택하면 각 케이스를 비교할 수 있다. 앞에 위치한 케이스가 더 작은 값이 된다.

/*
	코드 4-29. 비교 가능한 열거형의 사용
*/

enum Condition: Comparable {
    case terrible
    case bad
    case good
    case great
}

let myCondition: Condition = Condition.great
let yourCondition: Condition = .bad

if myCondition >= yourCondition {
    print("제 상태가 더 좋군요")
} else {
    print("당신의 상태가 더 좋아요")
}

enum Device: Comparable {
    case iPhone(version: String)
    case iPad(version: String)
    case macBook
    case iMac
}

var devices: [Device] = []
devices.append(Device.iMac)
devices.append(Device.iPhone(version: "14.3"))
devices.append(Device.iPhone(version: "6.1"))
devices.append(Device.iPad(version: "10.3"))
devices.append(Device.macBook)

let sortedDevices: [Device] = devices.sorted()
print(sortedDevices)
// [Device.iPhone(version: "14.3"), Device.iPhone(version: "6.1"),
// Device.iPad(version: "10.3"), Device.macBook, Device.iMac]

 

 

 

스위프트 프로그래밍: Swift 5 - 교보문고

객체지향, 함수형, 프로토콜 지향 패러다임까지 한 번에! | 스위프트를 제대로 이해하고 싶은 개발자를 위한 책스위프트는 iOS와 macOS용 애플리케이션 개발에 주로 사용하는 프로그래밍 언어입니

www.kyobobook.co.kr

 

반응형

댓글