애플사이다의 iOS 개발 일지

[Swift Language Guide 정독 시리즈] 4. Collection Types 본문

Swift/영문 공식문서 뜯어보기-Swift

[Swift Language Guide 정독 시리즈] 4. Collection Types

Applecider 2021. 9. 26. 06:13

안녕하세요. 애플사이다 입니다.

[Swift Language Guide 정독 시리즈]의 네 번째 챕터 Collection Types에 대해 정리해보겠습니다.

*Swift Language Guide를 읽어야 하는 이유는 시리즈 0. Language Guide란? 포스팅을 참고해주세요.

 


Collection Types (컬렉션 타입)

Swift에는 세 가지 기본 컬렉션 타입이 있다. 1) 배열 (arrays), 2) sets (집합), 3) dictionaries (딕셔너리)

여러 개의 값을 묶어서 (collections of values) 1개의 상수/변수에 저장할 때 사용한다.

  • Array는 순서가 있는 값의 묶음이다.
  • Set는 순서가 없는 값의 묶음이며, 값은 중복되지 않는다. (unique)
  • Dictionary는 순서가 없는 데이터의 묶음이며, 데이터는 키-값 (key-value) 형태로 들어있다.

배열, 집합, 딕셔너리의 사용 예시

Swift의 컬렉션 타입은 저장하는 값/키의 타입이 항상 명확하므로 실수로 다른 타입의 값을 컬렉션에 삽입할 수 없다.

또한 특정 컬렉션으로부터 값을 얻은 경우, 해당 값의 타입을 확실히 알 수 있다.

 

Note: Swift의 Array, Set, Dictionary 타입은 제네릭 컬렉션 (generic collections)으로 구현된다.

*제네릭에 대한 자세한 내용은 Generics 챕터에서 다루겠습니다. 

Mutability of Collections (컬렉션의 수정 가능 여부)

Array, Set, Dictionary를 생성하여 변수에 할당하는 경우, 해당 컬렉션은 수정 가능 (mutable)하다.

컬렉션을 생성한 이후에 컬렉션 내부의 item을 추가 / 삭제 / 수정할 수 있다는 뜻이다.

 

Array, Set, Dictionary를 생성하여 상수에 할당하는 경우, 해당 컬렉션은 수정 불가 (immutable)하다.

컬렉션의 크기 및 내용 (contents)을 수정할 수 없다.

Note: It’s good practice to create immutable collections in all cases where the collection doesn’t need to change. Doing so makes it easier for you to reason about your code and enables the Swift compiler to optimize the performance of the collections you create.

 

❗️컬렉션을 생성한 이후 수정할 필요가 없다면, 상수에 할당하는 것이 바람직하다. "앞으로 이 컬렉션은 수정하면 안 된다"라는 의도를 코드에 명시하므로 다른 개발자가 코드를 이해하기 쉽다. 또한 컴파일러의 성능 최적화 (performance optimization)를 가능하게 한다.

Arrays (배열)

Array는 동일한 타입의 값들을 순서가 있는 리스트 (ordered list) 형태로 저장한다. 동일한 값을 담을 수 있다.

 

Note: Swift의 Array 타입은 Foundation 프레임워크의 NSArray 클래스와 연결 (bridged) 되어 있다.

*Foundation 및 Cocoa에서 Array를 사용하는 방법은 Bridging Between Array and NSArray 에서 확인할 수 있다.

Array Type Shorthand Syntax (배열 타입의 축약 문법)

타입을 지정할 때, Array 타입을 표현하는 두 가지 방법이 있다. 기능적으로 동일한 방법이다.

1) Array<Element> 로 나타낸다. (Element는 값의 타입을 의미한다.)

2) [Element] 로 나타낸다. 축약 표현을 사용하는 것이 바람직하다.

Creating an Empty Array (빈 배열 생성)

이니셜라이저 (initializer) 문법을 사용하여 특정 타입의 빈 Array를 생성할 수 있다.

상수/변수/함수의 전달인자 등과 같이 문맥상 이미 타입이 지정된 경우, 빈 배열 리터럴 (empty array literal, [])을 사용하여 빈 Array를 생성할 수 있다.

var someInts1 = [Int]()
print("someInts1 is of type [Int] with \(someInts1.count) items.")
// Prints "someInts1 is of type [Int] with 0 items."

