[스위프트 프로그래밍-4장] 데이터 타입 고급

sanghee·2021년 9월 13일
0
post-thumbnail

이 글은 스위프트 프로그래밍(3판, 야곰 지음)을 읽고 간단하게 정리한 글입니다. 책에 친절한 설명과 관련 예제 코드들이 있으므로 직접 사서 읽기를 권장합니다.

4.1 데이터 타입 안심

스위프트는 안정성을 중요시하기에 타입에 굉장히 민감하다. 서로 다른 타입끼리 데이터 교환이 이루어지기 위해서는 타입 캐스팅, 엄밀히 말하면 새로운 인스턴스를 생성한 후 할당해야 한다.

4.1.1 데이터 타입 안심이란

스위프트는 Type-safe 언어로 타입을 안심하고 사용할 수 있다. 변수에 다른 타입의 값을 할당할 시 스위프트는 타입을 확인하여 컴파일 오류가 발생시킨다.

*에러종류

컴파일(compile) 에러: 코드를 변역하는 과정에서 일어나는 에러를 의미한다. 문법 오류, 데이터 타입 에러, 함수 선선 에러 등이 해당된다.

런타임(runtime) 에러: 말 그대로 실행시에 일어나는 에러이다.

논리 에러: 의도하지 않은 대로 결과가 나오는 경우를 의미한다.

4.1.2 타입 추론

변수나 상수를 선언할 때 타입을 명시하지 않아도 컴파일러가 타입을 추론해서 결정한다. name의 경우 타입을 명시하지 않았지만, "a"이라는 값을 통해 컴파일러가 추론해 name이 String타입이라고 결정하였다. 그래서 name에 Int타입을 할당하려고 하면 에러가 발생한다. 그러나 원하는 타입으로 추론되지 않거나 초기값이 없는 경우에 문제가 발생할 수 있다. 또한 컴파일러가 타입을 추론하는 과정이 추가되므로 가급적 타입은 명시하는 것이 좋다.

var name = "a"
name = 1 // 에러 발생

let float = 1.0
type(of: float) // Float 타입을 원했으나 Double타입으로 추론됨

let character = "A"
type(of: character) // Character 타입을 원했으나 String타입으로 추론됨

4.2 타입 별칭

타입 별칭(typealias)에서 alias는 가명, 별명이라는 뜻을 가지며 말 그대로 타입에 별명을 붙이는 것을 뜻한다. Int타입에 MyInt와 YourInt라는 별명을 붙였다. myAge는 MyInt타입이고 yourAge는 YourInt타입으므로 서로 다르지만, Int이기에 같은 타입으로 취급한다.

typealias MyInt = Int
typealias YourInt = Int

let myAge: MyInt = 10
let yourAge: YourInt = 11

myAge = yourAge
print(myAge) // 11

4.3 튜플

튜플은 지정된 데이터의 묶음으로 소괄호 안에 타입의 나열로 생성이 가능하다. 타입 이름은 따로 지정되지 않는다. 그러나 personA에서 보는 대로 String타입의 데이터가 무엇을 의미하는지 등을 쉽게 파악할 수 없다. 따라서 personB처럼 튜플의 요소마다 이름을 붙여줄 수 있다. 데이터에는 인덱스나 별칭으로 접근한다. 또는 personC처럼 튜플에 PersonTuple이라는 별칭을 주어 코드를 작성할 수 있다.

let personA: (String, Int) = ("a", 10)
let personB: (name: String, age: Int) = ("b", 11)

personA.0 // a
personA.1 // 10
personB.name // b
personB.0 // b
personB.1 // 11
personB.2 // error

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

let personC: PersonTuple = ("c", 12)

+ 튜플과 구조체, 성능 차이

사람이라는 상수를 만들 때 위와 같이 튜플을 활용할 수 있지만, 구조체를 사용할 수 있다.

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

let personTuple: PersonTuple = ("a", 10)
personTuple.name // a
struct PersonStruct {
    let name: String
    let age: Int
}

let personStruct: PersonStruct = PersonStruct(name: "b", age: 11)
personStruct.name // b

튜플과 구조체가 걸린 시간이 비슷함을 알 수 있다. 다만 typealias를 추가하면 추가하기 전보다 오래덜렸다.

