딕셔너리(Dictionary)는 사전에서 단어와 의미가 연결되어 있는 것과 같이 고유 키(Key)와 그에 대응하는 값(Value)을 연결해 데이터를 저장하는 자료형이다. 정의하는 형식은 다음과 같다.
[키 : 데이터, 키 : 데이터, ...]
딕셔너리의 경우 사용할 때 주의할 점이 몇 가지 있다.
- 하나의 키는 하나의 데이터에만 연결되어야 한다.
- 하나의 딕셔너리에서 키는 중복될 수 없다. 중복해서 선언하면 아이템이 추가되는 것이 아니라 수정이 이루어지기 때문에 기존에 키에 연결된 값은 삭제된다.(덮어쓰기가 된다는 뜻)
- 저장할 수 있는 데이터의 타입에는 제한이 따로 없지만, 하나의 딕셔너리에 저장되는 자료형은 모두 같아야한다.
- 딕셔너리의 아이템에는 순서가 따로 없다. 하지만 키에는 내부적으로 순서가 정해지므로 for ~ in구문을 사용해 순회탐색이 가능하다.
- 딕셔너리에 사용할 수 있는 키의 타입은 거의 제한이 없으나 해시(Hash) 연산이 가능한 타입이어야 한다.
딕셔너리에서 키로 사용되는 타입은 해시연산을 지원해야한다. 데이터 타입에 해시(Hash) 값을 추출할 수 있는 기능이 포함되어 있어야 한다는 것이다. 왜냐하면 딕셔너리가 아이템을 저장할 때는 입력된 키를 그대로 사용하는 것이 아니라 내부적으로 해시 연산을 거친 값으로 변환한 다음 이를 정렬하여 사용하기 때문이다.
스위프트에서 해시 연산을 위해선 Hashable 프로토콜이 구현되어야 하는데, 문자열 타입은 Hashable 프로토콜이 이미 구현되어 있으므로 대부분의 딕셔너리는 문자열을 키로 사용한다.
다음은 딕셔너리를 정적으로 정의한 예이다.
var capital = ["KR":"Seoul", "EN":"London", "FR":"Paris"]
// 딕셔너리의 참조
capital["KR"] // "Seoul"
capital["EN"] // "London"
capital["FR"[ // "Paris"
딕셔너리의 선언과 초기화
딕셔너리도 배열과 마찬가지로 처음부터 모든 아이템을 한 번에 정의하는 것이 아닌 빈 딕셔너리를 선언하고 초기화한 다음 후에 필요에 따아이템을 추가하는식으로 많이 사용한다. 빈 딕셔너리를 선언하고 초기화 하는 구문 형식은 다음과 같다.
// 정석
Dictionary<키 타입, 값 타입>()
// 예시
Dictionary<String, Int>()
Dictionary<String, String>()
Dictionary<String, AnyObject>()
Dictionary<Character, String>()
// 간결한 형식
[ 키로 사용할 타입 : 값으로 사용할 타입 ]()
// 예시
[String : Int]()
[String : String]()
[String : AnyObject]()
[Character : String]()
이 형식을 사용한 예시를 살펴보자.
// 정석
var capital = Dictionary<String, String>()
// 축약형
var capital = [String : String]()
위의 예제와 같이 선언과 초기화가 한 번에도 가능하지만 나눠서도 가능하다.
// 정석
var capital : Dictionary<String, String> // 선언
capital = Dictionary() // 초기화
// 축약형
var capital : [String : String] // 선언
capital = [String : String]() // 초기화
이렇게 표현할 때 주의할점이 있다. 배열과 달리 타입 어노테이션으로 키의 값의 타입이 이미 선언되었다고 하여 초기화 구문에서 함부로 타입을 생략해서는 안 된다는 것이다. ex) capital = [ : ]()
딕셔너리에 동적으로 아이템 추가하기
딕셔너리에 동적으로 아이템을 추가하는 방법은 메소드를 사용해 추가하는 방법, 직접 새로운 키와 값을 대입하는 방법으로 두 가지가 존재한다. 먼저 키와 값을 직접 대입해 아이템을 추가하는 방법에 대해 알아보자.
var newCapital = [String : String]()
newCapital["JP"] = "Tokyo"
딕셔너리에서 아이템을 참조할 때와 비슷하게 그냥 대입해 주면된다. 이로써 newCapital은 "JP" : "Tokyo"로 이뤄진 아이템 하나를 저장하게 되었다.
딕셔너리에 아이템이 저장되었는지는 isEmpty 속성을 통해 확인할 수 있다. 딕셔너리의 크기를 알려주는 count 값이 0일 때 isEmpty 속성의 값은 true로 설정된다.
...(중략)...
if newCapital.isEmpty {
print("딕셔너리가 비어 있는 상태입니다")
} else {
print("딕셔너리의 크기는 현재 \(newCapital.count)입니다")
}
/* 실행결과
딕셔너리의 크기는 현재 1입니다.
이번엔 메소드를 사용해 동적으로 값을 할당하는 방법에 대해 알아보자. 딕셔너리에 값을 할당하는데 사용되는 메소드는 updateValue(_:forKey:)이다. 이 메소드는 키가 있는지 없는지에 따라 수행역할일 달라지는데, 기존에 저장된 키가 존재하는 경우 연결된 값을 수정하는 역할을 하지만 새로운 키가 입력되면 아이템을 추가하는 역할도 수행한다.
조금 재밌는 점은 이 메소드를 사용해 딕셔너리에 저장된 값을 수정하면 수정하기 이전의 값이 결과값으로 반환된다는 점이다. 따라서 새로운 키와 값을 이 메소드를 사용해 추가하게되면 기존에 저장되어 있던 값이 없으므로 nil을 반환한다.
...(중략)...
newCapital.updateValue("Seoul", forKey: "KR")
// "KR" : "Seoul" 데이터가 추가되고 nil을 반환
newCapital.updateValue("London", forKey: "EN")
// "EN" : "London" 데이터가 추가되고 nil을 반환
newCapital.updateValue("Osaka", forKey: "JP")
// "JP" : "Osaka" 데이터로 수정되고 "Tokyo"를 반환
위에서 설명했듯 3번 째 예시를 보면 기존에 존재했던 "JP" : "Tokyo"의 값이 "JP" : "Osaka"로 수정되면서 기존 값인 "Tokyo"를 반환하는 모습을 확인할 수 있다.
딕셔너리에 저장된 아이템을 제거하는 방법은 두 가지가 존재한다. 하나는 키에 연결된 값에 직접 nil을 할당하는 것이고, 다른 하나는 removeValue(forKey:) 메소드를 사용하는 방법이다. nil은 "값이 없음"이라는 의미를 나타내는 특수 값으로 구체적으로 살펴보면 다르지만 C언어의 null과 비슷하다고 이해해도 큰 문제는 없을 것 같다.
메소드를 사용해 값을 삭제한 예를 살펴보자.
...(중략)...
newCapital.updateValue("Ottawa", forKey: "CA")
newCapital.updateValue("Beijing", forKey: "CN")
/*
(key "CN", value "Beijing")
(key "CA", value "Ottawa")
(key "EN", value "London")
(key "KR", value "Seoul")
(key "JP", value "Osaka")
먼저 nil을 할당하여 값을 삭제해보자.
newCapital["CN"] = nil
/*
(key "CA", value "Ottawa")
(key "EN", value "London")
(key "KR", value "Seoul")
(key "JP", value "Osaka")
"CN" 키에 해당하는 아이템이 삭제된 것을 확인할 수 있다. 이어서 removeValue(forKey:) 메소드를 사용해 아이템을 삭제해보자.
newCapital.removeValue(forKey: "CA")
/*
(key "EN", value "London")
(key "KR", value "Seoul")
(key "JP", value "Osaka")
이 메소드는 실행 결과로 삭제된 아이템의 값을 반환한다. 이 특성을 이용하면 다음과 같은 구문을 사용할 수도 있다.
...(중략)...
// "CA"에 해당하는 값을 삭제하고, 반환된 값을 removedValue에 할당한다.
if let removedValue = newCapital.removedValue(forKey: "CA") {
print("삭제된 값은 \(removedValue)입니다")
} else {
print("아무 것도 삭제되지 않았습니다")
}
/* 실행결과
삭제된 값은 Ottawa입니다
잠깐 짚고 넘어가야할 부분이 있다. 바로 배열의 인덱스와 딕셔너리의 키에 대한 접근 차이이다. 배열은 인덱스를 참조하기 위해 참조할 인덱스가 이미 만들어져 있어야만 한다는 제약조건이 있다. 하지만 딕셔너리의 경우 키 자체가 일련의순서를 가지고 있지 않다. 또한 타입은 알 수 있을지라도 어떤 데이터가 키로 사용될지 미리 알 수 없으므로 새로운 키가 입력되면 이 키와 값을 저장하기 위한 튜플을 하나 만들어 저장하면 될 뿐이다. 새로운 인덱스 공간을 확보하고 크기를 늘릴필요가 없는 것이다.
하지만 이런 편리한 특성 때문에 문제가 하나 발생하는데, 바로 '키와 값에대한 보장이 없다'는 점이다. 딕셔너리는 고유 키에 대한 제약이 덜하므로 프로그램이 딕셔너리로부터 키를 호출해서 저장된 값을 불러올 때 없는 키를 호출했을 가능성을 항상 염두에 두어야한다.
이러한 경우를 스위프트에선 옵셔널(Optional)이라는 개념을 통해 안전하게 사용할 수 있도록 해준다. 실제 딕셔너리의 값을 print해보면 Optional(값)의 형태로 사용이 되는 것을 볼 수 있다.
딕셔너리의 순회 탐색
딕셔너리에 저장된 아이템끼리는 순서가 따로 없지만, 아이템을 튜플 형식으로 저장할 때 내부적으로 키를 바탕으로 한 해시 연산 값을 기준으로 정렬하기 때문에 이 특성을 사용하면 순회탐색이 가능하다.
...(중략)...
for row in newCapital {
// 딕셔너리에서 키-값을 row 상수의 튜플로 받음
let (key, vluae) = row
print("현재 데이터는 \(key) : \(value)입니다")
}
for ~ in 구문이 반복되면서 딕셔너리에 저장된 아이템이 차례대로 row 상수에 대입된다. 딕셔너리는 키와 값으로 이루어진 아이템을 내부에 저장할 때 튜플 타입으로 저장하므로 순회탐색을 실행하면 차례대로 튜플 타입으로 된 키-값이 row 상수에 할당된다.
다음과 같이 좀 더 간결하게 표현하는 것도 가능하다.
...(중략)...
for (key, value) in newCapital {
print("현재 데이터는 \(key) : \(value)입니다")
}
실행결과는 다음과 같다.
현재 데이터는 EN : London입니다
현재 데이터는 KR : Seoul입니다
현재 데이터는 JP : Osaka입니다
여기서 유의해야할 부분은 딕셔너리를 순회 탐색하면 입력한 값의 순서대로 탐색이 되지 않는다는 점이다. 기본적으로 딕셔너리는 고유 키에 대한 해시 처리 값을 기준으로 내부 정렬하기 때문에 데이터가 우리의 생각대로 저장되지 않을 수도 있다는 점을 기억해야한다.
출처 : 꼼꼼한 재은씨의 Swift 문법편
'Swift > 문법' 카테고리의 다른 글
스위프트(Swift) - 함수(Function) Ⅰ. 함수의 기본 개념 (0) | 2022.05.12 |
---|---|
스위프트(Swift) - 옵셔널(Optional) (0) | 2022.05.11 |
스위프트(Swift) - 집단 자료형(Collection Types) Ⅲ. 튜플(Tuple) (0) | 2022.05.09 |
스위프트(Swift) - 집단자료형(Collection Types) Ⅱ. 집합(Set) (0) | 2022.05.06 |
스위프트(Swift) - 집단 자료형(Collection Types) Ⅰ. 배열(Array) (0) | 2022.05.05 |