var someInts2: [Int] = []
print("someInts2 is of type [Int] with \(someInts2.count) items.")
// Prints "someInts2 is of type [Int] with 0 items."

someInts2.append(3) // someInts2 now contains 1 value of type Int
someInts2 = []      // someInts2 is now an empty array, but is still of type [Int] - 빈 Array 할당
  • 변수 someInts1의 타입은 이니셜라이저의 타입에 따라 [Int]로 추론되었다. (Type Inference)
  • 변수 someInts2의 타입을 [Int]로 지정했다. (Type Annotation)
  • 이미 타입을 알고 있으므로 []을 할당하여 변수 someInts2를 빈 Array로 만들었다.

Creating an Array with a Default Value (기본값을 가지는 배열 생성)

Swift의 Array 타입은 기본값을 가지는 Array를 생성하는 이니셜라이저를 사용할 수 있다. 

이때 이니셜라이저는 특정 크기의 Array를 생성하며, 모든 값이 동일하다.

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 (2개의 배열을 더하여 새로운 배열 생성)

더하기 연산자 + 를 사용하여 2개의 배열을 더할 수 있다. 이때 두 배열은 연산이 가능한 타입 (compatible types)이어야 한다.

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 (배열 리터럴을 통한 배열 생성)

Array 리터럴을 통해 Array를 초기화할 수 있다.

Array 선언 시, 타입 지정을 하지 않아도 Array 리터럴을 통해 타입 추론이 가능하다. (단, 모든 값의 타입이 동일해야 한다.)

var shoppingList: [String] = ["Eggs", "Milk"] // Array literal
// shoppingList has been initialized with two initial items

var shoppingListWithLiteral = ["Eggs", "Milk"] // 타입 지정 ([String])이 없어도 Array literal을 통해 타입이 추론되었다.
print(type(of: shoppingListWithLiteral)) // Array<String>
  • 변수 shoppingList는 String값의 Array (an array of string values)로 선언되었다. [String]으로 타입 지정을 했기 때문이다.
    또한 배열 리터럴의 타입은 [String]이 맞으므로 배열 리터럴을 통해 Array의 초기값으로 2개의 item이 할당되었다.
  • 변수 shoppingListWithLiteral은 배열 리터럴을 통해 [String] 타입으로 추론되었다.
  • 두 변수 모두 String 타입의 값만 저장할 수 있다.

Note: shoppingList를 상수가 아닌 변수로 선언한 이유는 앞으로 item을 추가/삭제할 의도가 있기 때문이다.

Accessing and Modifying an Array (배열 접근 및 수정)

Array에 접근 및 수정하려면, 1) Array의 메서드/프로퍼티를 사용하거나, 2) 서브스크립트 (subscript) 문법을 사용하는 방법이 있다.

*서브스크립트는 Subscripts 챕터에서 자세히 다루겠습니다.

 

1) Array의 메서드/프로퍼티를 사용

  • 읽기전용 (read-only) 프로퍼티 count를 사용하면, Array의 item 개수를 확인한다.
  • Bool 타입의 isEmpty 프로퍼티를 사용하면, count 프로퍼티가 0인지 쉽게 알 수 있다.
    *성능 측면에서 isEmpty (O(1))가 count (O(n))보다 좋다.
  • append(_:) 메서드를 호출하면, Array의 끝에 item을 추가한다.
  • 더하기 할당 연산자 (+=)를 사용하면, 1개 이상의 item을 Array 끝에 추가한다.
  • insert(_:at:) 메서드를 호출하면, 특정 index에 1개 item을 삽입한다.
  • remove(at:) 메서드를 호출하면, 특정 index의 1개 item을 삭제한다. *이때 삭제한 item을 반환한다. 
    (반환값은 필요할 때만 사용해도 된다.) item이 삭제되면 빈 공간 (gap)은 자동으로 뒤의 값으로 채워진다.
  • removeLast() 메서드를 사용하면, 맨 뒤의 1개 item을 삭제한다. (remove(at:) 보다 편리하다.) 
    *이때 삭제한 item을 반환한다. 

2) 서브스크립트 (subscript) 문법을 사용

  • 서브스크립트 instance[index]에 원하는 item의 위치 (index)를 전달하면, Array의 item을 꺼낸다. (retrieve)
  • Array의 특정 index에 위치한 값을 수정한다.
  • ❗️유효하지 않은 index (Array 범위를 벗어난 index 등)에 접근하면 런타임 오류가 발생한다.
    (즉, 특정 index에 위치한 기존 item을 변경할 수 있지만, 새로운 item을 추가할 수는 없다.)
  • 특정 범위의 1개 이상의 값을 수정한다. 이때 수정 전/후의 item의 개수가 달라도 가능하다.
