[CH4.4.2~ CH 4.5] 데이터 타입 고급

Tabber·2021년 9월 9일
0
post-thumbnail

딕셔너리

딕셔너리는 요소들이 순서 없이 키와 값의 쌍으로 구성되는 컬렉션 타입이다.
딕셔너리에 저장되는 값은 항상 키와 쌍을 이루게 되는데, 딕셔너리 안에는 키가 하나이거나 여러개일 수 있다.

단, 하나의 딕셔너리 안의 키는 같은 이름을 중복해서 사용할 수 없다.
쉽게 말해, "ITlearning"이라는 키가 두번 쓰일 수 없다라는뜻이다.

딕셔너리는 Dictionary 라는 키워드와 키의 타입과 값의 타입 이름의 조합으로 써준다.
대괄호로 키와 값의 타입 이름의 쌍을 묶어 딕셔너리 타입을 표현한다.

let 키워드를 사용하여 상수로 선언하면 변경 불가능한 딕셔너리가 되고, var 키워드를 사용하여 변수로 선언해주면 변경 가능한 딕셔너리가 된다.

빈 딕셔너리는 이니셜라이저 또는 리터럴 문법을 통해서 생성할 수 있다. isEmpty 프로퍼티를 통해 비어있는 딕셔너리인지 확인할 수가 있다. 그리고 count 프로퍼티로 딕셔너리의 요소 개수를 확인할 수 있다.

import Foundation

// MARK: - 딕셔너리의 선언과 생성
// 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] = ["ITlearning": 100, "Chulsoo": 200, "Jenny": 300]

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

딕셔너리는 값 값에 키로 접근할 수 있다. 딕셔너리 내부에서 키는 유일해야 하며, 값은 유일하지 않다.
딕셔너리는 배열과 다르게 딕셔너리 내부에 없는 키로 접근해도 오류가 발생하지 않는다. 다만 nil을 반환한다.

특정 키에 해당하는 값을 제거하려면, removeValue(forKey:) 메서드를 사용한다. 키에 해당하는 값이 제거된 후 반환된다.

// MARK: - 딕셔너리의 사용
print(numberForName5["Chulsoo"]) // 200
print(numberForName5["Minji"])   // nil

// Edit
numberForName5["Chulsoo"] = 150
print(numberForName5["Chulsoo"]) // 150

// Add
numberForName5["Max"] = 999
print(numberForName5["Max"]) // 999

// remove
print(numberForName5.removeValue(forKey: "ITlearning")) // 100

// 위에서 ITlearning 키에 해당하는 값이 이미 삭제되었으므로 nil이 반환된다.
print(numberForName5.removeValue(forKey: "ITlearning")) // nil

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

세트

세트는 같은 타입의 데이터를 순서 없이 하나의 묶음으로 저장하는 형태의 컬렉션 타입이다.

세트 내의 값은 모두가 유일한 값, 즉 중복된 값이 존재하지 않는다.
그래서 세트는 보통 순서가 중요하지 않거나 각 요소가 유일한 값이어야 하는 경우에 사용한다.
또, 세트의 요소로는 해시 가능한 값이 들어와야 한다.

Hashable

세트는 Set 키워드와 타입 이름의 조합으로 써준다.
또, 배열과 마찬가지로 대괄호로 값들을 묶어 세트 타입임으로 표현한다.

배열과 달리 줄여서 표현할 수 있는 축약형(예를 들자면 Array를 [Int] 로 축약하던)이 없다. let 선언시 변경 불가능, var 선언시 변경 가능한 Set가 된다.

Set 또한 isEmpty 프로퍼티를 사용해 비어있는 세트인지 확인할 수 있다.

import Foundation

var names1: Set<String> = Set<String>() // 빈 세트 생성
var names_small: Set<String> = []      // 동일한 빈 세트 생성

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

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

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

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

