HashMap
HashMap은 Key에 의해 Value를 빠르게 찾을 수 있는 컬렉션으로서, Hashtable 자료구조를 사용하여 Key와 Value를 매핑하는 구조를 갖는다. HashMap은 표준 라이브러리로서 std::collections::HashMap 에 있으며, HashMap<K, V> 제네릭으로 구현되어 있고, Key 타입은 K, Value의 타입은 V에 들어간다.
HashMap 생성 및 초기화
HashMap을 생성하는 가장 기본적인 방법은 HashMap::new() 함수를 호출하는 것이다. 이 함수는 비어 있는 HashMap을 만들게 되고, 여기에 insert() 메서드를 사용하여 Key와 Value를 넣을 수 있다.
아래 예제는 문자열을 Key로 갖고, 숫자를 Value로 갖는 HashMap을 예시한 것이다. 먼저 HashMap::new()을 사용하여 빈 HashMap을 만들고, HashMap의 insert() 메서드를 사용하여 Key-Value 매핑값을 넣고 있다.
use std::collections::HashMap; fn main() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert(String::from("Korea"), 5); let germ = String::from("Germany"); map.insert(germ, 4); println!("{:?}", map); }
만약 Key 혹은 Value에 String과 같은 owned value가 전달된다면, 그 소유권(ownership)은 HaspMap으로 이동(move)한다. 따라서, 위 예제의 변수 germ은 map.insert(germ, 4); 이후에 사용될 수 없다.
위의 예에서 HashMap을 출력해 보기 위해서 {:?} 을 사용하였으며 이를 통해 다음과 같은 결과가 출력될 것이다.
{"Korea": 5, "Germany": 4}
HashMap을 만드는 또 다른 방법으로 2개의 벡터를 이용할 수 있다. 즉, Key를 나타내는 벡터와 Value를 나타내는 벡터가 있을 때, 두 벡터를 zip() 메서드를 통해 [Key, Value] 튜플들의 집합으로 만들고, 다시 collect() 메서드를 사용하여 이를 HashMap으로 바꿔주는 방식이다.
use std::collections::HashMap; fn main() { let countries = vec![String::from("Korea"), String::from("Germany")]; let scores = vec![5, 4]; let map: HashMap<_, _> = countries.into_iter().zip(scores.into_iter()).collect(); println!("{:?}", map); }
collect()는 여러 컬렉션 타입(예: 벡터, 해시맵 등)으로 리턴될 수 있으므로, 리턴 타입을 명시하기 위해 HashMap<_, _> 을 사용하였다. 여기서 _ 을 사용한 이유는 컴파일러가 구체적인 타입을 추론할 수 있기 때문인데, 결과적으로 컴파일러는 HashMap<String, i32> 으로 추론할 것이다.
HashMap 읽기
HashMap에서 데이타를 가져오기 위해서는 get() 메서드를 사용할 수 있다. 즉, HashMap.get(key)를 사용하여 value를 리턴하는 것인데, 이때 리턴되는 타입은 (값이 없을 수 있으므로) Option<&V> 타입이다.
use std::collections::HashMap; fn main() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert(String::from("Korea"), 5); map.insert(String::from("Germany"), 4); let val = map.get("Korea"); // println!("{:?}", val); // Some(5) 출력함 match val { None => println!(""), Some(v) => println!("{}", v) // 5 출력함 } }
위 예제에서 map.get("Korea")는 Key가 "Korea"일 때의 value를 Option<&i32> 로 리턴하는데, 이를 "{:?}" 으로 프린트하면 Some(5) 이 출력된다. 또한, match 를 사용하여 None인 경우와 Some인 경우를 나누어 핸들링 할 수 있을 것이다.
HashMap의 요소 전체를 루프를 돌며 처리하기 위해서는 아래와 같이 for 를 사용하여 (key, value)를 얻어와서 사용할 수 있다.
use std::collections::HashMap; fn main() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert(String::from("Korea"), 5); map.insert(String::from("Germany"), 4); for (k, v) in &map { println!("{}: {}", k, v); } }
HashMap 수정
HashMap의 수정은 HashMap.insert() 메서드를 사용하여 기본적으로 HashMap 키가 있을 때 기존의 값을 새로운 값으로 수정하게 된다. 즉, HashMap.insert() 메서드를 사용하여, 기존 Key가 없을 경우는 새 엔트리를 추가하고, 기존 Key가 있을 경우는 해당 Key의 Value를 수정한다. 예를 들어, 아래 예제는 두번째 map.insert()에서 "Korea"에 대한 값을 10으로 수정(overwrite)하므로 결과적으로 "Korea" key는 10을 value로 갖게 된다.
use std::collections::HashMap; fn main() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert(String::from("Korea"), 5); map.insert(String::from("Korea"), 10); // overwrite value }
만약 기존 Key의 값이 있는 경우는 값을 수정하지 않고, Key가 없는 경우에만 값을 수정하기 위해서는 아래 예제와 같이 HashMap.entry(key)를 사용하여 해당 HashMap 엔트리를 가져온 후, 그 엔트리가 없는 경우에만 or_insert()을 사용하여 value 값을 추가하게 한다. 아래 예제에서 (1) Korea 키는 이미 추가되어 있으므로 값을 수정하지 않고, (2) USA는 키가 없으므로 0을 추가한다.
use std::collections::HashMap; fn main() { let mut map: HashMap<String, i32> = HashMap::new(); map.insert(String::from("Korea"), 5); map.insert(String::from("Germany"), 4); // (1) Korea 키는 있으므로 갱신 안함 map.entry(String::from("Korea")).or_insert(0); // (2) USA 키는 없으므로 0을 추가 map.entry(String::from("USA")).or_insert(0); println!("{:?}", map); }
{"Korea": 5, "Germany": 4, "USA": 0}