벡터

우리가 보게될 첫번째 콜렉션은 벡터라고도 알려진 Vec<T>입니다. 벡터는 메모리 상에 서로 이웃하도록 모든 값을 집어넣는 단일 데이터 구조 안에 하나 이상의 값을 저장하도록 해줍니다. 벡터는 같은 타입의 값만을 저장할 수 있습니다. 이는 여러분이 파일 내의 텍스트의 라인들이라던가 장바구니의 아이템 가격들 같은 아이템 리스트를 저장하는 상황일 경우 유용합니다.

새 벡터 만들기

비어있는 새 벡터를 만들기 위해서는, 아래의 Listing 8-1과 같이 Vec::new 함수를 호출해 줍니다:


# #![allow(unused_variables)]
#fn main() {
let v: Vec<i32> = Vec::new();
#}

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

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

일단 우리가 값을 집어넣으면 러스트는 우리가 저장하고자 하는 값의 타입을 대부분 유추할 수 있으므로, 좀 더 현실적인 코드에서는 이러한 타입 명시를 할 필요가 거의 없습니다. 초기값들을 갖고 있는 Vec<T>을 생성하는 것이 더 일반적이며, 러스트는 편의를 위해 vec! 매크로를 제공합니다. 이 매크로는 우리가 준 값들을 저장하고 있는 새로운 Vec을 생성합니다. Listing 8-2는 1, 2, 3을 저장하고 있는 새로운 Vec<i32>을 생성할 것입니다:


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

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