func duration(_ block: () -> ()) -> TimeInterval {
  let startTime = Date()
  block()
  return Date().timeIntervalSince(startTime)
}

let tupleTime1 = duration {
    let personTuple: (name: String, age: Int) = ("a", 10)
}

let tupleTime2 = duration {
    typealias PersonTuple = (name: String, age: Int)
    let personTuple: PersonTuple = ("a", 10)
}

let structTime = duration {
    struct PersonStruct {
        let name: String
        let age: Int
    }
    let personStruct: PersonStruct = PersonStruct(name: "b", age: 11)
}

print(tupleTime1 < tupleTime2) // false
print(tupleTime1 < structTime) // false, true 번갈아 나옴

4.4 컬렉션형

컬렉션형 타입에는 배열, 딕셔너리, 세트 등이 있다.

  • 컬렉션형 타입.randomElement(): 임의의 요소를 반환한다.
  • 컬렉션형 타입.suffle(): 요소를 임의로 뒤섞는다.

4.4.1 Array

배열은 일렬로 나열한 후 순서대로 저장한다. 각 요소에 인덱스를 통해 접근할 수 있으며 잘못된 인덱스로 접근하는 경우 인셉션 오류(exception error)가 발생한다.

  • 배열.first, 배열.last: 맨 처음과 맨 마지막 요소를 반환한다.
  • 배열.firstIndexOf(요소): 해당 요소의 인덱스를 반환한다.
  • 배열.append(요소), append(contents Of: [요소A, 요소B]): 요소 또는 요소 배열을 맨 뒤에 추가한다.
  • 배열.insert(요소, at: 인덱스), 배열.insert(contentsOf: [요소A, 요소B], at: 인덱스): 해당 인덱스에 요소 또는 요소 배열을 추가한다.
  • 배열.removeFirst(), 배열.removeLast(): 맨 앞의 요소 또는 맨 뒤의 요소를 삭제하고 반환한다.
  • 배열.remove(at: 인덱스): 해당 인덱스의 요소를 삭제하고 반환한다.

4.4.2 Dictionary

딕셔너리는 키와 쌍으로 이루어진 컬렉션 타입이다. 키는 값을 대변하는 식별자이므로 중복할 수 없다. 각 값에 키로 접근할 수 있으며, 배열과는 다르게 없는 키로 접근해도 오류가 발생하지 않고 nil을 반환한다.

  • 딕셔너리[키]: 해당 키에 해당하는 값을 반환하거나 없을 경우 nil을 반환한다.
  • 딕셔터리[키, default: 기본값]: 해당 키에 해당하는 값을 반환하거나 없을 경우 기본값을 반환한다.
  • 딕셔너리.removeValue(forkey: 키): 해당 키에 해당하는 값을 반환하거나 없을 경우 nil을 반환한다.

4.4.3 Set

세트는 순서 없이 하나의 묶음으로 저장하는 컬렉션 타입이다. 모두 유일되는 값으로 중복되지 않는다. 따라서 순서가 중요하지 않으며 각 요소가 유일한 경우에 사용한다. 세트의 요소로는 해시 가능한 값들이 와야 한다. 배열과 딕셔너리와 달리 축약형으로 표현할 수 없다.

순서가 있는 세트를 원한다면 orderedSet을 사용한다.

*hashable: 해시가 가능한 성질이다. 즉 키를 이용해 값에 접근이 가능함을 의미한다.

  • 세트.insert(요소), 세트.remove(요소): 세트에 요소를 추가하거나 삭제한다.
var orderedSet = OrderedSet(["a", "b", "c"])

orderedSet[1] // b

세트는 집합관계를 표현하기 유용하다.

  • 세트A.intersection(세트B): 세트A와 세트B의 교집합을 반환한다.
  • 세트A.symmetricDifference(세트B): 세트A와 세트B의 여집합의 합을 반환한다.
  • 세트A.union(세트B): 세트A와 세트B의 합집합을 반환한다.
  • 세트A.subtracting(세트B): 세트A와 세트B의 차집합을 반환한다.
  • 세트A.isDisjoint(with: 세트B): 세트A와 세트B가 서로 배타적인지(교집합이 없는지)를 반환한다.
  • 세트A.isSubset(of: 세트B): 세트A가 세트B의 부분집합인지를 반환한다.
  • 세트A.isSuperset(of: 세트B): 세트A가 세트B의 전체집합인지(전부 포함하는 지)를 반환한다.

