HashMap으로 데이터 세기

RustBeginner
지금 연습하기

소개

벡터는 숫자 위치를 기준으로 값을 정리합니다. 반면 HashMap<K, V>는 K 타입의 각 고유 키를 V 타입의 값 하나와 연결합니다. 따라서 개수, 설정 및 기타 키 기반 데이터를 다루는 데 유용합니다.

작은 단어 수 맵을 만들고, 개수를 조회하고 교체합니다. 또한 기존 단어와 없는 단어에 entry API를 사용하고, 맵의 지정되지 않은 키 순서에 의존하지 않고 모든 값을 순회합니다.

키로 값 삽입하고 조회하기

이 단계에서는 키-값 쌍 두 개를 삽입하고 개수 하나를 안전하게 조회합니다.

프로젝트 디렉터리로 이동한 다음 소스 파일을 엽니다.

cd /home/labex/project/word-counts
nano src/main.rs

미리 준비된 경로 std::collections::HashMap은 Rust 표준 라이브러리의 컬렉션 모듈 안에 있는 HashMap을 가리킵니다. use 줄은 이 긴 경로에 로컬에서 사용할 짧은 이름 HashMap을 부여합니다. 지금은 이를 미리 준비된 import 코드로 생각합니다. 경로와 import는 이후 모듈 Lab에서 자세히 배웁니다. HashMap<String, u32>는 소유된 문자열 키와 부호 없는 정수 개수를 저장합니다. Step 1a 주석을 다음 코드로 바꿉니다.

    let _ = counts.insert(String::from("rust"), 1);
    let _ = counts.insert(String::from("safe"), 1);

insert는 해당 키에 이전에 저장되어 있던 값을 반환합니다. 여기서는 두 키가 새 키이므로 결과는 None입니다. let _ = ...; 패턴은 연산을 실행하되 그 결과를 의도적으로 버립니다. 이 단독 _는 _returned_id와 같은 이름과 다릅니다. 단독 와일드카드는 나중에 사용할 수 있는 바인딩을 만들지 않습니다.

Step 1b 주석을 다음 코드로 바꿉니다.

    match counts.get("rust") {
        Some(count) => println!("Rust count: {count}"),
        None => println!("Rust count: missing"),
    }

get은 빌린 텍스트 "rust"를 받아 키나 개수를 맵 밖으로 이동시키지 않고 Option<&u32>를 반환합니다. 저장한 후 종료하고 다음 명령으로 확인한 뒤 실행합니다.

cargo check
cargo run --quiet
Unique words: 2
Rust count: 1

첫 번째 줄은 키가 고유하다는 것을 보여 줍니다. 두 번째 줄은 조회를 통해 저장된 개수를 찾았다는 것을 보여 줍니다.

기존 개수 교체하기

이 단계에서는 기존 키에 insert를 사용하고, 교체된 값을 확인합니다.

소스 파일을 엽니다.

nano src/main.rs

Step 2 주석을 다음 코드로 바꿉니다.

    let previous = counts.insert(String::from("rust"), 2);
    println!("Previous rust count: {previous:?}");

맵은 키마다 값 하나만 유지합니다. 새 값 2가 기존 값 1을 교체하고, 반환된 Some(1)을 통해 이 교체 결과를 확인할 수 있습니다. 저장한 후 종료하고 다음 명령을 실행합니다.

cargo run --quiet

새로 추가된 줄은 다음과 같아야 합니다.

Previous rust count: Some(1)

교체는 새 키를 만들지 않으므로 고유 단어 수는 계속 2입니다.

Entry API로 개수 업데이트하기

이 단계에서는 같은 API를 사용해 이미 존재하는 단어 하나와 없는 단어 하나의 개수를 증가시킵니다.

entry(key).or_insert(default)는 키에 이미 값이 있으면 해당 값에 대한 가변 참조를 반환합니다. 값이 없으면 기본값을 삽입한 뒤 그 값에 대한 가변 참조를 반환합니다. 소스 파일을 엽니다.

nano src/main.rs

Step 3 주석을 다음 코드로 바꿉니다.

    *counts.entry(String::from("safe")).or_insert(0) += 1;
    *counts.entry(String::from("fast")).or_insert(0) += 1;
    println!("Safe count: {}", counts["safe"]);
    println!("Fast count: {}", counts["fast"]);

간결한 업데이트 코드를 안쪽부터 읽어 봅니다. 먼저 entry(key)는 해당 키에 연결된 위치를 선택합니다. 그 위치에 값이 있든 없든 사용할 수 있습니다. 그런 다음 or_insert(0)은 값이 존재하도록 만들고, 해당 값을 가리키는 가변 참조 &mut u32를 반환합니다.

맨 앞의 *는 이 참조가 가리키는 u32에 접근합니다. 마지막으로 += 1이 1을 더하고 새 개수를 맵에 저장합니다.

따라서 safe는 2가 됩니다. 아직 없는 fast는 0에서 시작한 뒤 1이 됩니다. 대괄호는 키로 값을 읽습니다. 키가 없으면 맵 인덱싱에서 패닉이 발생할 수 있지만, 여기서는 앞의 두 entry 호출이 두 키의 존재를 보장하므로 안전합니다. 키가 없을 수도 있는 경우에는 계속 get을 사용합니다.

저장한 후 종료하고 다음 명령을 실행합니다.

cargo run --quiet

이제 마지막 줄은 다음과 같아야 합니다.

Safe count: 2
Fast count: 1

하나의 연산으로 값이 있는 키와 값이 없는 키를 모두 처리했습니다.

키 순서에 의존하지 않고 순회하기

이 단계에서는 저장된 모든 개수를 순회하고 순서와 관계없이 합계를 계산합니다.

해시 맵은 키를 순회하는 순서를 안정적으로 보장하지 않습니다. 따라서 초보자용 확인에서는 키가 특정 순서로 출력된다고 가정하지 않아야 합니다. values() 메서드는 저장된 모든 값을 방문하므로 합계를 계산하기에 충분합니다.

소스 파일을 열고 Step 4 주석을 다음 코드로 바꿉니다.

nano src/main.rs
    let mut total = 0;
    for count in counts.values() {
        total += count;
    }
    println!("Total occurrences: {total}");

여기서 count는 공유 참조입니다. +=는 참조가 가리키는 정수를 더할 수 있습니다. 저장한 후 종료하고 다음 명령으로 확인한 뒤 실행합니다.

cargo check
cargo run --quiet

마지막 줄은 다음과 같아야 합니다.

Total occurrences: 5

이 합계는 순회 순서에 의존하지 않고 rust = 2, safe = 2, fast = 1을 더한 결과입니다.

요약

키 기반 개수를 삽입하고, 조회하고, 교체하고, 증가시켰습니다. 그런 다음 안정적인 키 순서를 가정하지 않고 맵의 모든 값을 순회했습니다.