-- 위 코드 이어서 -
print(names.count) // 3
names.insert("Jenny")
print(names.count) // 4

print(names.remove("Chulsoo")!) // Chulsoo
print(names.remove("John"))    // nil

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

// MARK: - 세트의 활용 - 포함관계 연산
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 열거형(Enum)

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

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

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

스위프트의 열거형은 항목별로 값을 가질 수도, 가지지 않을 수도 있다. 예를 들어 C언어는 열거형의 각 항목 값이 정수 타입으로 기본 지정되지만, 스위프트의 열거형은 각 항목이 그 자체로 고유의 값이 될 수 있다.

기존의 C언어 등에서 열거형은 주로 정수 타입 값의 별칭 형태로 사용될 뿐이었다. 그렇기 때문에 모든 열거형의 데이터 타입은 같은 타입(정수 타입 같은) 으로 취급한다. 이는 열거형 각각이 고유의 타입으로 인식될 수 없다는 문제 때문에, 여러 열거형을 사용할 때 프로그래머의 실수로 인한 버그가 생길 수 있다.

그러나 스위프트의 열거형은 각 열거형이 고유의 타입으로 인정되기 때문에 실수로 버그가 일어날 가능성을 원천 봉쇄할 수 있다.

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

4.5.1 기본 열거형

import Foundation

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

School 이라는 이름을 갖는 열거형에는 위 코드에 정의한 것처럼 있다.

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

// MARK: - 열거형 변수의 생성 및 값 변경
var highestEducationLevel: School = School.university

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

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

4.5.2 원시 값(Raw Value)

열거형의 각 항목은 자체로도 하나의 값이지만 항목의 원시 값(Raw Value)도 가질 수 있다.

즉, 특정 타입으로 지정된 값을 가질 수 있다는 뜻이다. 특정 타입의 값을 원시 값으로 가지고 싶다면, 열거형 이름 오른쪽에 타입을 명시해 주면 된다. 또, 원시 값을 사용하고 싶다면 rawValue라는 프로퍼티를 통해 가져올 수 있다.

import Foundation

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

let highestEducationLevel: School = 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씩 늘어난 값을 갖게 된다.

import Foundation

// MARK: - 열거형의 원시 값 일부 지정 및 자동 처리
enum School: String {
    case primary = "유치원"
    case elementary = "초등"
    case middle = "중등"
    case high = "고등"
    case collage
    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을 반환한다.

이는 실패 가능한 이니셜라이저 기능이다.

// MARK: - 원시 값을 통한 열거형 초기화
let primary = School(rawValue: "유치원") // primary
let grauate = School(rawValue: "석박사") // nil

let one = Numbers(rawValue: 1)   // one
let three = Numbers(rawValue: 3) // nil

4.5.3 연관 값

스위프트의 열거형 각 항목이 연관 값을 가지게 되면, 기존 프로그래밍 언어의 공용체 형태를 띌 수도 있다.

열거형 내의 항목(case)이 자신과 연관된 값을 가질 수 있다. 연관 값은 각 항목 옆에 소괄호로 묶어 표현할 수 있다. 다른 항목이 연관 값을 갖는다고 모든 항목이 연관 값을 가질 필요는 없다.

import Foundation

// MARK: - 연관 값을 갖는 열거형
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 // 밥

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

import Foundation

// MARK: - 여러 열거형 응용
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 = MainDish.pizza(dough: PizzaDough.cheeseCrust, topping: PizzaTopping.bacon)

4.5.4 항목 순회

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

CaseIterable

import Foundation

// MARK: - 여러 열거형 응용
enum School : CaseIterable {
    case primary    // 유치원
    case elementary // 초등
    case middle     // 중등
    case high       // 고등
    case collage    // 대학
    case university // 대학교
    case graduate   // 대학원
}
 
let allCase: [School] = School.allCases
print(allCase)

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

import Foundation

// MARK: - available 속성을 갖는 열거형의 항목 순회
enum School: String, CaseIterable{
    case primary = "유치원"
    case elementary = "초등"
    case middle = "중등"
    case high = "고등"
    case collage = "대학"
    case university = "대학교"
    @available(iOS, obsoleted: 12.0)
    case graduate = "대학원"
    