print("The shopping list contains \(shoppingList.count) items.")
// Prints "The shopping list contains 2 items."

if shoppingList.isEmpty {
    print("The shopping list is empty.")
} else {
    print("The shopping list isn't empty.")
}
// Prints "The shopping list isn't empty."

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

var firstItem = shoppingList[0] // 서브스크립트 문법 사용 - 원하는 item을 꺼낸다.
// firstItem is equal to "Eggs"

shoppingList[0] = "Six eggs" // 서브스크립트 문법 사용 - 기존의 item을 수정한다.
// the first item in the list is now equal to "Six eggs" rather than "Eggs"

shoppingList[shoppingList.count] = "Salt" // *런타임 에러 - 유효하지 않은 index에 접근했다. (서브스크립트 문법으로 새로운 item 추가는 불가하다.)

shoppingList[4...6] = ["Bananas", "Apples"] // 서브스크립트 문법 사용 - 특정 범위의 1개 이상의 값을 수정한다.
// shoppingList now contains 6 items

shoppingList.insert("Maple Syrup", at: 0) // "Maple Syrup" is now the first item in the list
// shoppingList now contains 7 items

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 - 삭제된 item이 상수에 반환되었다.

let apples = shoppingList.removeLast() // the last item in the array has just been removed
// shoppingList now contains 5 items, and no apples
// the apples constant is now equal to the removed "Apples" string - 삭제된 item이 상수에 반환되었다.

 

Note: If you try to access or modify a value for an index that’s outside of an array’s existing bounds, you will trigger a runtime error. You can check that an index is valid before using it by comparing it to the array’s count property. The largest valid index in an array is count - 1 because arrays are indexed from zero—however, when count is 0 (meaning the array is empty), there are no valid indexes.

 

❗️Note: 서브스크립트 문법을 사용할 때, Array 범위를 벗어난 index에 접근하면, 런타임 에러가 발생한다. index가 유효한지 count 프로퍼티를 통해 미리 확인하는 것이 바람직하다.

단, Array의 index는 0부터 시작하므로 (Arrays are zero-indexed) 유효한 index의 최대값은 "count" 값이 아니라 "count - 1" 값이다.

count 값이 0 이면 (즉, 빈 Array이면) 유효한 index는 없다.

Iterating Over an Array (배열 반복)

for-in문을 사용하면, Array의 모든 value를 반복 (iterate)하여 접근할 수 있다.

*For-in 반복문은 Control Flow 챕터에서 자세히 다루겠습니다.

 

for-in문에서 enumerated() 메서드를 사용하면, Array item에 대해 index 및 value를 꺼낸다.

enumerated() 메서드는 각 item의 (index, value) 형태로 구성된 튜플 (tuple)을 반환한다.
for-in문 내부에서 해당 튜플을 상수/변수로 분해 (decompose)하여 튜플 요소에 접근할 수 있다.

*튜플에 대한 내용은 [Swift Language Guide 정독 시리즈] 1. The Basics의 튜플을 참고해주세요.

for item in shoppingList {
    print(item)
}
// Six eggs
// Milk
// Flour
// Baking Powder
// Bananas

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

Set (집합)

Set에 들어있는 값은 집합 개념처럼 서로 중복되지 않는다. Set는 동일한 타입의 값을 순서 없이 묶은 것 (collection)이다.

Array가 아니라 Set를 사용해야 하는 상황은 1) item의 순서가 중요하지 않거나, 2) item이 중복되면 안 될 경우이다.

 

Note: Swift의 Set 타입은 Foundation 프레임워크의 NSSet 클래스와 연결 (bridged) 되어 있다.

*Foundation 및 Cocoa에서 Set를 사용하는 방법은 Bridging Between Set and NSSet 에서 확인할 수 있다.

Hash Values for Set Types (Set 타입에 대한 해쉬값)

A type must be hashable in order to be stored in a set—that is, the type must provide a way to compute a hash value for itself. A hash value is an Int value that’s the same for all objects that compare equally, such that if a == b, the hash value of a is equal to the hash value of b.

 

