본문 바로가기

코딩/Rust

[Rust] 5장 컬렉션(Collection) - 벡터(Vector), 문자열(String), 해시맵(HashMap)

해당 게시판에 올라오는 포스트는 The Rust Programming Language 의 한글 텍스트 버전을 보며 작성된 학습 게시판입니다.

전체 목차

[코딩/Rust] - [Rust] 1장 안정성 높은 언어

[코딩/Rust] - [Rust] 2장 일반적인 프로그래밍 개념

[코딩/Rust] - [Rust] 3장 러스트만의 특이함

[코딩/Rust] - [Rust] 4장 열거형(Enum), 모듈화(Module)

[코딩/Rust] - [Rust] 5장 컬렉션(Collection) - 벡터(Vector), 문자열(String), 해시맵(HashMap)

글 목차

  1. 벡터(Vector)
  2. 문자열(String)
  3. 해시맵(HashMap)

1. 벡터(Vector)

배열(array), 리스트(List) 등 과 같이 서로 이웃하도록 메모리에 단일 데이터 구조로 저장하는 타입니다.

벡터 생성

Vec::new 함수를 사용하려면 타입을 명시(type annotation)해야한다. 

let v: Vec<i32> = Vec::new();

만약 생성과 동시에 초기화를 하고 싶다면, 다음과 같이  vec! 메크로를 사용하도록 하자. 안에 들어가는 요새에 맞게 자동으로 타입을 지정해준다.

let v = vec![1, 2, 3];

벡터 업데이트

만약에 생성한 벡터에 요소를 추가하기 위해서는 반드시 가변(mutable) 설정을 해야한다.

let mut v = Vec::new();

v.push(5);
v.push(6);
v.push(7);
v.push(8);

여기서도 타입 명시를 하지 않았는 데, 집어 넣은 숫자가 모두 i32 타입인 점을 통해서 러스트가 자동으로 v 의 타입을 추론하기 때문이다.

벡터 요소 접근

인덱싱으로 접근하는 방법과 get 메소드를 사용하는 방법이 있다.

let v = vec![1, 2, 3, 4, 5];

let third: &i32 = &v[2];
println!("세번째 요소는 {}이다.",third);

let third: Option<&i32> = v.get(2);
match third {
    Some(third) => println!("세번째 요소는 {}이다.",third),
    None => println!("악! 패닉! 세번째 요소는 없어!"),
}

인덱스를 통해서 접근한다면 요소의 참조자를 얻게 되며, get 함수를 통해서 접근한다면 Option<&T> 즉, Null 체크를 한 결과를 얻게 된다.

인덱스를 통한 접근의 문제는 벡터 범위를 벗어난 인덱스가 주어지면 패닉을 일으킨다. 존재하지 않는 요소를 참조하기 때문이다.

다만 get 함수에서 벡터 범위를 벗어난 인덱스가 주어지면 패닉 없이 None 이 반환되어, 일반적인 상황에서 벡터의 범위 밖에 있는 요소에 접근하는 일이 종종 발생할 수 도 있다면 이 방법을 사용할 만 하다.

벡터 요소 접근 중 가변 문제

불변과 가변 항목에서 설명했다 싶이, 당연하게도 불변 참조자를 얻게 된 상태에서는 가변 참조자를 사용할 수 없다.

아래 코드는 컴파일 에러가 발생한다.

let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // 불변 참조자 획득
v.push(6); // 불변 참조자가 생긴 이후로는 가변 참조자를 통해 변수를 수정할 수 없다.
println!("악! 패닉! 패닉! 소유권 및 대여 규칙 위반!")

 벡터 요소 반복

당연하게도 for 문에서 벡터를 활용할 수 있다. 벡터 내의 각 요소를 차례대로 접근할 때에 인덱스 대신 요소에 대한 반복으로 for 루프를 활용한다.

let v = vec![100, 32, 57];
for i in &v {
    println!("나는 {i}이요!");
}

반복문 안에서 모든 요소를 변경하려면 가변 참조자로 반복 작업을 진행할 수 도 있다.

let mut v = vec![100, 32, 57];
for i in &mut v {
    *i += 50; // 역참조 연산자 '*'
}

역참조 연산자는 대충 참조자가 가지고 있는 값을 가져온다는 뜻으로 생각하면 된다.