초기 i32 값들을 제공했기 때문에, 러스트는 v가 `Vec 타입이라는 것을 유추할 수 있으며, 그래서 타입 명시는 필요치 않습니다. 다음은, 벡터를 어떻게 수정하는지를 살펴보겠습니다.

벡터 갱신하기

벡터를 만들고 여기에 요소들을 추가하기 위해서는 아래 Listing 8-3과 같이 push 메소드를 사용할 수 있습니다:


# #![allow(unused_variables)]
#fn main() {
let mut v = Vec::new();

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

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

3장에서 설명한 바와 같이, 어떤 변수에 대해 그 변수가 담고 있는 값이 변경될 수 있도록 하려면, mut 키워드를 사용하여 해당 변수를 가변으로 만들어 줄 필요가 있습니다. 우리가 집어넣는 숫자는 모두 i32 타입이며, 러스트는 데이터로부터 이 타입을 추론하므로, 우리는 Vec<i32> 명시를 붙일 필요가 없습니다.

벡터를 드롭하는 것은 벡터의 요소들을 드롭시킵니다

struct와 마찬가지로, Listing 8-4에 달려있는 주석처럼 벡터도 스코프 밖으로 벗어났을 때 해제됩니다:


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

    // v를 가지고 뭔가 합니다

} // <- v가 스코프 밖으로 벗어났고, 여기서 해제됩니다
#}

Listing 8-4: 벡터와 벡터의 요소들이 드롭되는 곳을 보여주기

벡터가 드롭될 때 벡터의 내용물 또한 전부 드롭되는데, 이는 벡터가 가지고 있는 정수들이 모두 제거된다는 의미입니다. 이는 직관적인 것처럼 보일 수도 있겠지만 벡터의 요소들에 대한 참조자를 만들때는 좀 더 복잡해 질 수 있습니다. 다음으로 이런 상황을 파해쳐 봅시다!

벡터의 요소들 읽기

지금까지 벡터를 만들고, 갱신하고, 없애는 방법에 대해 알아보았으니, 벡터의 내용물을 읽어들이는 방법을 알아보는 것이 다음 단계로 좋아보입니다. 벡터 내에 저장된 값을 참조하는 두 가지 방법이 있습니다. 예제에서는 특별히 더 명료하게 하기 위해 함수들이 반환하는 값의 타입을 명시했습니다.

Listing 8-5는 인덱스 문법이나 get 메소드를 가지고 벡터의 값에 접근하는 두 방법 모두를 보여주고 있습니다:


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

let third: &i32 = &v[2];
let third: Option<&i32> = v.get(2);
#}

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

두가지 세부사항을 주목하세요. 첫번째로, 인덱스값 2를 사용하면 세번째 값이 얻어집니다: 벡터는 0부터 시작하는 숫자로 인덱스됩니다. 두번째로, 세번째 요소를 얻기 위해 두 가지 다른 방법이 사용되었습니다: &[]를 이용하여 참조자를 얻은 것과, get 함수에 인덱스를 파라미터로 넘겨서 Option<&T>를 얻은 것입니다.

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


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

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

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

이 프로그램을 실행하면, 첫번째의 [] 메소드는 panic!을 일으키는데, 이는 존재하지 않는 요소를 참조하기 때문입니다. 이 방법은 여러분의 프로그램이 벡터의 끝을 넘어서는 요소에 접근하는 시도를 하면 프로그램이 죽게끔 하는 치명적 에러를 발생하도록 하기를 고려하는 경우 가장 좋습니다.

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

유효하지 않은 참조자

프로그램이 유효한 참조자를 얻을 때, 빌림 검사기(borrow checker)가 (4장에서 다루었던) 소유권 및 빌림 규칙을 집행하여 이 참조자와 벡터의 내용물로부터 얻은 다른 참조자들이 계속 유효하게 남아있도록 확실히 해줍니다. 같은 스코프 내에서 가변 참조자와 불변 참조자를 가질 수 없다는 규칙을 상기하세요. 이 규칙은 아래 예제에서도 적용되는데, Listing 8-7에서는 벡터의 첫번째 요소에 대한 불변 참조자를 얻은 뒤 벡터의 끝에 요소를 추가하고자 했습니다:

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

let first = &v[0];

v.push(6);

Listing 8-7: 아이템에 대한 참조자를 가지는 동안 벡터에 요소 추가 시도하기

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

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as
immutable
  |
4 | let first = &v[0];
  |              - immutable borrow occurs here
5 |
6 | v.push(6);
  | ^ mutable borrow occurs here
7 | }
  | - immutable borrow ends here

Listing 8-7의 코드는 동작을 해야만 할것처럼 보일 수도 있습니다: 왜 첫번째 요소에 대한 참조자가 벡터 끝에 대한 변경을 걱정해야 하죠? 이 에러에 대한 내막은 벡터가 동작하는 방법 때문입니다: 새로운 요소를 벡터의 끝에 추가하는 것은 새로 메모리를 할당하여 예전 요소를 새 공간에 복사하는 일을 필요로 할 수 있는데, 이는 벡터가 모든 요소들을 붙여서 저장할 공간이 충분치 않는 환경에서 일어날 수 있습니다. 이러한 경우, 첫번째 요소에 대한 참조자는 할당이 해제된 메모리를 가리키게 될 것입니다. 빌림 규칙은 프로그램이 이러한 상황에 빠지지 않도록 해줍니다.

노트: Vec<T> 타입의 구현 세부사항에 대한 그밖의 것에 대해서는 https://doc.rust-lang.org/stable/nomicon/vec.html 에 있는 노미콘(The Nomicon)을 보세요:

벡터 내의 값들에 대한 반복처리

만일 벡터 내의 각 요소들을 차례대로 접근하고 싶다면, 하나의 값에 접근하기 위해 인덱스를 사용하는것 보다는, 모든 요소들에 대해 반복처리를 할 수 있습니다. Listing 8-8은 for 루프를 사용하여 i32의 벡터 내에 있는 각 요소들에 대한 불변 참조자를 얻어서 이를 출력하는 방법을 보여줍니다:


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

Listing 8-8: for 루프를 이용한 요소들에 대한 반복작업을 통해 각 요소들을 출력하기

만일 모든 요소들을 변형시키길 원한다면 가변 벡터 내의 각 요소에 대한 가변 참조자로 반복작업을 할 수도 있습니다. Listing 8-9의 for 루프는 각 요소에 50을 더할 것입니다:


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

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

가변 참조자가 참고하고 있는 값을 바꾸기 위해서, i+= 연산자를 이용하기 전에 역참조 연산자 (*)를 사용하여 값을 얻어야 합니다.

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

이 장의 시작 부분에서, 벡터는 같은 타입을 가진 값들만 저장할 수 있다고 이야기했습니다. 이는 불편할 수 있습니다; 다른 타입의 값들에 대한 리스트를 저장할 필요가 있는 상황이 분명히 있지요. 다행히도, 열거형의 variant는 같은 열거형 타입 내에 정의가 되므로, 백터 내에 다른 타입의 값들을 저장할 필요가 있다면 열거형을 정의하여 사용할 수 있습니다!

예를 들어, 스프레드시트의 행으로부터 값들을 가져오고 싶은데, 여기서 어떤 열은 정수를, 어떤 열은 실수를, 어떤 열은 스트링을 갖고 있다고 해봅시다. 우리는 다른 타입의 값을 가지는 variant가 포함된 열거형을 정의할 수 있고, 모든 열거형 variant들은 해당 열거형 타입, 즉 같은 타입으로 취급될 것입니다. 따라서 우리는 궁극적으로 다른 타입을 담은 열거형 값에 대한 벡터를 생성할 수 있습니다. Listing 8-10에서 이를 보여주고 있습니다:


# #![allow(unused_variables)]
#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),
];
#}

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

러스트가 컴파일 타임에 벡터 내에 저장될 타입이 어떤 것인지 알아야할 필요가 있는 이유는 각 요소를 저장하기 위해 얼만큼의 힙 메모리가 필요한지 알기 위함입니다. 부차적인 이점은 이 백터에 허용되는 타입에 대해 명시적일 수 있다는 점입니다. 만일 러스트가 어떠한 타입이든 담을수 있는 벡터를 허용한다면, 벡터 내의 각 요소마다 수행되는 연산에 대해 하나 혹은 그 이상의 타입이 에러를 야기할 수도 있습니다. 열거형과 match 표현식을 사용한다는 것은 6장에서 설명한 바와 같이 러스트가 컴파일 타임에 모든 가능한 경우에 대해 처리한다는 것을 보장해준다는 의미입니다.

만약 프로그램을 작성할 때 여러분의 프로그램이 런타임에 벡터에 저장하게 될 타입의 모든 경우를 알지 못한다면, 열거형을 이용한 방식은 사용할 수 없을 것입니다. 대신 트레잇 객체(trait object)를 이용할 수 있는데, 이건 17장에서 다루게 될 것입니다.

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