소개
벡터를 사용하여 값 목록 저장하기에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
"이 랩에서는 Vec<T> 컬렉션 타입, 즉 벡터 (vector) 라고도 하는 것을 탐구할 것입니다. 이는 동일한 타입의 값 목록을 단일 데이터 구조에 저장할 수 있도록 해줍니다."
벡터를 사용하여 값 목록 저장하기에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
"이 랩에서는 Vec<T> 컬렉션 타입, 즉 벡터 (vector) 라고도 하는 것을 탐구할 것입니다. 이는 동일한 타입의 값 목록을 단일 데이터 구조에 저장할 수 있도록 해줍니다."
가장 먼저 살펴볼 컬렉션 타입은 Vec<T>이며, 벡터 (vector) 라고도 합니다. 벡터를 사용하면 메모리에서 모든 값을 서로 인접하게 배치하는 단일 데이터 구조에 둘 이상의 값을 저장할 수 있습니다. 벡터는 동일한 타입의 값만 저장할 수 있습니다. 파일의 텍스트 줄이나 장바구니의 품목 가격과 같이 항목 목록이 있는 경우 유용합니다.
새로운 빈 벡터를 생성하려면 Listing 8-1 에 표시된 대로 Vec::new 함수를 호출합니다.
let v: Vec<i32> = Vec::new();
Listing 8-1: i32 타입의 값을 저장할 새로운 빈 벡터 생성
여기서 타입 어노테이션 (type annotation) 을 추가했습니다. 이 벡터에 값을 삽입하지 않기 때문에 Rust 는 어떤 종류의 요소를 저장하려는지 알 수 없습니다. 이는 중요한 점입니다. 벡터는 제네릭 (generics) 을 사용하여 구현됩니다. 제네릭을 자체 타입과 함께 사용하는 방법은 10 장에서 다룰 것입니다. 현재로서는 표준 라이브러리에서 제공하는 Vec<T> 타입이 모든 타입을 저장할 수 있다는 것을 알아두세요. 특정 타입을 저장하는 벡터를 생성할 때, 꺾쇠 괄호 안에 타입을 지정할 수 있습니다. Listing 8-1 에서 v의 Vec<T>가 i32 타입의 요소를 저장하도록 Rust 에 지시했습니다.
더 자주, 초기 값을 사용하여 Vec<T>를 생성할 것이고 Rust 는 저장하려는 값의 타입을 추론하므로, 이 타입 어노테이션을 사용할 필요가 거의 없습니다. Rust 는 편리하게 vec! 매크로를 제공하며, 이 매크로는 제공한 값을 저장하는 새로운 벡터를 생성합니다. Listing 8-2 는 값 1, 2, 3을 저장하는 새로운 Vec<i32>를 생성합니다. 정수 타입은 "데이터 타입"에서 논의했듯이 기본 정수 타입이므로 i32입니다.
let v = vec![1, 2, 3];
Listing 8-2: 값을 포함하는 새로운 벡터 생성
초기 i32 값을 제공했으므로 Rust 는 v의 타입이 Vec<i32>임을 추론할 수 있으며, 타입 어노테이션은 필요하지 않습니다. 다음으로, 벡터를 수정하는 방법을 살펴보겠습니다.
벡터를 생성한 다음 요소를 추가하려면 Listing 8-3 에 표시된 대로 push 메서드를 사용할 수 있습니다.
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Listing 8-3: push 메서드를 사용하여 벡터에 값 추가
다른 변수와 마찬가지로, 값을 변경할 수 있도록 하려면 3 장에서 설명한 대로 mut 키워드를 사용하여 가변 (mutable) 으로 만들어야 합니다. 안에 넣는 숫자는 모두 i32 타입이며, Rust 는 데이터에서 이를 추론하므로 Vec<i32> 어노테이션은 필요하지 않습니다.
벡터에 저장된 값을 참조하는 방법에는 두 가지가 있습니다: 인덱싱 (indexing) 을 사용하거나 get 메서드를 사용하는 것입니다. 다음 예제에서는 추가적인 명확성을 위해 이러한 함수에서 반환되는 값의 타입을 주석 처리했습니다.
Listing 8-4 는 인덱싱 구문과 get 메서드를 사용하여 벡터의 값에 접근하는 두 가지 방법을 모두 보여줍니다.
let v = vec![1, 2, 3, 4, 5];
1 let third: &i32 = &v[2];
println!("The third element is {third}");
2 let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element."),
}
Listing 8-4: 인덱싱 구문과 get 메서드를 사용하여 벡터의 항목에 접근
여기서 몇 가지 세부 사항에 유의하세요. 벡터는 0 부터 시작하는 숫자로 인덱싱되므로 세 번째 요소를 얻기 위해 인덱스 값 2를 사용합니다 [1]. &와 []를 사용하면 인덱스 값의 요소에 대한 참조를 얻습니다. get 메서드를 인수로 전달된 인덱스와 함께 사용하면 [2], match와 함께 사용할 수 있는 Option<&T>를 얻습니다.
Rust 는 기존 요소 범위를 벗어난 인덱스 값을 사용하려는 경우 프로그램이 어떻게 동작할지 선택할 수 있도록 이러한 두 가지 방법으로 요소에 접근할 수 있도록 합니다. 예를 들어, 다섯 개의 요소가 있는 벡터가 있고 각 기술을 사용하여 인덱스 100 의 요소에 접근하려는 경우 어떻게 되는지 Listing 8-5 에서 살펴보겠습니다.
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
Listing 8-5: 다섯 개의 요소를 포함하는 벡터에서 인덱스 100 의 요소에 접근 시도
이 코드를 실행하면 첫 번째 [] 메서드는 존재하지 않는 요소를 참조하므로 프로그램이 패닉 (panic) 상태가 됩니다. 이 메서드는 벡터의 끝을 넘어 요소에 접근하려는 시도가 있을 경우 프로그램이 충돌하도록 하려는 경우에 가장 적합합니다.
get 메서드가 벡터 범위를 벗어난 인덱스를 전달받으면 패닉하지 않고 None을 반환합니다. 벡터 범위를 벗어난 요소에 접근하는 것이 정상적인 상황에서 가끔 발생할 수 있는 경우 이 메서드를 사용합니다. 그러면 코드는 6 장에서 설명한 대로 Some(&element) 또는 None을 처리하는 로직을 갖게 됩니다. 예를 들어, 인덱스는 사용자가 숫자를 입력하는 것에서 가져올 수 있습니다. 실수로 너무 큰 숫자를 입력하여 프로그램이 None 값을 얻으면 현재 벡터에 몇 개의 항목이 있는지 사용자에게 알리고 유효한 값을 입력할 수 있는 기회를 다시 줄 수 있습니다. 이는 오타로 인해 프로그램이 충돌하는 것보다 사용자 친화적입니다!
프로그램에 유효한 참조가 있는 경우, borrow checker 는 소유권 및 빌림 규칙 (4 장에서 다룸) 을 적용하여 이 참조와 벡터 내용에 대한 다른 모든 참조가 유효하게 유지되도록 합니다. 동일한 범위에서 가변 및 불변 참조를 가질 수 없다는 규칙을 기억하세요. 이 규칙은 Listing 8-6 에 적용됩니다. 여기서는 벡터의 첫 번째 요소에 대한 불변 참조를 유지하고 끝에 요소를 추가하려고 합니다. 이 프로그램은 함수에서 나중에 해당 요소를 참조하려는 경우 작동하지 않습니다.
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
Listing 8-6: 항목에 대한 참조를 유지하면서 벡터에 요소를 추가하려는 시도
이 코드를 컴파일하면 다음과 같은 오류가 발생합니다.
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
Listing 8-6 의 코드는 작동해야 하는 것처럼 보일 수 있습니다: 첫 번째 요소에 대한 참조가 벡터의 끝에서 변경 사항에 대해 왜 신경 써야 할까요? 이 오류는 벡터가 작동하는 방식 때문입니다: 벡터는 메모리에서 값을 서로 옆에 배치하므로, 벡터의 끝에 새 요소를 추가하려면 벡터가 현재 저장된 위치에 모든 요소를 서로 옆에 배치할 공간이 충분하지 않은 경우 새 메모리를 할당하고 이전 요소를 새 공간으로 복사해야 할 수 있습니다. 이 경우 첫 번째 요소에 대한 참조는 할당 해제된 메모리를 가리키게 됩니다. 빌림 규칙은 프로그램이 그런 상황에 빠지는 것을 방지합니다.
참고:
Vec<T>타입의 구현 세부 사항에 대한 자세한 내용은 *https://doc.rust-lang.org/nomicon/vec/vec.html*에서 "The Rustonomicon"을 참조하세요.
벡터의 각 요소에 차례로 접근하려면, 한 번에 하나씩 접근하기 위해 인덱스를 사용하는 대신 모든 요소를 반복합니다. Listing 8-7 은 for 루프를 사용하여 i32 값의 벡터에서 각 요소에 대한 불변 참조를 얻고 이를 출력하는 방법을 보여줍니다.
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
Listing 8-7: for 루프를 사용하여 요소를 반복하여 벡터의 각 요소 출력
또한 모든 요소를 변경하기 위해 가변 벡터의 각 요소에 대한 가변 참조를 반복할 수도 있습니다. Listing 8-8 의 for 루프는 각 요소에 50을 더합니다.
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
Listing 8-8: 벡터의 요소에 대한 가변 참조를 반복
가변 참조가 가리키는 값을 변경하려면, += 연산자를 사용하기 전에 * 역참조 연산자를 사용하여 i의 값에 접근해야 합니다. 역참조 연산자에 대한 자세한 내용은 "포인터를 따라 값으로"에서 다루겠습니다.
불변 또는 가변 여부에 관계없이 벡터를 반복하는 것은 borrow checker 의 규칙 때문에 안전합니다. Listing 8-7 과 Listing 8-8 의 for 루프 본문에서 항목을 삽입하거나 제거하려고 시도하면 Listing 8-6 의 코드에서 얻은 것과 유사한 컴파일러 오류가 발생합니다. for 루프가 유지하는 벡터에 대한 참조는 전체 벡터의 동시 수정을 방지합니다.
벡터는 동일한 타입의 값만 저장할 수 있습니다. 이는 불편할 수 있습니다; 서로 다른 타입의 항목 목록을 저장해야 하는 사용 사례가 분명히 있습니다. 다행히, 열거형의 변형 (variant) 은 동일한 열거형 타입으로 정의되므로, 서로 다른 타입의 요소를 나타내는 하나의 타입이 필요할 때 열거형을 정의하고 사용할 수 있습니다!
예를 들어, 스프레드시트의 행에서 값을 가져오려고 하는데, 행의 일부 열에는 정수, 일부 부동 소수점 숫자, 일부 문자열이 포함되어 있다고 가정해 보겠습니다. 서로 다른 값 타입을 저장할 변형을 가진 열거형을 정의할 수 있으며, 모든 열거형 변형은 동일한 타입, 즉 열거형의 타입으로 간주됩니다. 그런 다음 해당 열거형을 저장하는 벡터를 생성할 수 있으며, 궁극적으로 서로 다른 타입을 저장합니다. Listing 8-9 에서 이를 시연했습니다.
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-9: 하나의 벡터에 서로 다른 타입의 값을 저장하기 위해 enum 정의
Rust 는 컴파일 시간에 벡터에 어떤 타입이 포함될지 알아야 각 요소를 저장하는 데 필요한 힙 (heap) 메모리의 양을 정확히 알 수 있습니다. 또한 이 벡터에서 허용되는 타입에 대해 명시적으로 지정해야 합니다. Rust 가 벡터가 모든 타입을 저장하도록 허용하면 하나 이상의 타입이 벡터의 요소에 대해 수행되는 연산에서 오류를 발생시킬 가능성이 있습니다. 열거형과 match 표현식을 사용하면 Rust 는 6 장에서 설명한 대로 컴파일 시간에 모든 가능한 경우를 처리하도록 보장합니다.
프로그램이 런타임에 벡터에 저장할 타입의 전체 집합을 모르는 경우, 열거형 기술은 작동하지 않습니다. 대신, 17 장에서 다룰 트레이트 객체 (trait object) 를 사용할 수 있습니다.
이제 벡터를 사용하는 가장 일반적인 몇 가지 방법을 논의했으므로, 표준 라이브러리에 의해 Vec<T>에 정의된 많은 유용한 메서드의 API 문서를 검토하십시오. 예를 들어, push 외에도 pop 메서드는 마지막 요소를 제거하고 반환합니다.
다른 struct와 마찬가지로, Listing 8-10 에 주석으로 표시된 것처럼 벡터는 범위를 벗어날 때 해제됩니다.
{
let v = vec![1, 2, 3, 4];
// do stuff with v
} // <- v goes out of scope and is freed here
Listing 8-10: 벡터와 해당 요소가 삭제되는 위치 표시
벡터가 삭제되면 해당 내용도 모두 삭제됩니다. 즉, 벡터가 가지고 있는 정수가 정리됩니다. borrow checker 는 벡터 자체가 유효한 동안에만 벡터 내용에 대한 모든 참조가 사용되도록 보장합니다.
다음 컬렉션 타입인 String으로 넘어가 보겠습니다!
축하합니다! 벡터를 사용하여 값 목록 저장 (Storing Lists of Values With Vectors) 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.