에이 C 언어 했으면 알겠지 이정도는

벡터에 열거형 쓰기

벡터는 같은 타입을 저장할 수 있는 데, 열거형에서 정의된 배리언트들은 같은 타입으로 간주된다.

enum Cell {
    Int(i32),
    Float(f64),
    Text(String),
}

let row = vec![
    Cell::Int(3),
    Cell::Text(String::from("blue")),
    Cell::Float(10.12),
];

다양한 타입을 담을 수 있으나, 굳이 이렇게 까지 할 필요가 있나 싶기는 하다.

벡터의 라이프타임

당연히 스코프를 벗어나면 벡터 내에있는 요소들 또한 전부 해제된다.

{
    let v = vec![1, 2, 3, 4];
    // v를 가지고 작업하기
} // <- 여기서 v가 스코프 밖으로 벗어나고 해제된다

2. 문자열(String)

기본적인 러스트의 문자열 인코딩은 UTF-8 이다. 문자열의 뜻은 문자를 요소로 가진 배 이다. 여기서의 String 은 구조체 래퍼(struct wrapper) 로서 대충 JAVA 의 String Class 라고 생각하면 된다.

문자열 생성

let mut s = String::new();

해당 코드와 같이 비어있는 새로운 String 을 생성할 수 있다. 만약 변경 불가능한 &str 타입의 문자열 리터럴 또한 String 으로 변경할 수 있다.

let data = "아따 나는 수정 불가라고";

let s = data.to_string();

// 이 메서드는 리터럴에서도 바로 작동합니다:
let s = "나도 수정 불가하지".to_string();

혹은 다음과 같은 코드를 활용해 바로 문자열 리터럴로 String 을 생성할 수 있다.

let s = String::from("나도 수정은 못하지");

문자열 업데이트

String+ 연산자 format! 매크로를 사용하여 편리하게 값들을 이어붙일 수 있다.

push_str 과 push

let mut s = String::from("후");
s.push_str("하");
// s == 후하
let mut s = String::from("lo");
s.push('l');
// s == lol

+ 연산자 와 format! 매크로

둘 다 정상 작동 한다.

let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = s1 + "-" + &s2 + "-" + &s3;
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");

let s = format!("{s1}-{s2}-{s3}");

문자열 요소 접근

결론부터 말하자면 불가능하다.

정말로 인덱스로 접근하여 아스키 문자를 보고 싶다면 Vec<u8> 에 직접 하나하나 문자를 집어넣도록 하자.
아니면 as_bytes 라는 함수를 통해 문자열을 바이트 배열로 변경할 수 도 있다.
유니코드 문자를 인덱스로 접근하고 싶다면 Vec<char> 라던지 as_chars 라는 함수라던지 여러 대체 방법이 있다.

 

대부분의 많은 프로그래밍 언어에서, 인덱스를 이용한 참조를 통해 문자열 내부의 개별 문자에 접근하는 것은 유효하고 범용적인 연산에 속한다. 그런데 러스트에서는 String 의 부분에 인덱싱을 사용하게 되면 컴파일 에러를 발생시킨다.

 

그 이유는 인덱스 연산이 언제나 상수 시간(O(1))에 실행 될 것으로 기대받기 때문이다. 러스트에서는 String 을 가지고 그러한 성능을 보장하는 것이 불가능한데, 문자열 내에 유효한 문자가 몇 개 있는 지 알아내기 위해 내용물을 시작 지점부터 인덱스로 지정된 곳까지 훑어야 하기 때문이다.

문자열 슬라이싱

특정 바이트들이 담고 있는 문자열 슬라이스는 만들 수 있다. 하지만 범위를 지정하여 문자열 슬라이스를 생성하는 것은 프로그램을 죽게 할 수 도 있으니 주의 깊게 사용해야 한다.

let hello = "안녕하십니까";

let s = &hello[0..4]; // [include..exclude]

여기서 s 는 문자열의 첫 4바이트를 담고있는 &str 가 된다. 한국어는 유니코드로 되어있기 때문에 이 글자들이 각각 2바이트를 차지한다는 것을 알고 있으므로, s 가 '안녕' 이라는 문자열을 가질 것 이다.

 

만약에 &hello[0..1] 과 같이 문자 바이트 일부를 슬라이스하려고 한다면, 러스트는 벡터 내에 유효하지 않은 인덱스에 접근했을 때와 동일한 방식으로 패닉을 발생시킨다.

