오늘은 Rust의 컬렉션 중 벡터에 대해 알아보고자 합니다.
일반적인 컬렉
rust의 표준 라이브러리에는 컬렉션(collection)이라 불리는 매우 유용한 데이터 구조들이 여러 개 포함되어 있습니다.
내장된 배열(build-in array)이나 튜플 타입과는 달리, 이 컬렉션들이 가리키고 있는 데이터들은 힙에 저장되는데, 이는 즉 데이터의 양이 컴파일 타임에 결정되지 않아도 되며 프로그램 실행 중에 늘어나거나 줄어들 수 있다는 의미입니다.
rust 프로그램에서 굉장히 자주 사용되는 세 가지 컬렉션은 다음과 같습니다.
- 벡터(vector)는 여러 개의 값을 붙어 있게 저장할 수 있도록 해줍니다.
- 문자열(String)은 문자(character)의 모음입니다.
- 해시맵(hash map)은 어떤 값을 특정한 키와 연관지어 주도록 해줍니다. 이는 맵(map)이라 일컫는 좀 더 일반적인 데이터 구조의 특정한 구현 형태입니다.
벡터에 여러 값의 목록 저장하기
벡터(Vec) 를 사용하면 메모리에서 모든 값을 서로 이웃하도록 배치하는 단일 데이터 구조에 하나 이상의 값을 저장할 수 있습니다.
벡터는 같은 타입의 값만을 저장할 수 있습니다.
새 벡터 만들기
새 벡터는 Vec::new를 통해 호출가능합니다.
let v: Vec<i32> = Vec::new();
해당 예시에서는 타입 명시(type annotation)가 추가되어 있지만, 벡터는 어떠한 값도 저장하지 않았기 때문에, rust는 저장하고자 하는 요소가 어떤 타입인지 알지 못합니다.
벡터는 제네릭(generic)을 이용하여 구현되었습니다.
특정한 타입의 값을 저장할 벡터를 만들 때는 꺽쇠괄호(<>) 안에 해당 타입을 지정합니다.
대부분의 경우는 초깃값들과 함께 Vec를 생성하고 rust는 저장하고자 하는 값의 타입을 대부분 유추할 수 있으므로, 이런 타입 명시를 할 필요가 없습니다.
rust는 편의를 위해 vec! 매크로를 제공하는데, 이 매크로는 제공된 값들을 저장한 새로운 Vec를 생성합니다.
let v = vec![1, 2, 3];
다음 예제에서는 기본 정수형이 i32이기 때문에, Vec를 생성할 것입니다.
벡터 업데이트하기
벡터를 만들고 여기에 요소를 추가하는 방법은 다음과 같습니다.
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
벡터도 마찬가지로, 어떤 변수의 값으 변경하려면 mut 키워드를 사용하여 해당 변수를 가변으로 만들어야 합니다.
Vec 타입 명시를 붙이지 않아도 되는 이유는, 집어넣은 숫자가 모두 i32 타입인 점을 통하여 러스트가 v의 타입을 추론하기 때문입니다.
벡터 요소 읽기
벡터에 저장된 값을 참조하는 방법은 인덱싱과 get 메서드 두 가지가 있습니다.
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("The third element is {third}");
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 함수에 인덱스를 매개변수로 넘기면, match를 통해 처리할 수 있는 Option<&T>를 얻데 됩니다.
rust가 벡터 요소를 참조하는 방법을 두 가지 제공하는 이유는 벡터에 없는 인덱스 값을 사용하고자 했을 때, 프로그램이 어떻게 동작할 것인지 선택할 수 있도록 하기 위해서입니다.
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
다음과 같은 예시의 경우, 첫 번째의 [] 메서드는 패닉을 일으키는데, 이는 존재하지 않는 요소를 참조하기 때문입니다.
get 함수에 벡터 범위를 벗어난 인덱스가 주어지면 패닉 없이 None이 반환됩니다.
이 방법을 사용하기 위해서는 Some(&elemeent) 혹은 None에 대해 처리하는 로직이 있어야 합니다.
만일 사용자가 잘못하여 너무 큰 숫자를 입력하여 프로그램이 None 값을 받았을 경우, 사용자에게 현재 Vec에 몇 개의 아이템이 있으며 유효한 값을 입력할 기회를 다시 한 번 줄 수도 있습니다.
프로그램에 유효한 참조자가 있다면, 대여 검사기(borrow checker)가 소유권 및 대여 규칙을 집행하여 이 참조자와 벡터의 내용물로부터 얻은 다른 참조자들이 계속 유효하게 남아있도록 보장합니다.
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
다음과 같은 예시에서는 에러가 발생하게 됩니다.
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:6:5
|
4 | let first = &v[0];
| - immutable borrow occurs here
5 |
6 | v.push(6);
| ^^^^^^^^^ mutable borrow occurs here
7 |
8 | println!("The first element is: {first}");
| ----- immutable borrow later used here
For more information about this error, try `rustc --explain E0502`.
error: could not compile `collections` due to previous error
벡터는 모든 요소가 서로 붙어서 메모리에 저장됩니다.
그리고, 새로운 요소를 벡터 끝에 추가할 경우, 현재 벡터 메모리 위치에 새로운 요소를 추가할 공간이 없다면, 다른 넉넉한 곳에 메모리를 새로 할당하고 기존 요소를 새로 할당한 공간에 복사합니다.
그러므로 &v[0]을 한 first는 불변 참조자 였지만, 이후에 push연산을 통해 새로운 메모리에 할당을 받으면서 사용할 수 없게 되어 에러가 발생한 것입니다.
벡터 값에 대해 반복하기
벡터 내의 각 요소를 차례대로 접근하기 위해서는 인덱스를 사용하여 한 번에 하나의 값에 접근하기보다는 모든 요소에 대한 반복 처리를 합니다.
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;
}
가변 참조자가 가리키는 값을 수정하려면, += 연산자를 쓰기 전에 * 역참조 연산자로 i의 값을 얻어와야 합니다.
열거형을 이용해 여러 타입 저장하기
벡터는 같은 타입을 가진 값들만 저장할 수 있지만, 열거형의 배리언트를 정의하여, 벡터 내에 다른 타입의 값들을 저장할 수 있습니다.
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
rust가 컴파일 타임에 벡터 내에 저장될 타입이 무엇인지 알아야 하는 이유는 각 요소를 저장하기 위해 얼마만큼의 힙 메모리가 필요한지 알아야 하기 때문입니다.
만약 rust가 어떠한 타입이든 담을 수 있는 벡터를 허용한다면, 벡터의 각 요소마다 수행되는 연산에 대해 하나 혹은 그 이상의 타입이 에러를 발생시킬 수 있습니다.
런타임에 프로그램이 벡터가 저장할 모든 타입 집합을 알지 못하면 열거형을 이용한 방식을 사용할 수 없지만, 대신에 트레이트 객체(trait object)를 이용할 수 있습니다.
벡터가 버려지면 벡터의 요소도 버려집니다.
struct와 마찬가지로, 벡터는 스코프를 벗어날 때 해제됩니다.
{
let v = vec![1, 2, 3, 4];
}
정리
- 벡터는 하나의 타입의 값들이 줄지어 있는 컬렉션으로 C++의 vector와 유사합니다.
- vec의 값을 가져오는 방식으로는 &으로 가져오는 방식과 Option으로 반환하는 get 방식이 있다.
- vec의 & 연산과 []은 불변 참조자를 가지게 되고, push는 가변 참조자의 형태를 띈다.
- vec의 push에서는 저장하고자 하는 값이 들어갈 공간이 없다면, 새로운 heap 공간에 값을 복사하고, 추가하고자 하는 값을 넣게 된다.
'Rust > 학습' 카테고리의 다른 글
Rust 설치부터 실행까지 (use, pub use, as, mod) - 16 (0) | 2025.01.15 |
---|---|
Rust 설치부터 실행까지 (crate, 절대 경로, 상대 경로, super, use, pub) - 15 (0) | 2025.01.13 |
Rust 설치부터 실행까지 (match, if let) - 14 (0) | 2025.01.12 |
Rust 설치부터 실행까지 (열거형, Option) - 13 (0) | 2025.01.12 |
Rust 설치부터 실행까지 (메서드) - 12 (0) | 2025.01.09 |
댓글