Set에 저장하려는 값의 타입은 hashable 해야 한다. 즉, 타입은 자신의 해쉬값 (hash value)을 구할 수 있어야 한다.

동일한 객체 (object)이면, 각 객체의 해쉬값을 동일하다. 또한 해쉬값은 간단한 Int값 형태이다. 

예를 들어 a == b 이면, a의 해쉬값은 b의 해쉬값과 동일하다.

 

*해쉬값이란?

어떤 데이터를 간단한 숫자로 변환한 것이다. 정확히는 원본 데이터 (객체)를 해쉬 함수 (hash function)을 사용하여 64bit의 Int값으로 변환한 것이다. 2개의 데이터를 비교할 때, 데이터가 동일하면 각 데이터의 해쉬값도 동일하다.

*해쉬값에 대한 기초 개념은 Hashable 해야 한다? 해쉬값이란? (간단 요약)을 참고해주세요.

 

✅ Swift의 기본 데이터 타입 (String, Int, Bool 등)은 default로 hashable 하다. 

또한 Set 타입의 값, 그리고 Dictionary 타입의 키 (key)는 hashable 해야 한다.

따라서 Hashable 한 기본 데이터 타입은 모두 Set의 값, 그리고 Dictionary의 키에 들어갈 수 있다.

사용자 정의 타입은 어떨까? 연관값 (associated values)이 없는 열거형의 케이스 값 (Enumeration case values)도 default로 hashable 하다. 

*열거형은 Enumerations 챕터에서 자세히 다루겠습니다.

Note: You can use your own custom types as set value types or dictionary key types by making them conform to the Hashable protocol from the Swift standard library. For information about implementing the required hash(into:) method, see Hashable.

 

hashable 해야 한다는 것은 Swift 표준 라이브러리의 Hashable Protocol을 준수 (conform)한다는 의미이다.

따라서 사용자 정의 타입 (custom types)이 Hashable Protocol을 준수하도록 설정하면, Set의 값, 그리고 Dictionary의 키로 사용할 수 있다. 단, 이 경우 직접 1) hash(into:) 메서드 및 2) == 연산자 함수를 구현 (implement)해야 한다. (== 함수를 구현해야 하는 이유는 Hashable Protocol이 Equatable Protocol을 준수하기 때문이다.) 자세한 내용은 Hashable에서 확인할 수 있다.

*프로토콜은 Protocols 챕터에서 자세히 다루겠습니다.

Set Type Syntax (Set 타입의 문법)

타입을 지정할 때, Set 타입은 Set<Element> 로 나타낸다. (Element는 값의 타입을 의미한다.)

Array와 달리 Set는 축약 문법이 없다. 

Creating and Initializing an Empty Set (빈 Set 생성 및 초기화)

