👨🏻‍💻iOS 공부/Swift_CS공부

[Swift] Dictionary 파헤치기

728x90
반응형

https://developer.apple.com/documentation/swift/dictionary

 

Apple Developer Documentation

 

developer.apple.com

Dictionary란?

Key-value 쌍의 원소로 이루어진 collection을 말한다

Declaration

@frozen struct Dictionary<Key, Value> where Key : Hashble

 

이전에 Optional의 구조를 살펴볼 때도 @frozen 키워드를 본 적이 있다. 

 

Future versions of the library can’t change the declaration by adding, removing, or reordering an enumeration’s cases or a structure’s stored instance properties

즉, @frozen으로 선언하면 enum이나 struct의 stored instance properties를 추후에 추가, 삭제, 재배열등 변경할 수 없다는 것을 의미한다. 

 

그리고 struct로 이루어져 있으며, Key는 Hashble 프로토콜을 준수하는 타입이어야 한다.

 

Overview

딕셔너리는 hash table 타입으로, 보유하고 있는 값들에 빠르게 접근할 수 있도록 도와준다. 테이블 내의 각 입력들은 문자나 숫자 형태의 hashble한 키를 사용함으로써 인식된다. 결과값은 어떠한 타입으로도, 키를 사용하여 이에 대응하는 값을 얻을 수 있다. 

 

딕셔너리를 한 번 만들어보자!

딕셔너리는 key-value들의 쌍으로 이는 콜론( : )으로 구분되어 있고, key-value 쌍들은 쉼표( , )로 구분되어 있으며, 대괄호로( [ ] ) 둘러싸여있다. 

 

말로만 보면 이해가 어려울 것이다. 

아래의 HTTP response를 딕셔너리로 만든 예시를 보며 이해해보자. 

 

var responseMessages = [200: "OK",
                        403: "Access forbidden",
                        404: "File not found",
                        500: "Internal server error"]

 

이 딕셔너리의 경우 [Int: String] 타입인 것을 알 수 있다. Key가 Int, Value가 String인 것이다. 

 

만약 값을 추가하지 않고 빈 딕셔너리를 만들고 싶다면 다음과 같이 입력해주면 된다. 

 

var emptyDict: [String: String] = [:]

어떤 타입의 key와 value를 사용할지만 선언해준다면 빈 딕셔너리를 쉽게 선언할 수 있다. 

또한 어떠한 타입이더라도 Hashble 프로토콜만 채택하고 있다면 딕셔너리의 key로 사용될 수 있다. 그렇기에 커스텀한 타입을 만든 뒤 Hashble 프로토콜을 채택하여 딕셔너리의 key로 사용할 수도 있는 것이다. 

 

Getting and Setting Dictionary Values

딕셔너리의 값에 접근하기 위한 보편적인 방법은 키를 subscript 처럼 사용하는 것이다. 

(subscript란 콜렉션, 리스트, 시퀀스 등 집합의 특정 member elements에 간단하게 접근할 수 있는 문법이다)

print(responseMessages[200])
// Prints "Optional("OK")"

이와 같이 딕셔너리를 키로 subscripting할 경우 optional한 값을 반환받게 되는데, 이는 딕셔너리가 주어진 키에 해당하는 값을 가지고 있지 않을 수 있기 때문이다.

 

아래의 사례를 보면 보유하고 있는 값과 그렇지 않은 값들을 불러오는 방법을 알 수 있다. 

 

let httpResponseCodes = [200, 403, 301]
for code in httpResponseCodes {
    if let message = responseMessages[code] {
        print("Response \(code): \(message)")
    } else {
        print("Unknown response \(code)")
    }
}
// Prints "Response 200: OK"
// Prints "Response 403: Access forbidden"
// Prints "Unknown response 301"

아까 말했듯이 optional 타입으로 반환되기 때문에 optional binding을 통해서 값을 언래핑해줘야 한다. 

(옵셔널에 대해서 잘 모르겠다면, 한 번 살펴보고 오자: 옵셔널 정리)

 

보면 301은 키로 가지고 있지 않기 때문에 else문을 통과하는 모습을 볼 수 있다. 

 

만약 옵셔널 바인딩을 하지 않고 없는 key를 그냥 부르게 되면 어떻게 될까? 

print(responseMessages[301]) // nil

크래시가 나거나 하지는 않고 nil을 반환한다.

 

