벡터에 여러 값의 목록 저장하기

첫 번째로 살펴볼 컬렉션 타입은 벡터라고도 하는 Vec<T>입니다. 벡터를 사용하면 메모리에서 모든 값을 서로 이웃하도록 배치하는 단일 데이터 구조에 하나 이상의 값을 저장할 수 있습니다. 벡터는 같은 타입의 값만을 저장할 수 있습니다. 벡터는 파일 내의 텍스트 라인들이나 장바구니의 품목 가격 같은 아이템 목록을 저장하는 상황일 때 유용합니다.

새 벡터 만들기

비어있는 새 벡터를 만들려면 다음 예제 8-1과 같이 Vec::new 함수를 호출합니다:

fn main() {
    let v: Vec<i32> = Vec::new();
}

예제 8-1: i32 타입의 값을 가질 수 있는 비어있는 새 벡터 생성

위에서 타입 명시 (type annotation) 가 추가된 것에 주목하세요. 이 벡터에 어떠한 값도 집어넣지 않았기 때문에, 러스트는 저장하고자 하는 요소가 어떤 타입인지 알지 못합니다. 이는 중요한 지점입니다. 벡터는 제네릭 (generic) 을 이용하여 구현됐습니다; 제네릭을 이용하여 여러분만의 타입을 만드는 방법은 10장에서 다룰 것입니다. 지금 당장은 표준 라이브러리가 제공하는 Vec 타입은 어떠한 타입의 값이라도 저장할 수 있다는 것만 기억해 둡시다. 특정한 타입의 값을 저장할 벡터를 만들 때는 꺾쇠괄호(<>) 안에 해당 타입을 지정합니다. 예제 8-1에서는 러스트에게 vVeci32 타입의 요소를 갖는다고 알려주었습니다.

대부분의 경우는 초깃값들과 함께 Vec<T>를 생성하고 러스트는 저장하고자 하는 값의 타입을 대부분 유추할 수 있으므로, 이런 타입 명시를 할 필요가 거의 없습니다. 러스트는 편의를 위해 vec! 매크로를 제공하는데, 이 매크로는 제공된 값들을 저장한 새로운 Vec을 생성합니다. 예제 8-2는 1, 2, 3을 저장한 새로운 Vec<i32>을 생성할 것입니다. 3장의 ‘데이터 타입’절에서 본 것처럼, 기본 정수형이 i32기 때문에 여기서도 타입은 i32입니다.

fn main() {
    let v = vec![1, 2, 3];
}

예제 8-2: 값을 저장하고 있는 새로운 벡터 생성하기

러스트는 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);
}

예제 8-3: push 메서드를 사용하여 벡터에 값을 추가하기

3장에서 설명한 것처럼, 어떤 변수의 값을 변경하려면 mut 키워드를 사용하여 해당 변수를 가변으로 만들어야 합니다. 또한 Vec<i32> 타입 명시를 붙이지 않아도 되는 이유는, 집어넣은 숫자가 모두 i32 타입인 점을 통하여 러스트가 v의 타입을 추론하기 때문입니다.

벡터 요소 읽기

벡터에 저장된 값을 참조하는 방법은 인덱싱과 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."),
    }
}

예제 8-4: 인덱스 문법 혹은 get 메서드를 사용하여 벡터 내의 아이템에 접근하기

여기서 주의할 세부 사항이 몇 가지 있습니다. 벡터의 인덱스는 0부터 시작하므로, 세 번째 값을 얻어오기 위해서는 인덱스 값 2를 사용합니다. &[]를 사용하면 인덱스 값에 위치한 요소의 참조자를 얻게 됩니다. get 함수에 인덱스를 매개변수로 넘기면, match를 통해 처리할 수 있는 Option<&T>를 얻게 됩니다.

러스트가 벡터 요소를 참조하는 방법을 두 가지 제공하는 이유는 벡터에 없는 인덱스 값을 사용하고자 했을 때 프로그램이 어떻게 동작할 것인지 선택할 수 있도록 하기 위해서입니다. 예를 들어, 아래의 예제 8-5와 같이 5개의 요소를 가지고 있는 벡터가 있고 100 인덱스에 있는 요소에 접근을 시도하는 경우 어떤 일이 생기는지 확인해 봅시다:

fn main() {
    let v = vec![1, 2, 3, 4, 5];

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);
}

예제 8-5: 5개의 요소를 가진 벡터에 100 인덱스에 있는 요소에 접근하기

이 프로그램을 실행하면, 첫 번째의 [] 메서드는 패닉을 일으키는데, 이는 존재하지 않는 요소를 참조하기 때문입니다. 이 방법은 프로그램이 벡터의 끝을 넘어서는 요소에 접근하는 시도를 하면 프로그램이 죽게 만들고 싶은 경우 가장 좋습니다.

get 함수에 벡터 범위를 벗어난 인덱스가 주어지면 패닉 없이 None이 반환됩니다. 일반적인 상황에서 벡터의 범위 밖에 있는 요소에 접근하는 일이 종종 발생할 수도 있다면 이 방법을 사용할 만합니다. 이 방법을 사용한다면 6장에서 본 것처럼 Some(&element) 혹은 None에 대해 처리하는 로직이 있어야 합니다. 예를 들어 인덱스는 사람이 직접 번호를 입력하는 것으로 들어올 수도 있습니다. 만일 사용자가 잘못하여 너무 큰 숫자를 입력하여 프로그램이 None 값을 받았을 경우라면 사용자에게 현재 Vec에 몇 개의 아이템이 있으며 유효한 값을 입력할 기회를 다시 한 번 줄 수도 있습니다. 이렇게 하는 편이 오타 때문에 프로그램이 죽는 것보다는 더 사용자 친화적이겠죠?

프로그램에 유효한 참조자가 있다면, 대여 검사기 (borrow checker) 가 (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}");
}

예제 8-6: 아이템의 참조자를 가지고 있는 상태에서 벡터에 새로운 요소 추가 시도하기

이 예제를 컴파일하면 아래와 같은 에러가 발생합니다:

$ 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` due to previous error

예제 8-6의 코드는 동작해야 할 것처럼 보일 수도 있겠습니다: 첫 번째 요소의 참조자가 벡터 끝부분의 변경이랑 무슨 상관일까요? 이를 이해하기 위해, 잠시 벡터의 동작 방법을 알아보도록 하겠습니다. 벡터는 모든 요소가 서로 붙어서 메모리에 저장됩니다. 그리고 새로운 요소를 벡터 끝에 추가할 경우, 현재 벡터 메모리 위치에 새로운 요소를 추가할 공간이 없다면, 다른 넉넉한 곳에 메모리를 새로 할당하고 기존 요소를 새로 할당한 공간에 복사합니다. 이 경우, 기존 요소의 참조자는 해제된 메모리를 가리키게 되기 때문에, 이러한 상황을 대여 규칙으로 막아둔 것이죠.

Note: Vec<T> 타입의 구현 세부 사항에 대한 그 밖의 것에 대해서는 ‘러스토노미콘 (The Rustonomicon)’을 보세요:

벡터 값에 대해 반복하기

벡터 내의 각 요소를 차례대로 접근하기 위해서는 인덱스를 사용하여 한 번에 하나의 값에 접근하기보다는 모든 요소에 대한 반복 처리를 합니다. 예제 8-7은 for 루프를 사용하여 i32의 벡터에 있는 각 요소에 대한 불변 참조자를 얻어서 이를 출력하는 방법을 보여줍니다:

fn main() {
    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
}

예제 8-7: for 루프로 벡터의 요소들에 대해 반복하여 각 요소를 출력하기

모든 요소를 변경하기 위해서는 가변 벡터의 각 요소에 대한 가변 참조자로 반복 작업을 할 수도 있습니다. 예제 8-8의 for 루프는 각 요소에 50을 더할 것입니다:

fn main() {
    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }
}

예제 8-8: 벡터의 요소에 대한 가변 참조자로 반복하기

가변 참조자가 가리키는 값을 수정하려면, += 연산자를 쓰기 전에 * 역참조 연산자로 i의 값을 얻어야 합니다. 역참조 연산자는 15장 ‘포인터를 따라가서 값 얻기’에서 자세히 알아볼 예정입니다.

벡터에 대한 반복 처리는 불변이든 가변이든 상관없이 대여 검사 규칙에 의해 안전합니다. 만일 예제 8-7과 예제 8-8의 for 루프 본문에서 아이템을 추가하거나 지우는 시도를 했다면 예제 8-6의 코드에서 본 것과 유사한 컴파일 에러가 발생하게 됩니다. for 루프가 가지고 있는 벡터에 대한 참조자는 전체 벡터에의 동시다발적 수정을 막습니다.

열거형을 이용해 여러 타입 저장하기

벡터는 같은 타입을 가진 값들만 저장할 수 있습니다. 이는 불편할 수 있습니다; 다른 타입의 아이템들에 대한 리스트를 저장해야 하는 상황도 분명히 있으니까요. 다행히도, 열거형의 배리언트는 같은 열거형 타입 내에 정의가 되므로, 벡터 내에 다른 타입의 값들을 저장할 필요가 있다면 열거형을 정의하여 사용할 수 있습니다!

예를 들어, 스프레드시트의 행으로부터 값들을 가져오고 싶은데, 여기서 어떤 열은 정수를, 어떤 열은 실수를, 어떤 열은 문자열을 갖고 있다고 해봅시다. 다양한 타입의 값을 갖는 배리언트를 보유한 열거형을 정의할 수 있고, 모든 열거형 배리언트들은 해당 열거형 타입과 같은 타입으로 간주됩니다. 그러면 해당 열거형을 담을 벡터를 생성하여 궁극적으로 다양한 타입을 담을 수 있습니다. 예제 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),
    ];
}

예제 8-9: 열거형을 정의하여 벡터 내에 다른 타입의 데이터를 담을 수 있도록 하기

러스트가 컴파일 타임에 벡터 내에 저장될 타입이 무엇인지 알아야 하는 이유는 각 요소를 저장하기 위해 얼마만큼의 힙 메모리가 필요한지 알아야 하기 때문입니다. 또한 이 벡터가 담을 수 있는 타입을 명시적으로 보여줘야 합니다. 만일 러스트가 어떠한 타입이든 담을 수 있는 벡터를 허용한다면, 벡터의 각 요소마다 수행되는 연산에 대해 하나 혹은 그 이상의 타입이 에러를 발생시킬 수도 있습니다. 열거형과 match 표현식을 사용한다는 것은 6장에서 설명한 것처럼 러스트가 컴파일 타임에 가능한 모든 경우를 처리함을 보장해 준다는 뜻입니다.

런타임에 프로그램이 벡터에 저장할 모든 타입 집합을 알지 못하면 열거형을 이용한 방식은 사용할 수 없을 것입니다. 대신 트레이트 객체 (trait object) 를 이용할 수 있는데, 이건 17장에서 다룰 예정입니다.

지금까지 벡터를 이용하는 가장 일반적인 방식 몇 가지를 논의했는데, 표준 라이브러리의 Vec에 정의된 유용한 메서드들이 많이 있으니 API 문서를 꼭 살펴봐 주시기 바랍니다. 예를 들면, push에 더해서, pop 메서드는 제일 마지막 요소를 반환하고 지워줍니다.

벡터가 버려지면 벡터의 요소도 버려집니다

struct와 마찬가지로, 예제 8-10에 주석으로 표시된 것처럼 벡터는 스코프를 벗어날 때 해제됩니다.

fn main() {
    {
        let v = vec![1, 2, 3, 4];

        // v를 가지고 작업하기
    } // <- 여기서 v가 스코프 밖으로 벗어나고 해제됩니다
}

예제 8-10: 벡터와 요소들이 버려지는 위치를 표시

벡터가 버려질 때 벡터의 내용물도 전부 버려집니다. 즉, 벡터가 가지고 있던 정수들의 메모리도 정리됩니다. 대여 검사기는 벡터의 내용물에 대한 참조자의 사용이 해당 벡터가 유효할 때만 발생했는지 확인합니다.

이제 다음 컬렉션 타입인 String으로 넘어갑시다!