Swift 정리 - 4. 데이터 타입 고급
Swift의 더 많은 데이터 타입을 알아보자.
데이터 타입 안심
Swift.org:Type Safety and Type Inference
Swift가 뭔지 처음 공부했을 때도 Swift의 세 가지 특성 중 하나가 바로 safe였다. Swift는 type-safe 언어로, 타입에 굉장히 민감하고 엄격하다. 따라서 다른 타입끼리의 데이터 교환은 꼭 Type-Casting, 형변환을 거쳐야 한다. Swift에서의 값 타입의 데이터 교환은 엄밀히 말하면 타입캐스팅이 아닌 새로운 인스턴스를 생성해 할당하는 것이다.
데이터 타입 안심이란?
Swift는 데이터 타입을 안심하고 사용할 수 있는 Type-safe 언어다. Type-safe한 언어는 내가 작업하는 값들의 타입을 분명하게 할 수 있다는 걸 의미한다. 또 타입을 안심하고 사용할 수 있다는 것은 발생할 수 있는 에러르 방지해서 실수를 줄일 수 있다는 걸 의미한다. 예를 들어 코드에서 String
을 요구하는데 실수로 Int
값을 할당할 수 없다.
Swift가 type safe하기 때문에, 코드를 컴파일할 때 type check를 하고 맞지 않는 타입들을 에러로 표시한다. 이를 통해 개발 단계에서 에러들을 찾고 고칠 수 있다.
타입 추론
Type-checking을 통해 다른 타입의 값들을 작업할 때 에러를 방지할 수 있다. 하지만 이게 내가 생성하는 모든 상수와 변수의 타입을 명시해야 한다는 의미는 아니다. 만약 타입을 내가 명시하지 않으면 Swift는 type inference를 통해 적절한 타입을 지정해준다. 타입 추론은 내가 제공한 값들을 검사해서 코드를 컴파일 할 때 특정 표현의 타입을 컴파일러가 추론할 수 있게 해준다.
Type Aliases
Swift에서는 기본적으로 제공된느 타입이든, 사용자가 새로 지정한 타입이든 이미 존재하는 타입에 다른 이름(별칭)을 부여할 수 있다. 별칭을 부여하고 나서도 기본 타입 이름과 별칭을 모두 사용할 수 있다.
별칭은 typealias
키워드로 정의할 수 있다. 타입 별칭은 이미 존재하는 타입 이름을 문맥상 더 적절한 이름으로 부르고 싶을 때 유용하다. 특히 아래와 같이 외부 소스의 특정 사이즈의 데이터를 작업할 때 유용하다.
Tuples
Tuple은 여러 값들을 하나의 합쳐진 값으로 만든다. 타입의 이름이 따로 지정되어 있지 않고, 개발자 마음대로 만드는 타입이다. 즉 지정된 데이터의 묶음이라고 할 수 있다. 튜플 내의 값들은 어떤 타입이든 될 수 있고, 값들이 같은 타입이 될 피룡가 없다.
Swift의 튜플은 파이썬의 튜플과 유사하다. 타입 이름이 따로 없기 때문에 일정 타입의 나열만으로 튜플을 생성할 수 있다. 또한 튜플에 포함될 데이터의 개수도 자유롭게 정할 수 있다.
예를 들어 아래의 예제에서 (404, "Not Found")
는 HTTP 상태 코드를 나타내는 튜플이다. 튜플의 컨텐츠를 별개의 상수나 변수로 분해해서 이후에 이를 사용할 수도 있다.
그리고 만약 튜플의 값 중 일부만 필요하다면 튜플을 분해할 때 무시할 부분에 underscore(_)를 붙여 무시한다.
대신에 튜플 내의 요소들을 0부터 시작하는 인덱스 번호를 사용해서 접근할 수도 있다.
위처럼 튜플의 각 요소를 숫자를 통해 접근하면 간편하긴 하지만 다른 프로그래머가 코드를 보고 각 요소에 어떤 의미가 있는지 파악하기 힘들 수 있다. 따라서 튜플 내의 요소들에 이름을 붙여서 나중에 접근할 수도 있다.
튜플은 특히 함수의 리턴 값으로 쓰일 때 유용하다. 웹페이지를 받는 함수는 페이지를 가져오는 데 성공했는지 실패했는지를 알리기 위해 (Int, String)
튜플 타입을 리턴해야 할 수 있다. 다른 타입의 다른 값을 담은 튜플을 리턴해서 함수는 오직 하나의 값만 리턴할 수 있을 때보다 더 많은 정보를 제공할 수 있다.
튜플은 연관된 값들의 집합에서 유용하게 사용될 수 있다. 튜플은 복잡한 데이터 구조의 생성에 적합하지 않기 때문에, 만약 더 복잡한 데이터 구조를 원한다면 클래스나 구조체로 설계하길 바란다.
Collection Types
Swift은 값들의 컬렉션을 저장하는데 세 개의 주요 collection type을 제공하는데, 배열, 집합, 딕셔너리다. 배열은 값들의 순서가 있는 컬렉션이다. 집합은 유일한 값들을 저장하기 위한 순서가 없는 컬렉션이다. 딕셔너리는 키-값 연관성을 통해 값들을 저장하는 순서가 없는 컬렉션이다.
Swift의 배열, 집합, 딕셔너리는 항상 그들이 저장할 수 있는 값과 키의 타입을 분명하게 해야 한다. 이는 실수로 다른 타입을 저장할 수 없게 방지해준다. 또한 컬렉션에서 값을 받을 때 타입이 분명하기 때문에 헷갈리지 않고 사용할 수 있다.
Swift의 배열, 집합, 딕셔너리 타입은 제네릭 컬렉션으로 구현되어 있다. 제네릭에 대해서는 이후에 볼 것이다.
Mutability of Collections
만약 배열, 집합, 딕셔너리를 생성하고 변수에 할당한다면 컬렉션은 mutable하게 생성된 것이다. 이는 컬렉션을 생성한 후 값들을 추가하거나, 삭제하거나, 바꾸는 등으로 컬렉션을 바꿀 수 있다는 소리다. 만약 배열, 집합, 딕셔너리를 상수에 할당한다면 이는 immutable한 것이고, 크기와 컨텐츠는 바꿀 수 없다.
컬렉션이 바뀔 필요가 없다면 컬렉션을 불변하게 만드는 것이 좋다. 왜냐하면 변하지 않는 것을 굳이 변수로 선언할 필요도 없고 내가 생성한 컬렉션의 성능을 Swift 컴파일러가 최적화할 수 있기 때문이다.
Arrays
배열은 같은 타입의 값들을 일렬로 나열한 후 순서대로 저장하는 형태의 컬렉션이다. 같은 값들이 여러 다른 위치에서 등장할 수도 있다.
Swift의
Array
타입은 Foundation의NSArray
클래스와 연결되어 있다.
Array Type Shorthand Syntax
Swift 배열의 타입은 Array<Element>
로, Element
에 값들의 타입을 써서 작성한다. 또 [Element]
와 같이 간단하게 작성할 수도 있다. 주로 후자의 방법이 선호된다.
Creating an Empty Array
이니셜라이저 문법을 사용해서 특정 타입의 빈 배열을 만들 수 있다.
var someInts: [Int] = []
print("someInts is of type [Int] with \(someInts.count) items.")
// Prints "someInts is of type [Int] with 0 items."
만약 문맥상 이미 타입 정보가 주어졌다면 그냥 아래와 같이 빈 배열 리터럴을 사용해서 빈 배열을 생성할 수 있다.
someInts.append(3)
// someInts now contains 1 value of type Int
someInts = []
// someInts is now an empty array, but is still of type [Int]
Creating and Array with a Default Value
Swift의 Array
타입은 default 값들과 배열의 사이즈를 설정해서 배열을 생성할 수 있게 하는 이니셜라이저를 제공한다. 이 이니셜라이저의 repeating
부분에 기본 값을 전달하고, count
에 이 값이 얼마나 반복될지를 설정할 수 있다.
var threeDoubles = Array(repeating: 0.0, count: 3)
// threeDoubles is of type [Double], and equals [0.0, 0.0, 0.0]
Creating an Array by Adding Two Arrays Together
양립 가능한 타입의 이미 존재하는 두 배열을 덧셈 연잔자로 더해 새로운 배열을 만들 수 있다. 새로운 배열의 타입은 더한 두 배열의 타입에서 추론된다.
var anotherThreeDoubles = Array(repeating: 2.5, count: 3)
// anotherThreeDoubles is of type [Double], and equals [2.5, 2.5, 2.5]
var sixDoubles = threeDoubles + anotherThreeDoubles
// sixDoubles is inferred as [Double], and equals [0.0, 0.0, 0.0, 2.5, 2.5, 2.5]
Creating an Array with an Array Literal
배열 컬렉션에 하나 이상의 값을 간단히 쓸 수 있는 배열 리터럴로 배열을 생성할 수도 있다. 배열 리터럴은 대괄호 안에 콤마로 구분된 값들을 써서 표현한다.
var shoppingList: [String] = ["Eggs", "Milk"]
// shoppingList has been initialized with two initial items
shoppingList
변수는 문자열 값들의 배열로 선언되었고, [String]
으로 작성되었다. 이 배열이 String
타입의 값만 허용하기 때문에, String
값들만 넣을 수 있고 예시에서 볼 수 있듯이 두 개의 String
값들로 배열이 초기화되었다.
Swift에는 타입 추론이 있기 때문에 같은 타입의 값들을 포함한 배열 리터럴로 배열을 초기화할 때 배열의 타입을 쓸 필요는 없다. 즉 shoppingList
배열은 아래와 같이 더 간단하게 쓸 수 있다.
var shoppingList = ["Eggs", "Milk"]
배열 리터럴에 있는 모든 값들이 같은 타입이기 때문에 Swift는 shoppingList
변수의 타입으로 [String]
을 추론할 수 있는 것이다.
Accessing and Modifying and Array
- 메서드
- 프로퍼티
- subscript 문법
을 통해서 배열에 접근하고 수정할 수 있다.
배열의 원소 개수를 알고 싶으면 읽기 전용인 count
프로퍼티를 확인한다.
print("The shopping list contains \(shoppingList.count) items.")
// Prints "The shopping list contains 2 items."
불리언 타입의 isEmpty
프로퍼티를 사용해서 count
가 0인지 확인할 수 있다.
if shoppingList.isEmpty {
print("The shopping list is empty.")
} else {
print("The shopping list isn't empty.")
}
// Prints "The shopping list isn't empty."
append(_:)
메서드를 사용해서 배열의 끝에 새 요소를 추가할 수 있다.
shoppingList.append("Flour")
// shoppingList now contains 3 items, and someone is making pancakes
대신, 하나 이상의 양립 가능한 아이템들을 +=
연산자를 통해 배열에 추가할 수 있다.
shoppingList += ["Baking Powder"]
// shoppingList now contains 4 items
shoppingList += ["Chocolate Spread", "Cheese", "Butter"]
// shoppingList now contains 7 items
subscript 문법을 통해 값을 받을 수 있는데, 내가 받고 싶은 인덱스를 배열 뒤에 붙인 대괄호 안에 전달하면 된다.
var firstItem = shoppingList[0]
// firstItem is equal to "Eggs"
배열의 첫 번째 요소의 인덱스는 0이다. Swift의 배열은 항상 0-인덱스로 되어있다.
주어진 인덱스에 위치한 값을 바굴 때도 subscript 문법을 사용할 수 있다.
shoppingList[0] = "Six eggs"
// the first item in the list is now equal to "Six eggs" rather than "Eggs"
subscript 문법을 사용할 때 사용하는 인덱스는 유효한 인덱스여야 한다. subscript 문법을 사용해서 범위 내의 값들을 한 번에 바꿀 수 있다. 심지어 바꾸려는 값들의 개수가 범위에 포함된 원소의 개수와 다르더라도 가능하다. 아래의 예시는 “Chocolate Spread”, “Cheese”, “Butter”를 “Bananas”와 “Apples”로 바꾼 것이다.
shoppingList[4...6] = ["Bananas", "Apples"]
// shoppingList now contains 6 items
특정 인덱스에 요소를 삽입하려면 insert(_:at:)
매서드를 사용한다.
shoppingList.insert("Maple Syrup", at: 0)
// shoppingList now contains 7 items
// "Maple Syrup" is now the first item in the list
또 특정 인덱스의 아이템을 지우려면 remove(at:)
을 사용하면 된다. 이 메서드는 특정 위치에 있는 아이템을 지우고 지워진 아이템을 반환한다.
let mapleSyrup = shoppingList.remove(at: 0)
// the item that was at index 0 has just been removed
// shoppingList now contains 6 items, and no Maple Syrup
// the mapleSyrup constant is now equal to the removed "Maple Syrup" string
만약 배열의 현재 경계 밖에 있는 인덱스에 접근하면 런타임 에러가 발생한다. 인덱스가 유효한지는 배열의
count
프로퍼티와 비교해서 확인할 수 있다. 배열의 유효한 최대 인덱스는count - 1
이다.count
가 0일 때는 유효한 인덱스가 없다.
배열에서 원소가 지워졌을 때 그 지워진 부분(갭)은 없어지기 때문에 위에서 0번째 원소를 지운 후에 0번째 원소는 “Six eggs”가 된다.
firstItem = shoppingList[0]
// firstItem is now equal to "Six eggs"
removeFirst()
를 써서 첫 번째 아이템을 지울 수 있고, removeLast()
메서드를 써서 마지막 아이템을 지울 수 있다.
Iterating Over an Array
배열의 전체 값들을 for-in
문을 통해 반복할 수 있다.
for item in shoppingList {
print(item)
}
// Six eggs
// Milk
// Flour
// Baking Powder
// Bananas
만약 값 뿐만 아니라 정수 인덱스도 원한다면 enumerated()
메서드를 사용할 수 있다. 배열의 각 요소마다 enumerated()
메서드는 정수와 아이템으로 구성된 튜플을 반환한다. 튜플을 반복문 내에서 임시로 상수, 변수로 분해해서 사용할 수 있다.
for (index, value) in shoppingList.enumerated() {
print("Item \(index + 1): \(value)")
}
// Item 1: Six eggs
// Item 2: Milk
// Item 3: Flour
// Item 4: Baking Powder
// Item 5: Bananas
Sets
Set은 같은 타입의 유일한 데이터를 순서 없이 하나의 묶음으로 저장하는 형태의 컬렉션이다. 즉 집합 내의 값들을 모두 유일하기 때문에 중복된 값이 존재하지 않는다. 그래서 집합은 순서가 중요하지 않거나 각 요소가 유일한 값이어야 하는 경우에 사용한다.
Swift의
Set
타입은 Foundation의NSSet
클래스에 연결되어 있다.
Hash Values for Set Types
집합에는 무조건 hashable한 타입의 값이 들어와야 한다. 즉 타입은 자체로 hash value를 연산하는 방법을 제공해야 한다. 해시 값은 Int
값으로 객체가 같다면 같은 해시 값을 가지게 된다. 즉 a == b
라면 a
의 해시 값은 b
의 해시 값과 같다.
모든 Swift의 기본 타입(String
, Int
, Double
, Bool
)은 기본으로 hashable하고, 값 타입이나 딕셔너리의 키 타입으로 설정할 수 있다. 연관 값이 없는 열거형의 케이스 값들도 기본적으로 hashable하다. 열거형에 대해서는 이후에 더 자세히 볼 것이다.
값 타입이나 딕셔너리 키 타입을 커스텀 타입으로 설정할 수 있는데, 이 타입을 Swift 표준 라이브러리의
Hashable
프로토콜을 따르게 하면 된다.hash(into:)
메서드를 구현하는 자세한 방법은 Hashable을 참고해라. 프로토콜에 대해서도 나중에 자세히 볼 것이다.
Set Type Syntax
Swift의 집합의 타입은 Set<Element>
로 쓰고, Element
는 저장할 수 있는 요소의 타입이다. 배열과는 다르게, 집합은 축약해서 쓸 수 있는 형태가 없다.
Creating and Initializing and Empty Set
이니셜라이저 문법을 사용해서 빈 집합을 생성할 수 있다.
var letters = Set<Character>()
print("letters is of type Set<Character> with \(letters.count) items.")
// Prints "letters is of type Set<Character> with 0 items."
대신 문맥상으로 이미 타입 정보가 주어졌다면 빈 배열 리터럴로 빈 집합을 생성할 수 있다.
letters.insert("a")
// letters now contains 1 value of type Character
letters = []
// letters is now an empty set, but is still of type Set<Character>
Creating a Set with an Array Literal
배열 리터럴로 집합을 초기화 할 수 있다.
var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"]
// favoriteGenres has been initialized with three initial items
집합 타입은 배열 리터럴 하나만 있는 걸로는 추론될 수 없다. 따라서 Set
타입은 명시적으로 선언되어야 한다. 하지만 Swift의 타입 추론으로 한 타입만을 포함하는 배열 리터럴로 초기화 할 때는 집합 요소의 타입을 쓰지 않아도 된다. 즉 아래와 같이 다시 쓸 수 있다.
var favoriteGenres: Set = ["Rock", "Classical", "Hip hop"]
Accessing and Modifying a Set
- 메서드
- 프로퍼티
를 사용해서 집합에 접근하고 수정할 수 있다.
집합에 있는 요소 개수를 확인하려면 읽기 전용 count
프로퍼티를 사용한다.
print("I have \(favoriteGenres.count) favorite music genres.")
// Prints "I have 3 favorite music genres."
불리언 타입 isEmpty
프로퍼티를 사용해서 count
프로퍼티가 0인지 확인할 수 있다.
if favoriteGenres.isEmpty {
print("As far as music goes, I'm not picky.")
} else {
print("I have particular music preferences.")
}
// Prints "I have particular music preferences."
insert(_:)
메서드를 호출해서 새 아이템을 추가할 수 있다.
favoriteGenres.insert("Jazz")
// favoriteGenres now contains 4 items
remove(_:)
메서드를 호출해서 집합의 요소를 지울 수 있다. 이 메서드는 요소를 지우고 지워진 요소를 반환하는데, 만약 집합이 해당 요소를 가지고 있지 않았다면 nil
을 리턴한다. 만약 모든 요소들을 지우려면 removeAll()
를 사용할 수 있다.
if let removedGenre = favoriteGenres.remove("Rock") {
print("\(removedGenre)? I'm over it.")
} else {
print("I never much cared for that.")
}
// Prints "Rock? I'm over it."
특정 아이템이 있는지를 확인하려면 contains(_:)
메서드를 사용한다.
if favoriteGenres.contains("Funk") {
print("I get up on the good foot.")
} else {
print("It's too funky in here.")
}
// Prints "It's too funky in here."
Iterating Over a Set
for-in
반복문을 사용해서 값들을 반복할 수 있다.
for genre in favoriteGenres {
print("\(genre)")
}
// Classical
// Jazz
// Hip hop
Swift의 Set
타입은 순서가 정해져 있지 않다. 집합 내의 값들을 특정 순서대로 반복하려면 sorted()
메서드를 사용할 수 있는데, 이 메서드는 집합의 요소들을 <
연산자로 정렬한 배열을 반환한다.
for genre in favoriteGenres.sorted() {
print("\(genre)")
}
// Classical
// Hip hop
// Jazz
Performing Set Operations
기본적인 집합 연산들을 수행할 수도 있다. 합집합, 교집합을 구할 수도 있고 두 집합이 특정 값을 포함했는지, 혹은 한 집합 만이 갖고 있는지, 아니면 아무 집합도 가지고 있지 않은지를 판단할 수 있다.
Fundamental Set Operations
아래의 그림은 두 집합 a, b를 다양하게 연산한 것을 진한 색으로 표시한 것이다.
intersection(_:)
: 교집합을 새로 생성한다.symmetricDifference(_:)
: 합집합에서 교집합을 뺀 나머지로 구성된 새로운 집합을 생성한다.union(_:)
: 합집합을 새로 생성한다.subtracting(_:)
: 차집합을 생성한다.
let oddDigits: Set = [1, 3, 5, 7, 9]
let evenDigits: Set = [0, 2, 4, 6, 8]
let singleDigitPrimeNumbers: Set = [2, 3, 5, 7]
oddDigits.union(evenDigits).sorted()
// [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
oddDigits.intersection(evenDigits).sorted()
// []
oddDigits.subtracting(singleDigitPrimeNumbers).sorted()
// [1, 9]
oddDigits.symmetricDifference(singleDigitPrimeNumbers).sorted()
// [1, 2, 9]
Set Membership and Equaility
아래의 그림은 세 집합 a, b, c를 나타낸 것이다. 집합 a는 집합 b의 superSet인데, 이는 b의 모든 원소가 집합 a에 포함되어 있다는 뜻이다. 반대로, 집합 b는 집합 a의 subset이다. 집합 b, c는 disjoint 한데, 겹치는 요소가 없기 때문에 배타적이라고 할 수 있는 것이다.
==
연산자를 써서 두 집합이 같은 요소들을 가지고 있는지 확인한다.isSubset(of:)
: 특정 집합의 부분집합인지 확인한다.isSuperset(of:)
: 특정 집합의 전체집합인지 확인한다.isStrictSubset(of:)
/isStrictSuperset(of:)
: 특정 집합의 부분집합, 혹은 전체집합인지를 확인하는데 완전히 같지 않은지도 확인한다.isDisjoint(with:)
: 두 집합이 배타적인지 확인한다.
let houseAnimals: Set = ["🐶", "🐱"]
let farmAnimals: Set = ["🐮", "🐔", "🐑", "🐶", "🐱"]
let cityAnimals: Set = ["🐦", "🐭"]
houseAnimals.isSubset(of: farmAnimals)
// true
farmAnimals.isSuperset(of: houseAnimals)
// true
farmAnimals.isDisjoint(with: cityAnimals)
// true
Dictionaries
Dictionary는 요소들이 순서 없이 키와 값의 쌍으로 구성되는 컬렉션 타입이다. 키는 유일해야 하고, 딕셔너리의 값에 대한 식별자의 역할을 한다. 즉 한 딕셔너리 내의 키는 같은 이름을 중복해서 쓸 수 없다. 배열의 요소들과는 다르게, 딕셔너 내의 아이템은 특별한 순서가 없다.
Dictionay Type Shorthand Syntax
Swift에서 딕셔너리의 타입은 Dictionary<Key, Value>
와 같이 쓰고, Key
는 딕셔너리의 키 타입, Value
는 그 키가 저장하고 있는 값들의 타입이다.
Dictionary
Key
는 집합의 값 타입과 마찬가지로Hashable
프로토콜을 만족해야 한다.
또한 [Key:Value]
와 같이 쓸 수 있다. 보통 후자의 방법이 선호된다.
Creating an Empty Dictionary
배열과 마찬가지로 아래와 같이 특정 이니셜라이저 문법으로 빈 딕셔너리를 생성할 수 있다.
var namesOfIntegers: [Int: String] = [:]
// namesOfIntegers is an empty [Int: String] dictionary
이 예제에서 키는 Int
타입이고, 값은 String
타입니다.
만약 문맥상 이미 타입 정보가 주어졌다면 아래와 같이 빈 딕셔너리 리터럴을 사용해서 빈 딕셔너리를 생성할 수도 있다.
namesOfIntegers[16] = "sixteen"
// namesOfIntegers now contains 1 key-value pair
namesOfIntegers = [:]
// namesOfIntegers is once again an empty dictionary of type [Int: String]
Creating a Dictionary with a Dictionay Literal
배열과 비슷하게 생긴 딕셔너리 리터럴을 사용해서 딕셔너리를 초기화할 수도 있다. 딕셔너리 리터럴은 하나 이상의 키-값 쌍을 딕셔너리 컬렉션으로 작성할 수 있는 간단한 방법이다.
var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
배열과 같이 만약 딕셔너리 리터럴에서 키와 값들이 일정한 타입을 가지고 있다면 타입을 명시하지 않을 수 있다.
var airports = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
Accessing and Modifying a Dictionary
- 메서드
- 프로퍼티
- subscript 문법
을 사용해서 딕혀너리에 접근하고 수정할 수 있다.
배열과 같이, Dictionary
의 읽기 정용 프로퍼티인 count
를 통해 딕셔너리의 개수를 확인할 수 있다.
print("The airports dictionary contains \(airports.count) items.")
// Prints "The airports dictionary contains 2 items."
또한 isEmpty
프로퍼티를 사용해서 count
가 0인지 확인할 수 있다.
if airports.isEmpty {
print("The airports dictionary is empty.")
} else {
print("The airports dictionary isn't empty.")
}
// Prints "The airports dictionary isn't empty."
Subscript 문법을 사용해서 딕셔너리에 새로운 아이템을 추가하고, 특정 키와 연관된 값을 수정할 수도 있다.
airports["LHR"] = "London"
// the airports dictionary now contains 3 items
airports["LHR"] = "London Heathrow"
// the value for "LHR" has been changed to "London Heathrow"
Subscript하는 대신에 updateValue(_:forKey:)
메서드를 사용해서 특정 키의 값을 설정하거나 업데이트 할 수 있다. 이 메서드는 만약 해당 키에 대한 값이 설정되어 있지 않은 경우 값을 할당하고, 이미 값이 할당되어 있는 키에 대해서는 값을 업데이트한다. 하지만 subscript와는 다르게 updateValue(_:forKey)
메서드는 업데이트를 한 다음에 이전에 있었던 값을 리턴한다. 이를 통해 업데이트가 이루어졌는지 확인할 수 있다.
이 메서드는 딕셔너리 값 타입의 옵ㅂ셔널 값을 리턴한다. 만약 이전에 값이 할당되어 있으면 해당 값을 리턴하고, 값이 없었던 경우에는 nil
을 리턴한다.
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") {
print("The old value for DUB was \(oldValue).")
}
// Prints "The old value for DUB was Dublin."
또한 subscript 문법을 사용해서 특정 키에 대한 값을 받을 수 있다. 값이 없는 키에 대한 요청을 할 수도 있기 때문에, 딕셔너리의 subscript 는 딕셔너리 값 타입의 옵셔널 값을 리턴한다. 만약 키에 대한 값이 존재한다면 해당 값에 대한 옵셔널 값을 리턴하고, 그렇지 않은 경우 nil
을 리턴한다.
if let airportName = airports["DUB"] {
print("The name of the airport is \(airportName).")
} else {
print("That airport isn't in the airports dictionary.")
}
// Prints "The name of the airport is Dublin Airport."
또한 특정 키에 대한 값을 nil
로 설정해서 키-값 쌍을 없앨 수도 있다.
airports["APL"] = "Apple International"
// "Apple International" isn't the real airport for APL, so delete it
airports["APL"] = nil
// APL has now been removed from the dictionary
대신 removeValue(forKey:)
메서드를 사용해서 키-값 쌍을 없앨 수도 있다. 이 메서드는 키-값 쌍이 존재했다면 이를 지우고 지운 값을 리턴한다. 만약 키-값 쌍이 없었다면 nil
을 리턴한다.
if let removedValue = airports.removeValue(forKey: "DUB") {
print("The removed airport's name is \(removedValue).")
} else {
print("The airports dictionary doesn't contain a value for DUB.")
}
// Prints "The removed airport's name is Dublin Airport."
Iterating Over a Dictionary
for-in
문을 통해 딕셔너리의 키-값 쌍을 반복할 수 있다. 딕셔너리의 각 아이템은 (key, value)
튜플로 리턴되고, 이를 분해해서 상수나 변수에 할당할 수도 있다.
for (airportCode, airportName) in airports {
print("\(airportCode): \(airportName)")
}
// LHR: London Heathrow
// YYZ: Toronto Pearson
또 keys
, values
프로퍼티에 접근해서 딕셔너리의 키나 값들의 iterablegks zjffprtusdmf qkedmf tneh dlTek.
for airportCode in airports.keys {
print("Airport code: \(airportCode)")
}
// Airport code: LHR
// Airport code: YYZ
for airportName in airports.values {
print("Airport name: \(airportName)")
}
// Airport name: London Heathrow
// Airport name: Toronto Pearson
Swift의 딕셔너리 타입은 정해진 순서가 없다. 만약 키와 값을 특정 순서로 반복하고 싶다면, sorted()
메서드를 keys
나 values
프로퍼티를 사용한다.
컬렉션에서 임의의 요소 추출과 뒤섞기
randomElement()
: 컬렉션에서 임의의 요소 추출shuffle()
: 컬렉션의 요소를 임의로 뒤섞는 메서드shuffled()
: 자신의 요소는 그대로 둔 채 새로운 컬렉션에 임의의 순서로 섞어서 반환하는 메서드
Enumerations
열거형은 연관된 값의 묶어서 표현할 수 있게 해준다. 또 이런 값들을 type-safe하게 사용할 수 있게 해준다. 열거형은 앞에서 배열이나 딕셔너리가 이미 정의된 값에 새로운 요소를 추가하고, 수정하고 삭제할 수 있었던 것과는 달리 프로그래머가 정의한 항목 값 외에는 추가/수정을 할 수 없다.
열거형은 아래의 경우에서 유용하다.
- 제한된 선택지를 주고 싶을 때
- 정해지 값 외에는 입력받고 싶지 않을 때
- 예상된 입력 값이 한정되어 있을 때
즉 “선택지의 제한”과 관련된 상황으로, 케이스가 정해져 있는 경우에 유용하다고 생각하면 될 것 같다.
열거형으로 묶을 수 있는 예시들은 실생활에서도 많이 찾아볼 수 있다.
- 학생 - 초, 중, 고, 대, 대학원 …
- 지역 - 서울, 경기, 강원, 충북, …
이렇게 열거형을 사용해서 연관된 항목들의 그룹을 정의할 수 있다. Swift의 열거형은 항목별로 값을 가지거나 가지지 않을 수 있다. C에서의 열거형은 각 항목 값이 정수 타입으로 기본 지정되는데, Swift의 열거형은 각 항목이 그 자체로 고유한 값이 될 수 있다. 즉 각 열거형이 고유의 타입으로 인정되기 때문에 실수로 버그가 일어날 가능성을 없앨 수 있다.
각 항목이 정수, 실수, 문자 타입 등의 원시 값(raw value)을 가질 수 있다. 또한 연관 값(associated value) 를 사용해서 다른 언어에서 공용체라고 불리는 값들의 묶음도 구현할 수 있다.
열거형은 switch
문과 같이 사용했을 때 빛을 발한다.
Enumeration and Optional Swift의 주요 기능 중 하나인 옵셔널은 enum(열거형)으로 구현되어 있다.
Enumeration Syntax
enum
키워드를 쓰고, 괄호 내에 정의를 써서 열거형을 선언한다.
enum SomeEnumeration {
// enumeration definition goes here
}
enum CompassPoint {
case north
case south
case east
case west
}
열거형 내에 정의된 값들(north, south, east, west)는 enumeration case라고 한다. case
키워드를 써서 새로운 열거형 케이스를 생성할 수 있다.
Swift 열거형 case는 C나 Objective-C와는 다르게 기본적으로 정수 값을 가지지 않는다. 즉 위의 예제로 설명하면
north
,south
,east
,west
는 각각 0, 1, 2, 3과 같지 않다. 각 항목 자체가 고유의 값이다.
여러 케이스들은 컴마로 구분해서 한 줄에 나타낼 수도 있다.
enum Planet {
case mercury, venus, earth, mars, jupiter, saturn, uranus, neptune
}
이 열거형 정의는 새로운 타입에 대한 정의이기 때문에 이름은 대문자로 시작하고, 이름은 복수보다는 단수를 사용해서 독립적으로 보이게끔 해준다.
var directionToHead = CompassPoint.west
// you can drop the type in case when type is already known
directionToHead = .east
Matching Enumeration Values with a Switch Statement
switch
문으로 각각의 열거형 값을 매칭할 수 있다.
directionToHead = .south
switch directionToHead {
case .north:
print("Lots of planets have a north")
case .south:
print("Watch out for penguins")
case .east:
print("Where the sun rises")
case .west:
print("Where the skies are blue")
}
// Prints "Watch out for penguins"
이는 directionToHead
의 값이 .north
인 경우에는 “Lots of planets have a north”를 출력해라.. 와 같이 읽힌다.
switch
문 내에서는 모든 열거형 케이스를 포함해야 한다. 만약 위 예시에서 .west
가 빠진다면 컴파일이 되지 않는데, 그 이유는 CompasssPoint
의 전체 케이스를 고려하지 못했기 때문이다. 이를 통해 특정 열거형 케이스가 빠지는 실수롤 하지 않게 된다.
만약 모든 열거형 케이스를 switch
문 내에 쓰는 게 적절하지 않은 경우에는 default
를 통해 명시되지 않은 경우의 케이스를 커버할 수 있다.
let somePlanet = Planet.earth
switch somePlanet {
case .earth:
print("Mostly harmless")
default:
print("Not a safe place for humans")
}
// Prints "Mostly harmless"
Associated Values
위 예시들에서 열거형 케이스들은 자기 스스로가 고유한 값으로 정의되었다. 하지만, 이런 케이스 값들과 함께 다른 타입의 값들을 저장하는게 유용할 때가 있다. 이런 추가적인 정보를 associated vlaue라 한다.
이 연관 값은 케이스마다 다른 타입을 가질 수 있다. 예를 들어 두 종류의 바코드에 대한 정보를 나타내는 바코드 열거형이 있다고 해보자.
첫 번째 경우 4개의 숫자 정보를 저장하고 있어야 하고, 두 번째 이차원 QR 코드의 경우 문자열로 저장하는 것이 편리하다. Swift에서는 아래와 같이 이런 종류의 바코드들을 열거형으로 정의할 수 있다.
enum Barcode {
case upc(Int, Int, Int, Int)
case qrCode(String)
}
그리고 아래와 같이 바코드를 생성하고, 할당할 수 있다.
var productBarcode = Barcode.upc(8, 85909, 51226, 3)
productBarcode = .qrCode("ABCDEFGHIJKLMNOP")
또 switch
문을 사용해서 연관 값을 추출할 수도 있다. 각 연관 값을 상수나 변수로 추출할 수 있다.
switch productBarcode {
case .upc(let numberSystem, let manufacturer, let product, let check):
print("UPC: \(numberSystem), \(manufacturer), \(product), \(check).")
case .qrCode(let productCode):
print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
만약 열거형의 케이스의 모든 연관 값이 상수로 추출되면 하나의 let
키워드를 사용하고, 변수로 추출될 경우 하나의 var
키워드를 케이스 이름 전에 붙여서 간단하게 표현할 수도 있다.
switch productBarcode {
case let .upc(numberSystem, manufacturer, product, check):
print("UPC : \(numberSystem), \(manufacturer), \(product), \(check).")
case let .qrCode(productCode):
print("QR code: \(productCode).")
}
// Prints "QR code: ABCDEFGHIJKLMNOP."
또 열거형 내에서 한 케이스가 연관 값을 가지고 있다고 해서 모든 케이스가 열거형을 가질 필요는 없다.
enum MainDish {
case pasta(taste: String)
case pizza(dough: String, topping: String)
case rice
}
var dinner = MainDish.pasta(taste: "cream")
dinner = .rice
연관 값으로 또 다른 열거형을 갖게 해서 연관 값으로 가질 수 있는 옵션을 한정할 수도 있다.
enum MainDish {
case pasta(taste: PastaTaste)
case pizza(dough: PizzaDough, topping: PizzaTopping)
case rice
}
enum PastaTaste {
case cream, tomato
}
enum PizzaDough {
case cheeseCrust, thin, original
}
enum PizzaTopping {
case cheese, bacon
}
var dinner = MainDish.pasta(taste: .tomato)
dinner = .rice
Raw Values
열거형의 각 항목은 자체로도 하나의 값이지만 항목의 원시 값(raw value) 도 가질 수 있다. 특정 타입의 값을 가질 수 있다는 뜻으로, 원시값을 가지고 싶으면 열거형 이름 오른쪽에 타입을 명시해준다. 연관 값은 케이스마다 다른 타입을 값으로 가질 수 있었지만 원시 값의 경우 모두 같은 타입의 값을 가져야 한다.
enum ASCIIControlCharacter: Character {
case tab = "\t"
case lineFeed = "\n"
case carriageReturn = "\r"
}
원시 값은 연관 값이랑 같지 않다. 원시 값은 코드에서 열거형을 처음 정의할 때 미리 생성된 값들로 설정되어 있다. 따라서 특정 열거형 케이스의 원시 값은 항상 같다. 반면 연관 값은 내가 생성할 때 같이 새로 생성해주기 때문에 열거형을 생성할 때마다 다른 값들을 가질 수 있다.
Implicitly Assigned Raw Values
정수나 문자열 타입의 원시 값을 갖는 열거형은 각 케이스마다 명시적으로 원시 값을 할당할 필요가 없다. 내가 명시해주지 않으면 Swift가 자동으로 값을 할당해준다. 문자열 타입의 원시 값을 지정했다면 각 항목 이름이 그대로 원시 값이 되고, 정수 타입의 원시 값을 지정했다면 첫 항목이 0이 되고, 다음 케이스부터 1씩 증가한 값을 원시 값은로 갖게 된다.
enum Planet: Int {
case mercury = 1, venus, earth, mars, jupiter, saturn, uranus, neptune
}
// Panet.mercury이 명시적인 원시 값 1을 가지고 있기 때문에, 다음 케이스인 venus는 원시 값 2를 갖는다.
enum CompassPoint: String {
case north, south, east, west
}
// CompassPoint.south는 "south"라는 원시 값을 갖는다.
enum Numbers: Int {
case zero // 0
case one // 1
case two // 2
case ten = 10 // 10
}
열거형 케이스의 원시 값은 rawValue
프로퍼티로 접근할 수 있다.
let earthsOrder = Planet.earth.rawValue
// earthsOrder is 3
let sunsetDirection = CompassPoint.west.rawValue
// sunsetDirection is "west"
Initialzing from a Raw Value
열거형이 원시 값을 갖는다고 정의했다면 자동으로 원시 값을 rawValue
프로퍼티로 받아 열거형 케이스나 nil
을 리턴해주는 생성자를 사용할 수 있다.
let possiblePlanet = Planet(rawValue: 7)
// possiblePlanet is of type Planet? and equals Planet.uranus
위처럼 원시 값이 맞는 케이스가 있을 수도 있지만 없을 수도 있다. 따라서 이 원시 값 생성자는 항상 옵셔널 열거형 케이스를 리턴한다. 그래서 위 예시에서 possiblePlanet
의 타입이 옵셔널인 것이다.
원시 값 생성자는 failable initializer다.
이 생성자를 사용해서 특정 원시 값과 일치하는 케이스가 있는지 찾을 수도 있다.
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"
Recursive Enumeration
Recursive Enumeration은 하나 이상의 케이스에서 연관 값으로 자신의 열거형 인스턴스를 갖는 열거형을 의미한다. 즉 연관 값으로 자신을 갖는 열거형이라고 생각하면 된다. 순환 열거형은 indirect
키워드를 사용해서 명시한다. 열거형의 모든 케이스가 연관 값으로 자신을 가질 때는 열거형 이름 앞에 키워드를 붙이고, 특정 케이스에만 한정하려면 케이스 앞에 키워드를 붙이면 된다.
enum ArithmeticExpression {
case number(Int)
indirect case addition(ArithmeticExpression, ArithmeticExpression)
indirect case multiplication(ArithmeticExpression, ArithmeticExpression)
}
indirect enum ArithmeticExpression {
case number(Int)
case addition(ArithmeticExpression, ArithmeticExpression)
case multiplication(ArithmeticExpression, ArithmeticExpression)
}
예시는 아래와 같다.
let five = ArithmeticExpression.number(5) // 5
let four = ArithmeticExpression.number(4) // 4
let sum = ArithmeticExpression.addition(five, four) // 5+4
let product = ArithmeticExpression.multiplication(sum, ArithmeticExpression.number(2)) // (5+4) x 2
재귀 함수는 이런 재귀적인 구조를 갖는 데이터를 다루는 직관적인 방법 중 하나다. 위에서 생성한 열거형을 아래의 함수로 값을 계산할 수 있다.
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"
Iterating over Enumeration Cases
어떤 열거형에서는 해당 열거형의 모든 케이스의 컬렉션을 가지고 있는게 편리할 때가 있다. 이때는 열거형 이름 뒤에 : CaseIterable
를 작성해주면 된다. 이러면 열거형 타입의 allCases
프로퍼티에 접근해서 컬렉션에 접근할 수 있다.
enum Beverage: CaseIterable {
case coffee, tea, juice
}
let numberOfChoices = Beverage.allCases.count
print("\(numberOfChoices) beverages available")
// Prints "3 beverages available"
이 컬렉션은 다른 컬렉션돠 똑같이 사용할 수 있다. 컬렉션의 요소는 열거형 타입의 인스턴스들이 된다. 또 for-in
문을 통해 모든 케이스를 반복할 수 있다.
for beverage in Beverage.allCases {
print(beverage)
}
// coffee
// tea
// juice
위처럼 단순한 열거형은 CaseIterable
프로토콜을 채택만 해줘서 allCases
프로퍼티를 사용할 수 있지만, 복잡한 열거형의 경우 allCases
프로퍼티를 직접 구현해줘야 한다.
enum School: String, CaseIterable {
case high = "고등학교", college = "대학"
// 케이스를 사용할 수 없는 경우가 생긴다.
@available(iOS, obsoleted: 12.0)
case graduate = "대학원"
static var allCases: [School] {
let all: [School] = [.high, .college]
// 플랫폼에 따라 다르게 allCases return
#if os(iOS)
return all
#else
return all + [.graduate]
#endif
}
}
위처럼 allCases
프로퍼티를 바로 사용할 수 없는 경우는 열거형의 케이스가 연관 값을 갖는 경우다.
연관 값을 갖는 열거형에 allCases
프로퍼티를 따로 구현해주지 않으면 위처럼 컴파일 에러가 난다.
enum MainDish: CaseIterable {
case pasta(taste: PastaTaste)
case pizza(dough: PizzaDough, topping: PizzaTopping)
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)
})
})
}
}
enum PastaTaste: CaseIterable {
case cream, tomato
}
enum PizzaDough: CaseIterable {
case cheeseCrust, thin, original
}
enum PizzaTopping: CaseIterable {
case cheese, bacon
}
print(MainDish.allCases)
결과는 아래와 같이 나온다.
pasta의 크림, 토마토가 있고, cheeseCrust-cheese topping pizza, cheeseCrust-bacon topping pizza, think-cheese topping pizza… 이렇게 연관 값이 여러 개일 경우 조합을 만들어서 리턴해줬다. 여기에 나오는 문법은 복잡하므로 뒤에서 더 자세히 공부한 다음 참고하면 된다.
Comparable Enumerations
연관 값만 갖거나, 연관 값이 없는 열거형은 Comparable 프로토콜을 채택할 경우 각 케이스를 비교할 수 있다. 앞에 위치한 케이스가 더 작은 값이 된다.
enum Condition: Comparable {
case terrible, bad, good, great
}
let myCondition = Condition.great
let yourCondition = Condition.terrible
if myCondition > yourCondition {
print("제 컨디션이 더 좋습니다.")
} else {
print("당신의 컨디션이 더 좋습니다.")
}
//제 컨디션이 더 좋습니다.
enum Device: Comparable {
case iPhone(version: String), macBook
}
let myDevice = Device.macBook
let yourDevice = Device.iPhone(version: "13.0")
if myDevice > yourDevice {
print("제 기기가 더 크네요")
}
//제 기기가 더 크네요
sdfsdf