이니셜라이저 (initializer) 문법을 사용하여 특정 타입의 빈 Set를 생성할 수 있다.
상수/변수/함수의 전달인자 등과 같이 문맥상 이미 타입이 지정된 경우, 빈 배열 리터럴 (empty array literal, [])을 사용하여 빈 Set를 생성할 수 있다. (빈 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>
  • 변수 letters의 타입은 이니셜라이저의 타입에 따라 Set<Character>로 추론되었다. (Type Inference)
  • 이미 타입을 알고 있으므로 []을 할당하여 변수 letters을 빈 Set로 만들었다.

Creating a Set with an Array Literal (배열 리터럴을 통한 Set 생성)

배열 리터럴을 통해 Set를 초기화할 수 있다. 1개 이상의 값을 나열하는 것만으로 손쉽게 Set를 생성하는 방법이다.

var favoriteGenres: Set<String> = ["Rock", "Classical", "Hip hop"] 
// favoriteGenres has been initialized with three initial items

var favoriteGenresWithLiteral1: Set = ["Rock", "Classical", "Hip hop"] // Set<String>으로 타입 추론된다.
var favoriteGenresWithLiteral2 = ["Rock", "Classical", "Hip hop"]      // Array<String>으로 타입 추론된다.
// Array와 달리 Set는 리터럴만으로 타입 추론이 불가하다.
  • 변수 favoriteGenres는 String값의 Set (a Set of string values)로 선언되었다. Set<String>으로 타입 지정을 했기 때문이다.
    또한 배열 리터럴의 타입은 String 타입이 맞으므로 배열 리터럴을 통해 Set의 초기값으로 3개의 item이 할당되었다.
  • 변수 favoriteGenresWithLiteral1은 Set로 타입 지정을 했으므로 배열 리터럴을 통해 Set<String> 타입으로 추론되었다.
    Array와 달리 Set는 리터럴만으로 타입 추론이 불가하다. 
  • 변수 favoriteGenresWithLiteral2는 타입 지정을 하지 않았으므로 Set가 아닌 Array로 타입 추론되므로 주의해야 한다.

Note: favoriteGenres를 상수가 아닌 변수로 선언한 이유는 앞으로 item을 추가/삭제할 의도가 있기 때문이다.

Accessing and Modifying a Set (Set 접근 및 수정)

Set에 접근 및 수정하려면, Set의 메서드/프로퍼티를 사용한다. (Set는 순서가 없으므로 서브스크립트 문법을 사용할 수 없다.)

 

*Array와 다른 내용을 파란색으로 Marking 했다.

  • 읽기전용 (read-only) 프로퍼티 count를 사용하면, Set의 item 개수를 확인한다.
  • Bool 타입의 isEmpty 프로퍼티를 사용하면, count 프로퍼티가 0인지 쉽게 알 수 있다.
    *성능 측면에서 isEmpty (O(1))가 count (O(n))보다 좋다.
  • insert(_:) 메서드를 호출하면, 1개 item을 추가한다. *Array와 달리 Set는 순서가 없으므로 at 매개변수가 필요 없다.
  • remove(_:) 메서드를 호출하면, 특정 index의 1개 item을 삭제한다. *이때 삭제한 item을 반환한다. 
    또한 Set에 해당 item이 없으면 nil을 반환한다.
  • removeAll() 메서드를 사용하면, 모든 item을 삭제한다.
    *Array와 달리 Set는 순서가 없으므로 append(_:) 및 removeLast() 메서드가 없다.
  • contains(_:) 메서드를 사용하면, Set가 특정 item을 포함하는지 확인한다.
print("I have \(favoriteGenres.count) favorite music genres.")
// Prints "I have 3 favorite music genres."

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."

favoriteGenres.insert("Jazz")
// favoriteGenres now contains 4 items

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."

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 (Set 반복)

for-in문을 사용하면, Set의 모든 value를 반복 (iterate)하여 접근할 수 있다.

*For-in 반복문은 Control Flow 챕터에서 자세히 다루겠습니다.

 

Set는 값을 순서가 없이 저장하지만, sorted() 메서드를 사용하면 Set의 값을 특정 순서에 따라 정렬할 수 있다.

sorted() 메서드는 Set의 요소 (elements)를 < 연산자로 정렬 (A-Z 또는 작은 수-큰 수 순서로 정렬)한 Array를 반환한다.

for genre in favoriteGenres {
    print("\(genre)")
}
// Classical - Set는 순서가 없으므로 실행할 때마다 출력 순서가 바뀐다.
// Jazz
// Hip hop

for genre in favoriteGenres.sorted() { // < 연산자로 정렬된 배열을 iterate 한다.
    print("\(genre)")
}
// Classical - 항상 A-Z 순서로 정렬된 값이 출력된다.
// Hip hop
// Jazz

Performing Set Operations (집합 연산)

기본적인 집합 연산을 수행할 수 있다. 

예를 들어 1) 2개 Set를 결합 (combining)하거나, 2) 2개 Set가 공통으로 가지는 값을 확인하거나, 3) 2개 Set가 모든 값 (all) 또는 일부 값 (some)을 공통으로 가지는지, 또는 공통되는 값이 전혀 없는지 (none of the same values) 확인할 수 있다.

Fundamental Set Operations (기본적인 집합 연산)

아래 그림은 2개의 Set a, b의 다양한 집합 연산 결과를 나타낸다.

집합 연산 결과를 나타내는 벤 다이어그램 (Venn Diagram)

  • intersection(_:) 메서드는 2개 Set의 공통적인 값만 가지는 새로운 Set를 생성한다. (교집합)
  • symmetricDifference(_:) 메서드는 2개 Set의 공통적인 값을 제외한 모든 값을 가지는 새로운 Set를 생성한다. (여집합)
  • union(_:) 메서드는 2개 Set의 모든 값을 가지는 새로운 Set를 생성한다. (합집합)
  • subtracting(_:) 메서드는 하나의 Set 값 중에서 다른 하나의 Set 값을 제외한 새로운 Set를 생성한다. (차집합)
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 Equality (집합 포함 여부 및 동일 여부)