    static var allCases: [School] {
        let all: [School] = [.primary,
                             .elementary,
                             .middle,
                             .high,
                             .collage,
                             .university]
        #if os(iOS)
        return all
        #else
        return all + [.graduate]
        #endif
    }
}
 
let allCase: [School] = School.allCases
print(allCase)
// 실행 환경에 따라 다른 결과

#if 등의 표현은 부록의 [조건부 컴파일 블록]을 알아야한다.


조건부 컴파일 블록

조건부 컴파일 블록에서 사용하는 키워드는 #if #elseif #endif 등이 존재한다.
조건부 컴파일 블록을 사용하면 컴파일 조건에 맞는 코드는 컴파일 단계에서 포함시키고, 그렇지 않은 코드는 컴파일 하지 않는다.

모든 조건부 컴파일 블록은 #if 로 시작하여 #endif 로 끝난다. #if 외에 다른 조건을 추가하고 싶다면 #elseif 를 사용한다. if-else 구문과 거의 비슷한데, 다만 #endif 가 마지막에 꼭 따라붙어야 하며 프로그램 실행 중에 동작하는 것이 아니라, 컴파일할 때 영향을 준다.

#if 컴파일 조건 1
// 컴파일 조건 1이 참이면 컴파일될 코드
#elseif 컴파일 조건 2
// 컴파일 조건 1이 거짓이고 컴파일 조건 2가 참이면 컴파일될 코드
#else
// 컴파일 조건 1과 컴파일 조건 2가 모두 거짓인 경우 컴파일될 코드
#endif

컴파일 조건은 Boolean 타입의 값이 들어갈 수 있으며, 빌드 플래그 값이 들어갈 수도 있고, 플랫폼이나 언어 버전을 확인하는 함수가 들어갈 수도 있다. 버전을 확인하는 함수 중 스위프트 버전 확인 함수swift() 함수와 컴파일러 버전 확인 함수compiler() 함수 전달인자 값을 전달할 때는 공백이 포함되면 안 된다.

또 비교연산자는 ≥와 <만 사용할 수 있다.

import Foundation
#if os(Linux)
    print("이 프로그램은 리눅스 환경을 위해 컴파일 했습니다.")
#elseif os(macOS)
    print("이 프로그램은 macOS 환경을 위해 컴파일 했습니다.")
#elseif os(iOS)
    print("이 프로그램은 iOS 환경을 위해 컴파일 했습니다.")
#else
    print("플랫폼을 인식할 수 없습니다.")
#endif

#if DEBUG
    print("DEBUG 환경으로 컴파일 했습니다.")
#elseif TEST_RELEASE
    print("TEST RELEASE 환경으로 컴파일 했습니다.")
#else
    print("RELEASE 환경으로 컴파일 했습니다.")
#endif

#if swift(>=3.0)
    print("Swift 3.0 과 같거나 높은 버전의 환경에서 컴파일 했습니다.")
#elseif swift(>=2.0)
    print("SWift 2.0 과 같거나 높은 버전의 환경에서 컴파일 했습니다.")
#else
    print("Swift 2.0 미만 버전의 환경에서 컴파일 했습니다.")
#endif

#if os(macOS) && swift(<5.0)
    print("macOS를 위해 Swift 5.0보다 낮은 버전의 환경에서 컴파일 했습니다.")
#elseif os(Linux) || swift(>=3.0)
    print("Linux를 위해 빌드되었거나 Swift 3.0과 같거나 높은 버전의 환경에서 컴파일 했습니다.")
#endif

#if canImport(UIKit)
    print("UIKit을 사용할 수 있습니다.")
#elseif canImport(AppKit)
    print("AppKit을 사용할 수 있습니다.")
#else
    print("UIKit과 AppKit을 사용할 수 없습니다.")
#endif

#if targetEnvironment(simulator)
    print("시뮬레이터 환경으로 컴파일 했습니다.")
#endif

// Mark: - Result
// 이 프로그램은 iOS 환경을 위해 컴파일 했습니다.
// RELEASE 환경으로 컴파일 했습니다.
// Swift 3.0과 같더나 높은 버전의 환경에서 컴파일 했습니다.
// Linux를 위해 빌드되었거나 Swift 3.0과 같거나 높은 버전의 환경에서 컴파일 했습니다.
// UIKit을 사용할 수 있습니다.
// 시뮬레이터 환경으로 컴파일 했습니다.

또 한가지 알아둘 점은 각각의 조건부 컴파일 블록 내부의 코드들은 컴파일이 되든 되지않든 간에 문법 검사를 하는데, 예외적으로 스위프트 버전 검사를 하는 조건부 컴파일 블록 내부의 코드는 문법 검사를 하지 않는다. 언어의 버전이 변경됨에 따라 변경된 문법이 컴파일 오류로 처리되지 않게 하기 위함이다.

// Mark - 스위프트 및 컴파일러 버전 확인 조건부 컴파일 블록의 사용
import Foundation

var i: Int = 0

#if swift(>=2.2)
    i += 1
#else
    i++
#endif

print(i) // 1

#if compiler(<2.2)
    i++
#else
    i += 1
#endif

print(i) //2

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

import Foundation

// MARK: - 여러 열거형 응용
enum PastaTaste : CaseIterable {
    case cream, tomato
}

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

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

enum MainDish : CaseIterable {
    case pasta(taste: PastaTaste)
    case pizza(dough: PizzaDough, topping: PizzaTopping)
    case chicken(withSauce: Bool)
    case rice
    
