Rust 공식 문서를 참고한 글입니다.
러스트의 표준 라이브러리에 정의되어 있는 collection는 하나의 자료형에 여러 개의 값을 저장한다. build-in array나 튜플 타입과는 다르게 collection들이 가리키는 데이터들은 Heap 영역에 저장된다. 이 말은 데이터의 양이 컴파일 시점에 결정되지 않아도 되고, 프로그램 실행 중에 변화할 수 있다는 뜻이다.
1. 벡터 Vector
벡터 자료형은 메모리에서 동일한 자료형 값들을 연속된 공간에 저장하는 컬렉션이다. 이는 Vec<T>로 표기하고 Vec::new를 통해 생성한다. 이때, T에 해당하는 자료형이 무엇인지 알리기 위해서 자료형을 명시해야 한다.
벡터 선언
그러나 대부분 초기값과 함께 Vec<T>를 생성하기 때문에 값의 타입을 유추할 수 있다. 그래서 대부분 타입을 명시할 필요가 없다.
let v: Vec<i32> = Vec::new();
러스트에서는 편의를 위해서 vec! 매크로를 제공한다. 제공된 값들을 새로운 Vec를 생성한다. 아래 예시를 보면, i32 값이 초기값으로 적용되기 때문에 Vec<i32>로 유추할 수 있다.
let v = vec![1, 2, 3];
벡터 수정
벡터를 만들고 나서 내용을 추가하고 싶다면 push 메서드로 추가할 수 있다. 여기에서 추가를 위해서면 항상 변수를 선언할 때 mut과 함께 작성해야 한다.
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
벡터 참조
벡터를 참조하는 방법은 인덱싱과 get 메서드를 사용하는 것 두가지가 있다.
let v = vec![1, 2, 3, 4, 5];
// 인덱싱
let third: &i32 = &v[2];
println!("The third element is {third}");
// get 메서드
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
만약, 인덱스가 벡터 범위를 넘으면 오류가 발생한다. get 메서드를 사용하면 Option<&T>를 반환하면서 인덱스가 벡터 범위에 존재하면 Some, 아니면 None을 반환한다.
지난 번에 언급했듯 가변 참조와 불변 참조는 공존할 수 없다. 때문에 가변 벡터 값을 참조하고 있을 때는 벡터에 값을 추가하는 등의 작업을 할 수 없다. 참조하는 값과 벡터에 수정이 이루어지는 부분이 다르더라도 제한된다. 이는 값을 추가하다가 공간이 모자라면 더 넓은 메모리 공간으로 이동하는데, 이런 경우 참조가 잘못된 메모리를 가리키기 때문이다.
벡터 순회
벡터 값에 차례대로 접근하기 위해서는 반복 처리를 해줘야 한다. 아래 예시처럼 불변 참조자를 통해서 각 벡터 요소에 접근한다.
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
모든 요소를 변경하려면, 가변 벡터의 요소에 대해서 가변 참조자로 작업할 수 있다. mut 키워드를 붙여야 한다.
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
서로 다른 타입 저장하기
벡터는 같은 타입의 값들만 저장할 수 있다. 그러나 다른 타입의 아이템을 저장해야 할 때도 있다. 이때는 열거형을 사용하 ㄹ수 있다.
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
러스트가 컴파일할 때 저장될 타입을 알아야 하는 이유는 각 요소를 저장하기 위해서 얼마나 힙 메모리가 필요한지 알아야 하기 때문이다. 런타임에 벡터에 저장할 타입을 알지 못하면 열거형을 이용해서 서로 다른 타입을 저장할 수 없다.
2. 문자열 String
문자열을 바이트의 컬렉션으로 구현되어 있고 문자를 UTF-8 형식으로 저장한다. 문자열은 러스트의 기능이 아니라 표준 라이브러리로 제공되고 있다. Rust 언어 명세에는 문자열 슬라이스인 str만 정의되어 있는데, 보통 값을 대여한 &str 형태로 사용된다.
문자열 생성
문자열도 벡터처럼 연관함수인 String::new를 사용해 생성할 수 있다. 또 Display 크레이트를 구현한 자료형의 메서드인 to_string을 통해서 초기값이 있는 문자열을 생성할 수 있다.
// 빈 문자열 생성
let mut s = String::new();
// 초기값이 있는 문자열 생성
let data = "initial contents";
let s = data.to_string();
문자열 리터럴에서 String을 생성하기 위해서 String::from을 사용할 수 있는데 to_string()과 같은 기능이다.
문자열 추가
벡터와 마찬가지로 + 연산자나 format! 매크로를 사용해서 String 값을 추가할 수 있다. 또 push나 push_str를 사용할 수도 있다.
// push_str 사용
let mut s = String::from("foo");
s.push_str("bar");
// + 연산자 사용
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
// format! 매크로 사용
let s = format!("{s1}-{s2}-{s3}");
문자열 인덱싱
다른 언어와 달리, Rust에서는 인덱스를 이용해 문자열에 접근하면 오류가 발생한다.
let s1 = String::from("hello");
let h = s1[0];
러스트 문자열은 인덱싱을 지원하지 않는다는 것을 확인할 수 있다.
문자열은 내부적으로 Vec<u8>을 감싼 형태의 자료형이다. u8은 1바이트 크기를 가지고 있고 이것의 조합으로 하나의 문자가 표현된다. 그러나 인덱스로 값을 받아오는 경우 조합된 문자가 아니라 1바이트 값을 가져오는 것이다. 그래서 Rust 문자열은 이런 방식을 허용하지 않는다.
문자열 슬라이싱
문자열 인덱싱 대신에 문자열 슬라이스를 통해서 적절한 바이트를 읽어올 수는 있다.
let hello = "Здравствуйте";
let s = &hello[0..4];
&hello[0..1]처럼 문자 바이트의 일부를 얻으려고 하면, 유효하지 않은 인덱스에 접근한 것과 동일한 방식으로 오류가 발생할 것이다. 그래서 슬라이싱은 사용할 때 주의가 필요하다.
문자열 순회
문자열을 문자 단위로 사용하고 싶다면 순회로 각 문자에 접근하는 방법이 있다. String 자료형의 chars 메소드를 사용하면 개별 문자를 순회할 수 있다.
3. 해시맵 Hash Map
해시 맵을 벡터처럼 인덱스가 아니라 어떤 타입의 키를 이용해서 데이터를 찾는다. HashMap<K, V> 타입으로 K 타입의 키와 V 타입의 값에 대해서 해시 함수에 매핑한다. 이때 해시 함수는 키-값 쌍을 메모리 어디에 저장할지 결정한다.
해시맵 생성
빈 해시맵을 생성하는 방법은 new를 사용해 정의한 후 insert로 요소를 추가하는 것이다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
벡터와 마찬가지로 해시맵도 데이터를 힙에 저장한다. String 타입의 키와 i32 타입의 값의 쌍으로 구성되어 있고, 모든 키와 값들은 서로 같은 타입이어야 한다.
해시맵 값에 접근
get 메서드를 사용해서 해시맵에서 값을 가져올 수 있다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
get 메서드는 해당 키에 대한 값이 존재하는 경우에 Option<&v>를 반환하고 없는 경우 None을 반환한다. 위 예시에서는 Option<&i32> 가 아닌 Option<i32>를 가져와서 해당 키에 대한 값에 대한 처리를 하고 있다.
벡터와 유사한 방식으로 for 루프로 해시맵 내 키-값 쌍에 접근할 수도 있다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{key}: {value}");
}
해시맵과 소유권
해시맵은 Copy와 Move로 값을 가져온다. i32를 비롯한 Copy 크레이트를 구현하는 자료형의 경우엔 Copy 방식이다. String 을 비롯한 그렇지 않은 자료형의 경우 Move로 소유권을 가져온다. 따라서 소유권이 이전되는 자료형은 해시맵에 넣고 나면 원래 변수에 접근할 수 없다.
use std::collections::HashMap;
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value); // field_name과 field_value에 접근 불가
해시맵 수정
키-값 쌍의 개수는 늘어날 수 있어도 하나의 키에 대한 여러 개의 값을 가질 수는 없다. 그래서 해시맵 데이터를 변경하고 싶다면 키에 값이 이미 할당된 경우에 대해 처리해줘야 한다.
덮어쓰기
해시맵에 키-값 쌍을 삽입하고 나서 같은 키에 다른 값을 삽입하면, 값은 새 값으로 대체하는 방식이다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
키가 없을 때만 키-값 쌍 추가하기
해시 맵에 특정 키가 있는지 검사하고 나서 동작하는 방식이다. 키가 해시맵에 존재하면 값을 그대로 두고, 키가 없다면 키와 값을 추가할 수 있다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
이때 entry라는 API를 사용해서 키가 존재하는지 검사한다. or_insert 메서드를 통해 해당 키가 존재하는 경우 Entry 키에 대한 값을 리턴하도록 정의되어 있고, 그렇지 않은 경우 매개변수 값을 새 값으로 삽입하고 리턴한다.
이전 값을 참고하여 업데이트하기
해시맵에서 키에 대한 이전 값을 기반으로 값을 업데이트하는 방식이다. 예를 들어서, 어떤 텍스트에 단어가 몇번 나왔는지 세는 프로그램의 경우, 이전 값에 값을 더해야 한다.
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);
count 변수에 가변 참조자를 저장하고 *를 사용해 count 를 역참조한다.
'언어 > Rust' 카테고리의 다른 글
[Rust] 7. 프로젝트 관리 (1) | 2024.10.09 |
---|---|
[Rust] 6. 열거형과 패턴 매칭 (8) | 2024.10.09 |
[Rust] 5-2. 메서드 (0) | 2024.10.09 |
[Rust] 5-1. 구조체 사용 (0) | 2024.10.09 |
[Rust] 4-3. Slice Type (0) | 2024.09.28 |