아래 그림은 3개의 Set a, b, c가 공유하는 요소 (elements shared among sets)를 나타낸다.

a는 b의 모든 요소를 포함한다. 따라서 a는 b의 초집합 (superset) 이다. 반대로 b는 a의 부분집합(subset) 이다. 

b 및 c는 서로의 disjoint (분리집합) 이다. 두 Set는 공통 요소가 전혀 없기 때문이다.

  • == 연산자는 2개 Set가 동일한지 (2개 Set의 *모든 요소가 동일한지) 확인한다.
  • isSubset(of:) 메서드는 어떤 Set가 특정 Set의 부분집합인지 (어떤 Set의 모든 값이 특정 Set에 포함되는지) 확인한다.
  • isSuperset(of:) 메서드 어떤 Set가 특정 Set의 초집합인지 (어떤 Set가 특정 Set의 모든 값을 포함하는지) 확인한다.
  • isStrictSubset(of:) 메서드 또는 isStrictSuperset(of:) 메서드는 동일한 Set가 아니며, 초집합/부분집합 관계인지 (어떤 Set가 특정 Set의 초집합/부분집합이면서 동시에 동일한 집합이 아닌 것이 맞는지) 확인한다.
  • isDisjoint(with:) 메서드는 2개 Set가 공통된 값이 전혀 없는지 확인한다.
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 타입은 키-값 (key-value) 형태의 쌍 (association)을 순서 없이 저장한다. 동일한 타입의 키, 동일한 타입의 값을 저장한다.

각 값은 키에 연결 (associated)되어 있으며, 키는 중복되지 않는 고유의 값이다. 따라서 키는 자신의 값을 찾아내는 식별자 (identifier) 역할을 한다. 

 

Array의 item과 달리 Dictionary의 item은 정해진 순서가 없다. 식별자를 기준으로 값을 검색 (look up)할 때, Dictionary가 유용하다.

예를 들면 ["AppleCider": "애플사이다", "Lemonade": "레몬에이드", "Study": "공부하다"] 라는 사전이 있을 때, "AppleCider"라는 단어를 기준으로 사전의 페이지를 넘겨서 "애플사이다"라는 뜻을 확인하는 것과 비슷하게 작동한다.

 

Note: Swift의 Dictionary 타입은 Foundation 프레임워크의 NSDictionary 클래스와 연결 (bridged) 되어 있다.

*Foundation 및 Cocoa에서 Dictionary를 사용하는 방법은 Bridging Between Dictionary and NSDictionary 에서 확인할 수 있다.

 

 

Dictionary Type Shorthand Syntax (딕셔너리 타입의 축약 문법)

타입을 지정할 때, Dictionary 타입을 표현하는 두 가지 방법이 있다. 기능적으로 동일한 방법이다.

1) Dictionary<Key, Value> 로 나타낸다.
    ("Key"는 Dictionary의 key에 저장할 값의 타입이고, "Value"는 Dictionary의 value에 저장할 값의 타입이다.)

2) [Key: Value] 로 나타낸다. 축약 표현을 사용하는 것이 바람직하다.

 

Note: Set의 값 타입과 마찬가지로 Dictionary의 키 타입은 Hashable Protocol을 준수해야 한다.

Creating an Empty Dictionary (빈 딕셔너리 생성)

Array와 마찬가지로 이니셜라이저 (initializer) 문법을 사용하여 특정 타입의 빈 Dictionary를 생성할 수 있다.

문맥상 타입을 이미 알고 있는 경우, 빈 Dictionary 리터럴 (empty dictionary literal, [:] )을 통해 빈 Dictionary를 생성할 수 있다.

var namesOfIntegers: [Int: String] = [:]
// namesOfIntegers is an empty [Int: String] dictionary

namesOfIntegers[16] = "sixteen"
// namesOfIntegers now contains 1 key-value pair
namesOfIntegers = [:]
// namesOfIntegers is once again an empty dictionary of type [Int: String]
  • 변수 namesOfIntegers의 타입을 [Int: String]로 지정했고, 빈 Dictionary로 초기화했다.
    (Key의 타입은 Int, Value의 타입은 String 이다.)
  • 이미 타입을 알고 있으므로 [:]을 할당하여 변수 namesOfIntegers을 빈 Dictionary로 만들었다.