또한 subscript를 통해 딕셔너리의 key와 value를 업데이트, 수정, 삭제할 수 있다. 새로운 key-value쌍을 추가하기 위해서는 아직 딕셔너리에 없는 키에 값을 할당해주면 된다. 즉 이전에 301은 키로 없었으니, 301을 키로 설정하고 이에 맞는 값을 할당해주면 되는 것이다. 

 

responseMessages[301] = "Moved permanently"
print(responseMessages[301])
// Prints "Optional("Moved permanently")"

 

기존의 값을 업데이트 하는 방법은 간단하다. 딕셔너리에서 이미 값을 가지고 있는 키를 불러오고 이에 새로운 값을 할당해주면된다. 다만 여기서 존재하는 값이 아닌 nil을 할당하게 된다면 해당 key와 value는 사라지게 된다. 

 

예를 들어 아래와 같이 404 키에 해당하는 값을 다른 문자열로 바꿔주고, 500 키에 해당하는 값을 nil로 바꿔주자. 그러면 404에 해당하는 값은 변경되고 500에 해당하는 value는 사라지고, 당연히 Key도 사라지게 된다. 

 

responseMessages[404] = "Not found"
responseMessages[500] = nil
print(responseMessages)
// Prints "[301: "Moved permanently", 200: "OK", 403: "Access forbidden", 404: "Not found"]"

mutable(가변)한 딕셔너리 인스턴스의 경우, 키를 통해 접근한 내부의 값들을 변경할 수도 있다. 

예를 들어 숫자 배열을 값으로 가지는 딕셔너리를 구성한 후, 해당 값을 내림차순으로 정렬하는 경우가 이에 해당한다. 

 

var interestingNumbers = ["primes": [2, 3, 5, 7, 11, 13, 17],
                          "triangular": [1, 3, 6, 10, 15, 21, 28],
                          "hexagonal": [1, 6, 15, 28, 45, 66, 91]]
for key in interestingNumbers.keys {
    interestingNumbers[key]?.sort(by: >)
}

print(interestingNumbers["primes"]!)
// Prints "[17, 13, 11, 7, 5, 3, 2]"

여기서는 간편하게 강제 언래핑( ! )을 사용하였지만, 키가 있는 것을 알지 못하는 상태라면 안전하게 옵셔널 바인딩 처리를 해주면 된다.

 

Iterating Over the Contents of a Dictionary

모든 딕셔너리는 순서가 없는 key-value 쌍의 콜렉션 타입이다. 딕셔너리 내의 값을 반복적으로 접근하고 싶을 때는 for문을 통해 key와 value에 대해 따로 접근이 가능하다. 

let imagePaths = ["star": "/glyphs/star.png",
                  "portrait": "/images/content/portrait.jpg",
                  "spacer": "/images/shared/spacer.gif"]

for (name, path) in imagePaths {
    print("The path to '\(name)' is '\(path)'.")
}
// Prints "The path to 'star' is '/glyphs/star.png'."
// Prints "The path to 'portrait' is '/images/content/portrait.jpg'."
// Prints "The path to 'spacer' is '/images/shared/spacer.gif'."

딕셔너리의 key-value쌍의 순서는 변하지 않지만, 예측할 수 없긴 하다. 만약 순서가 있는 key-value쌍과 딕셔너리의 장점인 key를 빠르게 찾는 것이 필요하지 않다면 KeyValuePairs 타입을 대안으로 사용할 수 있다. 

 

또한 딕셔너리가 특정한 값을 가지고 있는지를 확인하기 위해 contains(where: ) 또는 firstIndex(where: ) 메서드를 사용한다. 

 

let glyphIndex = imagePaths.firstIndex(where: { $0.value.hasPrefix("/glyphs") })
if let index = glyphIndex {
    print("The '\(imagePaths[index].key)' image is a glyph.")
} else {
    print("No glyphs found!")
}
// Prints "The 'star' image is a glyph.")

 

imagePaths에 key가 아니라 인덱스로 접근하고 있는 것을 알 수 있다. key 기반의 subscript가 아닌 인덱스 기반의 subscipt를 하게 되면 이에 해당하는 (옵셔널이 아닌 튜플) key-value 쌍을 얻게 된다

 

print(imagePaths[glyphIndex!])
// Prints "(key: "star", value: "/glyphs/star.png")"