    static var allCases: [MainDish] {
        return PastaTaste.allCases.map(MainDish.pasta) + PizzaDough.allCases.reduce([]) { (result, dough) -> [MainDish] in
            result + PizzaTopping.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 키워드를 붙이면 된다.

다음 코드는 산술 연산을 위해 정의한 열거형(enum)이다.

import Foundation

// MARK: - 특정 항목에 순환 열거형 항목 명시
enum ArithmeticExpression {
    case number(Int)
    indirect case addition(ArithmeticExpression, ArithmeticExpression)
    indirect case multiplication(ArithmeticExpression,ArithmeticExpression)
}
// MARK: - 열거형 전체에 순환 열거형 명시
indirect enum ArithmeticExpression {
    case number(Int)
    case addition(ArithmeticExpression,ArithmeticExpression)
    case multiplocation(ArithmeticExpression,ArithmeticExpression)
}

두 번째 코드의 열거형에는 정수를 연관 값으로 갖는 number 라는 항목이 있고 덧셈을 위한 addition 이라는 항목, 곱셈을 위한 multiplication 항목이 있다.

import Foundation

// MARK: - 순환 열거형의 사용
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 evaluate(_ expression: ArithmeticExpression) -> Int {
    switch expression {
    case .number(let value):
        return value
    case .addition(let left, let right):
        return evaluate(left) + evaluate(right)
    case .multiplication(let left, let right):
        return evaluate(left) * evaluate(right)
    }
}

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

위 코드는 ArithmeticExpression 열거형을 사용하여 (5 + 4) * 2 연산을 구현해보는 예제이다.

evaluateArithmeticExpression 열거형의 계산을 도와주는 재귀함수(Recursive Function) 이다.

indirect 키워드는 예제 뿐만 아니라, 이진 탐색 트리 등의 재귀 알고리즘을 구현할 때 유용하게 사용할 수 있다.

profile
iOS 정복중인 Tabber 입니다.

0개의 댓글