Creating a Dictionary with a Dictionary Literal (딕셔너리 리터럴을 통한 딕셔너리 생성)

Dictionary 리터럴을 사용하여 Dictionary를 초기화할 수 있다. 

1개 이상의 키-값 쌍 (key-value pair)을 나열하는 것만으로 손쉽게 Dictionary 타입을 생성하는 방법이다.

 

키-값 쌍 (key-value pair)은 키와 값을 결합 (combination)한 형태이다. 

아래 예시는 국제공항의 정보를 저장하는 Dictionary이다. Key에는 세 글자의 코드를 저장하고, Value에는 공항 이름을 저장한다.

var airports: [String: String] = ["YYZ": "Toronto Pearson", "DUB": "Dublin"] // Dictionary 리터럴을 사용하여 초기화했다.

var airportsWithLiteral = ["YYZ": "Toronto Pearson", "DUB": "Dublin"]
  • 변수 airports의 타입을 [String: String]로 지정했다. Dictionary의 Key는 String 타입이고, Value도 String 타입이라는 뜻이다.
  • Dictionary 리터럴의 타입은 [String: String]이 맞으므로 Dictionary 리터럴을 통해 Dictionary가 두 쌍의 키-값 (2개의 item)으로 초기화되었다.
  • 변수 airportsWithLiteral은 Dictionary 리터럴을 통해 [String: String] 타입으로 추론되었다.

Note: airports를 상수가 아닌 변수로 선언한 이유는 앞으로 item을 추가/삭제할 의도가 있기 때문이다.

Accessing and Modifying a Dictionary (딕셔너리 접근 및 수정)

Dictionary에 접근 및 수정하려면, 1) Dictionary의 메서드/프로퍼티를 사용하거나, 2) 서브스크립트 (subscript) 문법을 사용하는 방법이 있다.

 

*Array와 다른 내용을 파란색으로 Marking 했다.

1) Dictionary의 메서드/프로퍼티를 사용

  • 읽기전용 (read-only) 프로퍼티 count를 사용하면, Dictionary의 item (pair) 개수를 확인한다.
  • Bool 타입의 isEmpty 프로퍼티를 사용하면, count 프로퍼티가 0인지 쉽게 알 수 있다.
    *성능 측면에서 isEmpty (O(1))가 count (O(n))보다 좋다.
  • ✅ updateValue(_:forKey:) 메서드를 사용하면, 특정 Key의 Value를 업데이트할 수 있다. 
    서브스크립트와 마찬가지로 1) 해당 Key가 이미 존재하면, Value를 수정한다. 2) 해당 Key가 존재하지 않으면, Value를 추가한다. 
    *이때 1) 값을 수정했다면, 기존의 값 (old value)을 반환한다. 2) 값을 추가했다면, nil을 반환한다.
    즉, updateValue(_:forKey:) 메서드의 반환 타입은 "옵셔널"이다. Dictionary의 Value 타입의 옵셔널 값을 반환한다. 
  • ✅ removeValue(forKey:) 메서드를 사용하면, 특정 Key에 해당하는 Key-Value 쌍을 삭제한다. (Value만 삭제하는 것이 아니다.)
    *이때 1) 해당 key가 존재하면, 삭제한 Value를 반환한다. 2) 해당 Key가 존재하지 않으면, nil을 반환한다.

2) 서브스크립트 (subscript) 문법을 사용

  • 서브스크립트 instance[index]에 원하는 item의 위치 (index)를 "Key"로 전달하면, Dictionary의 item의 Value를 꺼낸다.
    *Array는 원하는 위치 (index)를 "정수값"으로 전달하여 Array의 item을 꺼낸다.
    이때, Key에 해당하는 값이 없을 가능성이 있으므로 (possible to request a key for which no value exists) 서브스크립트의 반환 타입은 "옵셔널"이다. 1) 값이 있으면, Dictionary의 Value 타입의 옵셔널 값을 반환한다. 2) 값이 없으면, nil을 반환한다.
  • index에 Key를 전달하여 기존 item의 Value를 수정한다.
  • ❗️index에 새로운 Key를 전달하고, Value를 할당하면 어떻게 될까? 새로운 Dictionary item을 추가한다.
    *
    Array는 현재 Array 범위를 벗어난 index를 전달하면 런타임 오류가 발생한다.

    (즉, Array는 특정 index에 위치한 기존 item을 변경할 수 있지만, 새로운 item을 추가할 수는 없다.)
  • ❗️index에 Key를 전달하여 기존 item의 Value에 nil을 할당하면, Key-Value 쌍을 삭제한다. (Value만 삭제하는 것이 아니다.)
    *Array는 서브스크립트를 통해 item을 삭제할 수 없다. (remove(at:) 및 removeLast() 메서드 밖에 없다.)

 