문자열 요소 반복

대충 두 가지 방법을 많이 사용한다고 생각하면 된다.

for c in "안녕".chars() {
    println!("{c}");
}
// 출력 결과
// 안
// 녕

for b in "안녕".bytes() {
    println!("{b}");
}
// 출력 결과
// 236
// 149
// 136
// 235
// 133
// 149

3. 해시맵(HashMap)

키와 값을 저장하는 딕셔너리(Dictionary) 타입이라고 생각하자. 거기다 해시 함수(hashing function)를 사용하여 매핑한 것을 저장한다.

해시맵 생성

use std::collections::HashMap;
let mut scores:HashMap<String, i32> = HashMap::new();

해시맵 업데이트

해시맵과 소유권

값의 소유권이 해시맵으로 넘어간다.

use std::collections::HashMap;

let field_name = String::from("좋아하는 색상");
let field_value = String::from("파랑");

let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name과 field_value는 이 시점부터 유효하지 않는 다.

값을 덮어쓰기

아래 코드는 {"파랑팀":25} 를 출력할 것이다. 원래 값 10은 덮어써졌다.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("파랑팀"), 10);
scores.insert(String::from("파랑팀"), 25);

println!("{:?}", scores);

키가 없을 때만 키와 값 추가하기

아래 코드는 {"노랑팀": 50, "파랑팀": 10} 를 출력할 것이다.

 

Entryor_insert 메소드는 해당 키가 존재할 경우 Entry 키에 대한 연관된 값을 반환하도록 정의되어 있고, 그렇지 않은 경우 매개변수로 제공된 값을 해당 키에 대한 새 값으로 삽입하고 수정된 Entry에 대한 값을 반환한다.

use std::collections::HashMap;

let mut scores = HashMap::new();
scores.insert(String::from("파랑팀"), 10);

scores.entry(String::from("노랑팀")).or_insert(50);
scores.entry(String::from("파랑팀")).or_insert(50);

println!("{:?}", scores);

예전 값에 기초하여 값을 업데이트 하기

해시맵에 대한 또 다른 일반적인 사용 방식은 키에 대한 값을 찾아서 예전 값에 기초하여 값을 업데이트 하는 것이다.

예를 들어, 아래의 코드는 어떤 텍스트 내에 각 단어가 몇 번이나 나왔는 지를 세는 코드를 보여준다. 단어를 키로 사용하는 해시맵을 이용하여 해당 단어가 몇 번이나 나왔는 지 추적하기 위해 값을 증가 시켜준다. 처음 본 단어라면, 값 0을 삽입할 것이다.

use std::collections::HashMap;

let text = "hello world wonderful world";

let mut map = HashMap::new();

for word in text.split_whitespace() {
    let count = map.entry(word).or_insert(0);
    *count += 1;
}

println!("{:?}", map);

이 코드는 {"world": 2, "hello": 1, "wonderful": 1} 을 출력할 것이다.

해시맵은 반복의 처리가 임의의 순서로 일어나기 때문에 키/값 쌍의 출력 순서가 다를 수 도 있다는 것을 명심하자.

해시맵 요소 접근

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("파랑팀"), 10);
scores.insert(String::from("노랑팀"), 50);

let team_name = String::from("파랑팀");
let score = scores.get(&team_name).copied().unwrap_or(0);

get 함수는 Option<&i32> 를 반환한다. 만일 이 해시맵에 해당 키에 대한 값이 없다면 getNone 을 반환할 것이다. 이 프로그램에서는 copied 를 호출하여 Option<&i32> 가 아닌 Option<i32> 를 얻어온 다음, unwrap_or 를 사용하여 scroes 가 해당 키에 대한 아이템을 가지고 있지 않을 경우 score 에 0 을 설정하도록 처리 했다.

해시맵 요소 반복

벡터에서와 유사한 방식으로 for 루프를 사용해서 해시맵 내의 키/값 쌍에 대한 반복 작업을 수행할 수 있다.

use std::collections::HashMap;

let mut scores = HashMap::new();

scores.insert(String::from("파랑팀"), 10);
scores.insert(String::from("노랑팀"), 50);

for (key, value) in &scores {
    println!("{key}: {value}");
}