4.5 열거형

열거형은 연관된 항목들을 묶어 표현하는 타입이다. 추가 및 수정이 불가능하다. 제한된 선택지를 주고 싶을 때, 정해진 값 외에는 입력받지 않을 때, 예상된 입력값이 한정되어 있을 때 사용한다. 스위프트의 열거형은 각 항목이 그 자체로 고유한 값을 가질 수 있다. 각 항목이 원시값(raw value)라는 형태로 실제 값을 가지는 경우도 있다. switch 구문과 함께 사용하면 좋다. enum이라는 키워드로 설정한다.

4.5.1~2 기본 열거형, 원시값(raw value)

각 항목(case)은 그 자체가 고유이다. 원시값은 일부만 줄 수도 있다. 문자열 형식의 원시값이라면 각 항목의 이름을, 정수 타입이면 첫 항목 0부터 1씩 늘어는 정수값을 가진다. 열거형의 원시값을 통해 열겨형 변수나 상수를 생성할 수 있다. 만약 원시값이 올바르지 않은 경우 nil을 반환한다.

enum School: String {
    case high = "고등학교"
    case universary
}

School.high.rawValue // "대학교"
School.universary.rawValue // "universary"

enum Number: Int {
    case zero = 0
    case one
    case two
}

Number.zero.rawValue // 0
Number.one.rawValue // 1

Number(rawValue: 0) // zero
Number(rawValue: 1) // one
Number(rawValue: 3) /// nil

4.5.3 연관 값

열거형 내의 항목은 자신과 연관된 값을 가질 수 있다.

enum Dish {
    case pizza(dough: Dough, topping: Topping)
    case chichen(withSauce: Bool)
    case rice
}

enum Dough {
    case original, cheeseCrust
}

enum Topping {
    case original, cheese, bacon
}

var dinner: Dish = Dish.pizza(dough: Dough.cheeseCrust, topping: Topping.original)
dinner = Dish.chichen(withSauce: true)
dinner = Dish.rice

4.5.4 항목 순회

열거형에 포함된 모든 항목들을 알아야 할때 CaseIterable 프로토콜을 채택한다. 그 후 열거형에 allCases 타입 프로퍼티를 통해 모든 케이스의 컬렉션을 생성해준다. 그러나 플랫폼별로 사용 조건을 추가하는 경우 등에는 실행 환경에 따라 일부 케이스 컬렉션이 생성될 수 있다.

*obsoleted: 선언이 폐기된 지정된 플랫폼 또는 언어의 첫번째 버전을 나타낸다.

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

let allCases: [School] = School.allCases
// 실행 환경에 따라 [high, universary] 또는 [high, universary, graduate]

4.5.5 순환 열거형

순환 열거형은 열거형 항목의 연관 값이 자신의 값이고자 할 때 사용한다. indirect 키워드를 사용한다. 열거형 전체에 적용하고 싶다면 enum 키워드 앞에, 특정 항목에 적용하고 싶다면 case키워드 앞에 명시한다. indirect 키워드는 이진 탐색 트리 등의 순환 알고리즘을 구현할 때 사용할 수 있다.

indirect enum MathExpression {
    case number(Int)
    case add(MathExpression, MathExpression)
    case multiply(MathExpression, MathExpression)
}

func evaluate(_ expression: MathExpression) -> Int {
    switch expression {
    case .number(let value): return value
    case .add(let left, let right): return evaluate(left) + evaluate(right)
    case .multiply(let left, let right): return evaluate(left) * evaluate(right)
    }
}

let two = MathExpression.number(2)
let three = MathExpression.number(3)
let four = MathExpression.number(4)

let sum = MathExpression.add(two, three)
let final = MathExpression.multiply(sum, four)

let result = evaluate(final) // 20

4.5.6 비교 가능한 열거형

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

enum Number: Comparable {
    case zero
    case one
    case two
}

print(Number.zero < Number.one) // true
profile
👩‍💻

0개의 댓글