print("The airports dictionary contains \(airports.count) items.")
// Prints "The airports dictionary contains 2 items."

if airports.isEmpty {
    print("The airports dictionary is empty.")
} else {
    print("The airports dictionary isn't empty.")
}
// Prints "The airports dictionary isn't empty."

airports["LHR"] = "London" // *Dictionary는 서브스크립트를 사용하여 새로운 item 추가가 가능하다.
// the airports dictionary now contains 3 items

airports["LHR"] = "London Heathrow" // 서브스크립트 사용 - 기존 item의 Value를 수정한다.
// the value for "LHR" has been changed to "London Heathrow"

// *updateValue(_:forKey:) 메서드 사용
if let oldValue = airports.updateValue("Dublin Airport", forKey: "DUB") { // 이미 존재하는 Key
    print("The old value for DUB was \(oldValue).")
}
// Prints "The old value for DUB was Dublin." - 업데이트 중에서 1) 값을 수정한 경우

if let oldValue = airports.updateValue("New Key", forKey: "New Value") { // 존재하지 않는 Key
    print("The old value for New Key is \(oldValue).")
} else {
    print("There is no Old Value")
}
// Prints "There is no Old Value" - 업데이트 중에서 2) 값을 추가한 경우, *oldValue가 없다. updateValue 메서드는 nil을 반환한다.

if let airportName = airports["DUB"] { // 서브스크립트 사용 - Key에 해당하는 값을 꺼낸다.
    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."

airports["APL"] = "Apple International" // 서브스크립트 사용 - Key에 해당하는 키-값 쌍을 삭제한다.
// "Apple International" isn't the real airport for APL, so delete it
airports["APL"] = nil
// APL has now been removed from the dictionary

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문을 사용하면, Array의 모든 Dictionary를 반복 (iterate)하여 접근할 수 있다.

*For-in 반복문은 Control Flow 챕터에서 자세히 다루겠습니다.

 

Dictionary의 각 item은 (key, value) 형태의 튜플 (tuple)로 반환된다.

for-in문 내부에서 해당 튜플을 상수/변수로 분해 (decompose)하여 튜플 요소에 접근할 수 있다.

*튜플에 대한 내용은 [Swift Language Guide 정독 시리즈] 1. The Basics의 튜플을 참고해주세요.

 

Array 및 Set와 달리 Dictionary는 keys 및 values 프로퍼티를 가진다. 프로퍼티를 통해 Key 값 또는 Value 값만 꺼낼 수도 있다.

또한 이렇게 꺼낸 Key 값 또는 Value 값으로 Array를 초기화할 수 있다.

keys 및 values 프로퍼티에 sorted() 메서드를 사용하면, 값을 정렬한다. (< 연산자로 정렬 (A-Z 또는 작은 수-큰 수 순서)된다.)

for (airportCode, airportName) in airports { // Key 및 Value은 튜플 형태로 반환된다.
    print("\(airportCode): \(airportName)")
}
// LHR: London Heathrow - Dictionary는 순서가 없으므로 실행할 때마다 출력 순서가 바뀐다.
// YYZ: Toronto Pearson

for airportCode in airports.keys { // keys 프로퍼티로 접근
    print("Airport code: \(airportCode)")
}
// Airport code: LHR
// Airport code: YYZ

for airportName in airports.values { // values 프로퍼티로 접근
    print("Airport name: \(airportName)")
}
// Airport name: London Heathrow
// Airport name: Toronto Pearson

let airportCodes = [String](airports.keys) // Key 값으로 Array 초기화
// airportCodes is ["LHR", "YYZ"]

let airportNames = [String](airports.values) // Value 값으로 Array 초기화
// airportNames is ["London Heathrow", "Toronto Pearson"]

 

- Reference

 

🍎 포스트가 도움이 되었다면, 공감🤍 / 구독🍹 / 공유🔗 / 댓글✏️ 로 응원해주세요. 감사합니다.

Comments