벡터로 값 목록 저장하기
우리가 처음 살펴볼 컬렉션 타입은 벡터라고도 부르는 Vec<T> 입니다. 벡터는 여러 값을
하나의 자료구조 안에 저장하며, 그 값들은 메모리 안에서 서로 붙어 있습니다. 벡터는 같은
타입의 값만 저장할 수 있습니다. 파일의 텍스트 줄들이나 장바구니 안 상품 가격처럼,
항목들의 목록을 가지고 있을 때 유용합니다.
새 벡터 만들기
새로운 빈 벡터를 만들려면 목록 8-1처럼 Vec::new 함수를 호출합니다.
fn main() {
let v: Vec<i32> = Vec::new();
}
i32 타입 값을 담을 새 빈 벡터 만들기여기서는 타입 주석을 추가했다는 점에 주목하세요. 아직 이 벡터에 아무 값도 넣지 않았기
때문에, 러스트는 우리가 어떤 종류의 요소를 저장하려는지 알 수 없습니다. 이것은 중요한
점입니다. 벡터는 제네릭으로 구현되어 있으며, 여러분 자신의 타입에 제네릭을 적용하는
방법은 10장에서 다룹니다. 지금은 표준 라이브러리의 Vec<T> 가 어떤 타입이든 담을 수
있다는 것만 알면 충분합니다. 특정 타입을 저장하는 벡터를 만들 때는 꺾쇠 괄호 안에
그 타입을 명시할 수 있습니다. 목록 8-1에서는 v 안의 Vec<T> 가 i32 타입의
요소를 담을 것이라고 러스트에게 알려 준 것입니다.
하지만 대부분은 초기값을 가진 Vec<T> 를 만들고, 러스트가 저장하려는 값의 타입을
추론하게 할 것입니다. 그래서 이런 타입 주석은 드물게만 필요합니다. 러스트는
vec! 매크로를 제공하는데, 이것은 여러분이 넘긴 값들을 담은 새 벡터를 만들어 줍니다.
목록 8-2는 값 1, 2, 3 을 담은 새 Vec<i32> 를 만듭니다. 이 정수 타입은,
3장의 “데이터 타입” 절에서 다뤘듯이 기본 정수 타입인
i32 가 됩니다.
fn main() {
let v = vec![1, 2, 3];
}
초기값으로 i32 값을 넣었기 때문에, 러스트는 v 의 타입이 Vec<i32> 라고 추론할
수 있고 타입 주석도 필요 없습니다. 다음으로는 벡터를 어떻게 수정하는지 살펴봅시다.
벡터 갱신하기
벡터를 만든 뒤 요소를 추가하려면 목록 8-3처럼 push 메서드를 사용할 수 있습니다.
fn main() {
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
push 메서드로 벡터에 값 추가하기모든 변수와 마찬가지로 값을 바꾸려면, 3장에서 이야기했듯이 mut 키워드로 변수를
가변으로 만들어야 합니다. 벡터 안에 넣는 숫자는 모두 i32 타입이고, 러스트는 이
사실을 데이터로부터 추론하므로 Vec<i32> 주석이 따로 필요 없습니다.
벡터 요소 읽기
벡터 안에 저장된 값을 참조하는 방법은 두 가지가 있습니다. 인덱싱을 사용하거나
get 메서드를 사용하는 것입니다. 다음 예제에서는 각 방법이 반환하는 값의 타입을
이해하기 쉽도록 주석처럼 드러내 두었습니다.
목록 8-4는 인덱스 문법과 get 메서드, 두 방식으로 벡터 값에 접근하는 예를 보여 줍니다.
fn main() {
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 메서드로 벡터 항목 접근하기여기서 몇 가지 세부를 눈여겨봅시다. 인덱스 값 2 로 세 번째 요소를 가져오는데,
이는 벡터 인덱스가 0부터 시작하기 때문입니다. & 와 [] 를 사용하면 해당 인덱스
위치의 요소에 대한 참조를 얻게 됩니다. 반면 get 메서드에 인덱스를 인수로 넘기면,
match 와 함께 사용할 수 있는 Option<&T> 를 얻게 됩니다.
러스트가 벡터 요소를 참조하는 두 가지 방법을 제공하는 이유는, 기존 요소 범위를 벗어난 인덱스를 사용했을 때 프로그램이 어떻게 동작하길 원하는지를 여러분이 선택할 수 있게 하기 위해서입니다. 예를 들어 요소가 다섯 개인 벡터가 있을 때, 인덱스 100의 요소에 각 기법으로 접근해 보면 어떤 일이 벌어지는지 목록 8-5가 보여 줍니다.
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
이 코드를 실행하면 첫 번째 [] 방식은 존재하지 않는 요소를 참조하기 때문에 프로그램을
패닉하게 만듭니다. 이 방식은 벡터 끝을 넘는 요소 접근이 시도되면 프로그램을 즉시
중단시키고 싶을 때 적합합니다.
get 메서드는 인덱스가 벡터 범위를 벗어나면 패닉하지 않고 None 을 반환합니다.
정상적인 상황에서도 가끔 범위를 벗어난 접근이 일어날 수 있다면 이 방식을 쓰는 것이
좋습니다. 그러면 여러분의 코드는 6장에서 다룬 것처럼 Some(&element) 또는 None
둘 다를 처리하는 로직을 갖게 됩니다. 예를 들어 인덱스가 사용자의 입력에서 온 값일
수 있습니다. 사용자가 실수로 너무 큰 숫자를 입력해 None 이 반환되면, 현재 벡터에
항목이 몇 개인지 알려 주고 유효한 값을 다시 입력할 기회를 줄 수 있습니다. 이것이
오타 하나로 프로그램이 크래시하는 것보다 사용자 친화적입니다.
프로그램이 유효한 참조를 가지고 있으면, 대여 검사기는 4장에서 다룬 소유권 및 대여 규칙을 적용하여 그 참조와 벡터 내용에 대한 다른 참조들이 계속 유효하도록 보장합니다. “같은 스코프 안에서는 가변 참조와 불변 참조를 동시에 가질 수 없다”는 규칙을 떠올려 보세요. 이 규칙은 목록 8-6에 그대로 적용됩니다. 여기서는 벡터 첫 번째 요소에 대한 불변 참조를 잡아 둔 상태에서 벡터 끝에 새 요소를 추가하려 합니다. 이 프로그램은 나중에 그 요소를 다시 참조하려 한다면 동작하지 않습니다.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
}
이 코드를 컴파일하면 다음과 같은 오류가 납니다.
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
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` (bin "collections") due to 1 previous error
목록 8-6의 코드는 얼핏 보기에는 동작해야 할 것처럼 보일 수 있습니다. 벡터 맨 끝에 변화가 생기는 것이 왜 첫 번째 요소에 대한 참조와 상관있을까요? 이 오류는 벡터가 동작하는 방식에서 비롯됩니다. 벡터는 값을 메모리 안에 서로 붙여 두기 때문에, 벡터 끝에 새 요소를 추가하는 일이 현재 저장된 위치에 모든 요소를 계속 붙여 놓을 공간이 없다면, 새 메모리를 할당하고 기존 요소를 통째로 복사하는 작업을 필요로 할 수도 있습니다. 그렇게 되면 첫 번째 요소를 가리키던 참조는 이미 해제된 메모리를 가리키게 됩니다. 대여 규칙은 프로그램이 그런 상황에 빠지는 것을 막아 줍니다.
Note: Vec<T> 타입의 구현 세부에 대해 더 알고 싶다면 [“The Rustonomicon”]
nomicon을 참고하세요.
벡터 값 순회하기
벡터 안의 각 요소에 차례로 접근하려면, 하나씩 인덱스로 가져오기보다 모든 요소를
반복(iterate)하면 됩니다. 목록 8-7은 for 루프를 사용해 i32 값을 담은 벡터의
각 요소에 대한 불변 참조를 얻고 출력하는 방법을 보여 줍니다.
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
}
for 루프로 벡터 요소를 순회하며 각각 출력하기가변 벡터의 각 요소에 대한 가변 참조를 순회하면서, 모든 요소 값을 바꿀 수도 있습니다.
목록 8-8의 for 루프는 각 요소에 50 을 더합니다.
fn main() {
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
}
가변 참조가 가리키는 값을 바꾸려면, i 안의 실제 값에 도달하기 위해 먼저 *
역참조 연산자를 사용해야 하고, 그 뒤에 += 연산자를 쓸 수 있습니다. 역참조
연산자에 대해서는 15장의 “역참조 연산자로 값 따라가기”
절에서 더 자세히 다룹니다.
불변이든 가변이든 벡터를 순회하는 일은 대여 검사기 규칙 덕분에 안전합니다. 만약
목록 8-7이나 8-8의 for 루프 본문 안에서 항목을 삽입하거나 삭제하려 했다면,
목록 8-6에서 본 것과 비슷한 컴파일 오류를 얻게 됩니다. for 루프가 가지고 있는
벡터에 대한 참조가, 벡터 전체를 동시에 수정하는 것을 막기 때문입니다.
enum으로 여러 타입 저장하기
벡터는 같은 타입의 값만 저장할 수 있습니다. 이는 때때로 불편할 수 있습니다. 실제로는 서로 다른 타입의 항목 목록을 저장해야 하는 경우도 분명 있기 때문입니다. 다행히 enum의 variant들은 모두 같은 enum 타입 아래에 정의되므로, 서로 다른 타입의 요소를 표현할 하나의 타입이 필요할 때 enum을 정의해 사용할 수 있습니다!
예를 들어 어떤 스프레드시트의 한 행에서 값을 가져오고 싶은데, 그 행의 어떤 열은 정수, 어떤 열은 부동소수점 수, 어떤 열은 문자열을 담고 있다고 해 봅시다. 이때 다양한 값 타입을 담을 variant를 가지는 enum을 정의할 수 있고, 모든 variant는 동일한 타입, 즉 그 enum 타입으로 취급됩니다. 그러면 그 enum을 담는 벡터를 만들 수 있고, 결과적으로 서로 다른 타입을 하나의 벡터 안에 담을 수 있습니다. 목록 8-9가 이를 보여 줍니다.
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
러스트는 각 요소를 저장하는 데 힙 메모리가 정확히 얼마나 필요한지 알아야 하므로,
벡터 안에 어떤 타입들이 들어갈지 컴파일 시점에 알고 있어야 합니다. 또한 이 벡터에
어떤 타입이 허용되는지도 명시적이어야 합니다. 만약 러스트가 벡터가 아무 타입이나
담을 수 있게 허용했다면, 벡터 요소에 수행하는 연산과 맞지 않는 타입이 들어가 오류를
일으킬 가능성이 생깁니다. enum과 match 식을 함께 사용하면, 6장에서 보았듯이
러스트는 가능한 모든 경우가 컴파일 시점에 처리되었는지를 확인해 줍니다.
런타임에 벡터에 저장될 타입의 전체 집합을 미리 알 수 없다면, enum 기법은 통하지 않습니다. 그런 경우에는 18장에서 다룰 trait object를 사용할 수 있습니다.
지금까지 벡터를 사용하는 가장 흔한 방법들을 살펴보았으니, 표준 라이브러리가 Vec<T>
에 정의해 둔 유용한 메서드들을 API 문서에서 꼭
확인해 보세요. 예를 들어 push 외에도 마지막 요소를 제거해 반환하는 pop 메서드가
있습니다.
벡터가 drop 되면 요소도 함께 drop 된다
다른 struct 와 마찬가지로, 벡터도 스코프를 벗어나면 해제됩니다. 목록 8-10은 그
지점을 주석으로 보여 줍니다.
fn main() {
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
}
벡터가 drop 되면 그 안의 모든 내용도 함께 drop 됩니다. 즉 벡터가 들고 있던 정수들도 정리됩니다. 대여 검사기는 벡터 내용에 대한 참조가 벡터 자체가 유효한 동안에만 사용되도록 보장합니다.
이제 다음 컬렉션 타입인 String 으로 넘어가 봅시다!