딕셔너리의 인덱스들은 딕셔너릭가 추가적인 버퍼를 할당하지 않고, 추가된  값을 저장할 공간이 충분하다면 추가에 대해서 유효하다. 

딕셔너리가 버퍼를 벗어나게 되면 기존의 인덱스들은 어떠한 알림없이 무효화 될 수 있다. 

 

얼마나 많은 새로운 값들을 추가할지 알고 있다면 처음 딕셔너리 생성시 버퍼의 크기를 할당하여 정확하게 버퍼의 크기를 정할 수 있다. 

 

let dictionary = [Int:Int](minimumCapacity: 15)

Bridging Between Dictionary and NSDictionary

연산자로 사용함으로써 Dictionary와 NSDictionary를 브릿징(bridging)할 수 있다. 연결을 가능하게 하려면, 딕셔너리의 key와 value는 클래스여야 하고, @objc 프로토콜 또는 Foundation 타입으로 연결되는 유형이여야 한다. 

 

Dictionary를 NSDictionary로 브릿징하는 것의 시간복잡도와 공간복잡도는 항상 O(1)이다. 딕셔너리의 Key와 value 타입이 클래스나 @objc 프로토콜이 아닌 경우, 첫 번째 접근시 필요한 요소 연결이 발생하게 되는데, 이러한 이유 때문에 딕셔너리의 컨텐츠에 접근하는 첫 번째 작업에는 O(n)이 걸릴 수도 있다. 

 

추가적으로 Hash 타입을 준수하기 때문에 Key를 통해 값을 탐색하는데에는 평균 시간복잡도가 O(1)이나, 모든 key값에 대해 해시 충돌이 일어나는 최악의 경우 O(n)이 될 수 있다. 

해시 충돌 : 다른 key값이 같은 value로 나오는 경우

ex) dict[0]과 dict[1]이 같은 value를 가리키는 경우

해시 충돌을 해결하는 방법 중에 Linear Probing이라는 방법이 있다. 해시 충돌이 발생할 경우, 해당 테이블 주소부터 빈 공간이 나올 때 까지 순회하며, 빈 공간이 나오면 그 때 key-value를 저장하는 방식이다. 

 

즉 위처럼 dict[0]과 dict[1]이 충돌될 때(테이블 주소가 같을 때) + 빈공간이 3번째 일 때

                충돌
[0:값0]      ->     1 : [1:값a]
                           2 : [2:값b]
                           3 : empty

3에 0의 key와 value를 넣어 충돌을 방지하는 것이다.

1 : [1:값a]
2 : [2:값b]
3 : [0:값0]

그렇기 때문에 최악의 경우라는 것은 모든 key가 해시 충돌하는 경우, 즉 매번 빈 공간을 찾아다녀야 하기 때문에 O(n)의 시간 복잡도가 나오게 되는 것이다. 

 

다시 돌아와서! NSDictionary에서 Dictionary로 브릿징(bridging)하게 되면 먼저 copy(with: ) 메서드가 호출하여, immutable(불변)한 복사본을 가져온 다음 O(1) 시간이 걸리는 Swift 부기(기입) 작업을 수행한다. 이미 불변의 상태인 NSDictionary 인스턴스의 경우 일반적으로 copy(with: )는 동일한 딕셔너리는 O(1)의 시간으로 반환한다. 동일하지 않다면 성능을 알 수 없다. NSDictionary와 Dictionary 인스턴스는 두 개의 딕셔너리 인스턴스가 버퍼를 공유할 때 동일한 copy-on-write 최적화를 사용하여 버퍼를 공유한다. 

 

아직 NSDictionary와 Dictionary의 차이나 브릿징하는 방법은 확실히 알지 못하고 있는 것 같다.

간단하게 차이만 알아두고 추후에 자세히 보도록 하자!

 

Dictionary NSDictionary
구조체(struct), 값 타입 클래스(class), 참조 타입
swift 기본 라이브러리  cocoa 라이브러리
타입 추론 가능( 특정 타입이 있어야 한다) 타입이 정해져 있지 않다
swift Objective-C

 

 

https://stackoverflow.com/questions/25554259/what-is-difference-between-nsdictionary-vs-dictionary-in-swift

 

What is difference between NSDictionary vs Dictionary in Swift?

I'm learning Swift, and I can see Dictionary in it. But there are lots of examples that are using NSDictionary with Swift. What's the difference between these two? I want to use an array with index...

stackoverflow.com

 

728x90
반응형