벡터와 값 목록
Vec를 생성·갱신·조회·순회하고 참조 규칙과 열거형을 이용해 길이가 변하는 동종 값 목록을 안전하게 관리합니다.
러스트의 표준 라이브러리에는 컬렉션(collection)이라 불리는 매우 유용한 데이터 구조들이 여러 개 포함되어 있습니다.
대부분의 다른 데이터 타입은 단일한 특정 값을 나타내지만, 컬렉션은 다수의 값을 담을 수 있습니다.
내장 배열(built-in array)이나 튜플 타입과 달리, 이 장에서 다루는 컬렉션은 필요할 때 힙에 할당되는 저장소를 사용하므로 데이터의 양이 컴파일 타임에 결정되지 않아도 되며 프로그램 실행 중에 늘어나거나 줄어들 수 있습니다.
각 컬렉션 종류는 서로 다른 크기와 비용을 가지고 있으며, 현재의 상황에 따라 적절한 컬렉션을 선택하는 것은 장시간에 걸쳐 발전시켜야 하는 기술입니다.
이번 장에서는 러스트 프로그램에서 자주 사용되는 세 가지 컬렉션을 사용 조건과 비용 기준으로 다룹니다.
- 벡터(vector)는 여러 개의 값을 서로 붙어 있게 저장할 수 있도록 해줍니다.
- 문자열(string)은 문자(character)의 모음입니다.
String타입은 전에도 다루었지만, 이번 장에서는 저장 방식과 수정 비용을 더 자세히 봅니다. - 해시맵(hash map)은 어떤 값을 특정한 키와 연관지어 주도록 해줍니다. 이는 맵(map)이라 일컫는 좀 더 일반적인 데이터 구조의 특정한 구현 형태입니다.
표준 라이브러리가 제공하는 다른 컬렉션에 대해 알고 싶으시면, 문서를 봐주세요.
이제부터 벡터, 문자열, 해시맵을 만들고 업데이트하는 방법과 각 컬렉션을 구분하는 기준을 정리합니다.
대표 컬렉션은 저장 형태, 접근 패턴, 키 조회 필요 여부로 고릅니다.
첫 번째로 살펴볼 컬렉션 타입은 벡터라고도 하는 Vec<T>입니다.
벡터를 사용하면 메모리에서 값을 서로 이웃하도록 배치하는 단일 데이터 구조에 0개 이상의 값을 저장할 수 있습니다.
크기가 0보다 큰 요소를 위해 실제 저장 공간을 할당한 Vec<T>에서는 초기화된 요소가 인덱스 순서대로 하나의 연속된 힙 버퍼에 놓입니다. 다만 용량이 0인 빈 벡터와 크기가 0인 타입(ZST)을 담는 벡터는 요소 저장 공간을 할당하지 않으므로, 모든 Vec<T>가 항상 힙 버퍼를 가진다고 일반화하면 안 됩니다.
벡터는 같은 타입의 값만을 저장할 수 있습니다.
벡터는 파일 내의 텍스트 라인들이나 장바구니의 품목 가격 같은 아이템 목록을 저장하는 상황일 때 유용합니다.
새 벡터 만들기
비어 있는 새 벡터를 만들려면 다음 예제 7-1과 같이 Vec::new 함수를
호출합니다.
i32 타입의 값을 가질 수 있는 비어 있는 새 벡터
생성
let v: Vec<i32> = Vec::new();
이 절의 let 발췌는 함수 안에 놓는 코드입니다. 기존 Cargo 패키지의 src/main.rs에서 main 본문에 한 예제씩 넣어 확인합니다. 파일 전체를 바꾸는 완전한 예제는 아래 따라하기에서 제공하므로, 여러 발췌를 한꺼번에 연결해 같은 변수나 예상 오류를 섞지 않아도 됩니다.
위에서 타입 명시(type annotation)가 추가된 것에 주목하세요.
이 벡터에 어떠한 값도 집어넣지 않았기 때문에, 러스트는 저장하고자 하는 요소가 어떤 타입인지 알지 못합니다.
이는 중요한 지점입니다.
벡터는 제네릭(generic)을 이용하여 구현됐습니다; 제네릭을 이용하여 여러분만의 타입을 만드는 방법은 9장에서 다룰 것입니다.
지금 당장은 표준 라이브러리가 제공하는 Vec 타입은 어떠한 타입의 값이라도 저장할 수 있다는 것만 기억해 둡시다.
특정한 타입의 값을 저장할 벡터를 만들 때는 꺾쇠괄호(<>) 안에 해당 타입을 지정합니다.
예제 7-1에서는 러스트에게 v의 Vec이 i32 타입의 요소를 갖는다고 알려주었습니다.
대부분의 경우는 초깃값들과 함께 Vec<T>를 생성하고 러스트는 저장하고자 하는 값의 타입을 대부분 유추할 수 있으므로, 이런 타입 명시를 할 필요가 거의 없습니다.
러스트는 편의를 위해 vec! 매크로를 제공하는데, 이 매크로는 제공된 값들을 저장한 새로운 Vec을 생성합니다.
예제 7-2는 1, 2, 3을 저장한 새로운 Vec<i32>를 생성할 것입니다.
2장의 ‘데이터 타입’ 절에서 본 것처럼, 기본 정수형이 i32이기 때문에 여기서도 타입은 i32입니다.
let v = vec![1, 2, 3];
러스트는 i32 값이 초깃값으로 설정된 것을 이용해, v의 타입을 Vec<i32>로 추론할 수 있습니다.
따라서 타입 명시는 필요 없습니다.
다음으로 벡터를 수정할 때는 가변 벡터와 push의 저장 공간 변화를 함께 봐야 합니다.
벡터 업데이트하기
벡터를 만들고 여기에 요소를 추가하기 위해서는 다음 예제 7-3처럼
push 메서드를 사용할 수 있습니다.
push 메서드를 사용하여 벡터에 값을
추가하기
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
2장에서 설명한 것처럼, 어떤 변수의 값을 변경하려면 mut 키워드를 사용하여 해당 변수를 가변으로 만들어야 합니다.
또한 Vec<i32> 타입 명시를 붙이지 않아도 되는 이유는, 집어넣은 숫자가 모두 i32 타입인 점을 통하여 러스트가 v의 타입을 추론하기 때문입니다.
벡터 요소 읽기
다음 다이어그램은 벡터 접근 방식과 범위 밖 인덱스의 실패 경로를 요약합니다.
Rust Vec 작업과 접근 실패 정책
길이 5 Vec에서 get(2)는 값 3의 참조 Some, get(100)은 None으로 갈리는 실제 조회다. 한 입력의 범위 판정·반환과 별도 입력의 부재를 표시하면 [] 실패 정책과 차이가 드러난다.
CREATE · ACCESS · TYPE · DROP
Vec<T>는 작업보다 실패와 소유권 정책을 먼저 고른다
인덱싱이 위험하고 get이 무조건 안전한 것이 아닙니다. 범위 위반을 프로그램 불변식의 실패로 볼지, 정상적으로 처리할 부재로 볼지가 접근 API를 결정합니다.
보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.
생성·접근·타입·수명을 실패 정책과 함께 고른다
생성 · 추론할 근거를 둔다
let v: Vec<i32> = Vec::new();처럼 다른 타입 제약이 없을 때 표기합니다.vec!로 만든 값 목록과 이후push는 추론 근거가 됩니다. 용량 0과 ZST는 요소 버퍼를 할당하지 않습니다.인덱싱 · 불변식 위반은 패닉
&v[i]는 범위 안이라는 프로그램 계약을 짧게 표현하며, 위반하면 현재 스레드에서 패닉합니다.get· 정상적인 부재는 값으로v.get(i)는Option<&T>를 돌려Some과None을 호출자가 분기하게 합니다.종류의 경계 · 닫힌 집합과 열린 구현
Box<dyn Trait>을Vec에 담는 트레이트 객체 경계는 구현 종류가 열려야 할 때 검토합니다. 컴파일 시점에 아는 닫힌 종류는SpreadsheetCell같은enum으로 묶습니다.길이와 수명 · 재배치와 소유 값
push는 가변 벡터 끝에 값을 추가하고, 용량이 부족하면 새 요소 버퍼를 할당해 기존 요소를 옮길 수 있습니다.pop은 마지막 요소를 제거해 그 소유 값을Option<T>로 반환합니다. 벡터가 드롭되면 남은 소유 요소도 함께 드롭되고 할당한 버퍼가 해제됩니다.
판단 질문: 없는 인덱스가 버그입니까, 입력의 정상적인 부재입니까? 그 답이 &v[i]와 get을 가릅니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 길이 5인 벡터에서 usize 인덱스 i가 범위 안이면 Some 요소 참조, 밖이면 None이다. 2는 값 3을 가리키고, 100에 해당하는 요소는 없다. 인덱싱 []의 실패 정책은 원래 보조 설명과 대조한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 조회 입력 길이 5 Vec에 index 2를 요청한다. |
|
|
| 성공 반환 2<5이므로 값 3을 빌려 반환한다. |
|
|
| 별도 부재 사례 index 100이면 None이다. |
|
|
Rust Vec 작업과 접근 실패 정책
길이 5 Vec에서 get(2)는 값 3의 참조 Some, get(100)은 None으로 갈리는 실제 조회다. 한 입력의 범위 판정·반환과 별도 입력의 부재를 표시하면 [] 실패 정책과 차이가 드러난다.
CREATE · ACCESS · TYPE · DROP
Vec<T>는 작업보다 실패와 소유권 정책을 먼저 고른다
인덱싱이 위험하고 get이 무조건 안전한 것이 아닙니다. 범위 위반을 프로그램 불변식의 실패로 볼지, 정상적으로 처리할 부재로 볼지가 접근 API를 결정합니다.
보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.
생성·접근·타입·수명을 실패 정책과 함께 고른다
생성 · 추론할 근거를 둔다
let v: Vec<i32> = Vec::new();처럼 다른 타입 제약이 없을 때 표기합니다.vec!로 만든 값 목록과 이후push는 추론 근거가 됩니다. 용량 0과 ZST는 요소 버퍼를 할당하지 않습니다.인덱싱 · 불변식 위반은 패닉
&v[i]는 범위 안이라는 프로그램 계약을 짧게 표현하며, 위반하면 현재 스레드에서 패닉합니다.get· 정상적인 부재는 값으로v.get(i)는Option<&T>를 돌려Some과None을 호출자가 분기하게 합니다.종류의 경계 · 닫힌 집합과 열린 구현
Box<dyn Trait>을Vec에 담는 트레이트 객체 경계는 구현 종류가 열려야 할 때 검토합니다. 컴파일 시점에 아는 닫힌 종류는SpreadsheetCell같은enum으로 묶습니다.길이와 수명 · 재배치와 소유 값
push는 가변 벡터 끝에 값을 추가하고, 용량이 부족하면 새 요소 버퍼를 할당해 기존 요소를 옮길 수 있습니다.pop은 마지막 요소를 제거해 그 소유 값을Option<T>로 반환합니다. 벡터가 드롭되면 남은 소유 요소도 함께 드롭되고 할당한 버퍼가 해제됩니다.
판단 질문: 없는 인덱스가 버그입니까, 입력의 정상적인 부재입니까? 그 답이 &v[i]와 get을 가릅니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 길이 5인 벡터에서 usize 인덱스 i가 범위 안이면 Some 요소 참조, 밖이면 None이다. 2는 값 3을 가리키고, 100에 해당하는 요소는 없다. 인덱싱 []의 실패 정책은 원래 보조 설명과 대조한다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 조회 입력 길이 5 Vec에 index 2를 요청한다. |
|
|
| 성공 반환 2<5이므로 값 3을 빌려 반환한다. |
|
|
| 별도 부재 사례 index 100이면 None이다. |
|
|
벡터에 저장된 값을 참조하는 방법은 인덱싱과 get 메서드 두 가지가 있습니다.
다음 예제에서는 명료한 전달을 위해 각 함수들이 반환하는 값의 타입을 명시했습니다.
예제 7-4는 인덱스 문법과 get 메서드를 가지고 벡터의 값에
접근하는 두 방법을 모두 보여줍니다.
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."),
}
여기서 주의할 세부 사항이 몇 가지 있습니다.
벡터의 인덱스는 0부터 시작하므로, 세 번째 값을 얻어오기 위해서는 인덱스 값 2를 사용합니다.
&와 []를 사용하면 인덱스 값에 위치한 요소의 참조자를 얻게 됩니다.
get 메서드에 인덱스를 매개변수로 넘기면, match를 통해 처리할 수 있는 Option<&T>를 얻게 됩니다.
러스트가 벡터 요소를 참조하는 방법을 두 가지 제공하는 이유는 벡터에 없는 인덱스 값을 사용하고자 했을 때 프로그램이 어떻게 동작할 것인지 선택할 수 있도록 하기 위해서입니다.
예를 들어, 아래의 예제 7-5와 같이 5개의 요소를 가지고 있는 벡터가 있고 인덱스 100에 있는 요소에 접근을 시도하는 경우 어떤 일이 생기는지 확인해봅시다.
예제 7-5: 5개의 요소를 가진 벡터에 인덱스 100에 있는 요소에 접근하기 let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
이 프로그램을 실행하면 첫 번째 [] 인덱싱 표현식은 존재하지 않는 요소를 참조하려 하므로 현재 스레드를 패닉 상태로 만듭니다.
범위 밖 접근이 정상적인 입력이 아니라 프로그램 불변식 위반이어서 즉시 드러내야 할 때 이 방식을 선택할 수 있습니다.
get 메서드에 벡터 범위를 벗어난 인덱스가 주어지면 패닉 없이 None이 반환됩니다.
예제 7-5의 두 접근은 실패 정책을 비교하는 코드입니다. 그대로 한 번에 실행하면 첫 번째 인덱싱에서 패닉하므로 그다음 get까지 도달하지 않습니다. get의 None을 확인할 때는 인덱싱 줄을 제외한 별도 실험으로 봅니다. 두 방법이 같은 실행에서 연달아 결과를 반환하는 것은 아닙니다.
일반적인 상황에서 벡터의 범위 밖에 있는 요소에 접근하는 일이 종종 발생할 수도 있다면 이 방법을 사용할 만합니다.
이 방법을 사용한다면 5장에서 본 것처럼 Some(&element) 혹은 None에 대해 처리하는 로직이 있어야 합니다.
예를 들어 인덱스는 사람이 직접 번호를 입력하는 것으로 들어올 수도 있습니다.
만일 사용자가 잘못하여 너무 큰 숫자를 입력하여 프로그램이 None 값을 받았을 경우라면 사용자에게 현재 Vec에 몇 개의 아이템이 있는지 알려 주고, 유효한 값을 입력할 기회를 다시 한 번 줄 수도 있습니다.
이렇게 하는 편이 오타 때문에 실행 흐름이 패닉으로 중단되는 것보다 더 사용자 친화적이겠죠?
프로그램에 유효한 참조자가 있다면, 대여 검사기(borrow checker)가 (3장에서 다루었던) 소유권 및 대여 규칙을 집행하여 이 참조자와 벡터의 내용물로부터 얻은 다른 참조자들이 계속 유효하게 남아있도록 보장합니다.
서로 겹치는 저장 장소에 대한 가변 참조자와 불변 참조자의 사용 구간이 겹칠 수 없다는 규칙을 상기하세요.
이 규칙은 아래 예제에서도 적용되는데, 예제 7-6에서는 벡터의 첫 번째 요소에 대한 불변 참조자를 얻은 뒤 벡터의 끝에 요소를 추가하는 시도를 합니다.
함수 끝에서 해당 요소에 대한 참조까지 시도한다면 이 프로그램은 동작하지 않을 것입니다.
예제 7-6: 아이템의 참조자를 가지고 있는 상태에서 벡터에 새로운 요소 추가 시도하기 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` due to previous error
예제 7-6의 코드는 동작해야 할 것처럼 보일 수도 있습니다.
첫 번째 요소의 참조자가 벡터 끝부분의 변경이랑 무슨 상관일까요?
이를 이해하려면 벡터의 저장 공간이 바뀔 수 있다는 점을 확인해야 합니다.
실제 요소 버퍼가 할당된 일반적인 벡터에서는 초기화된 요소가 서로 붙어서 메모리에 저장됩니다.
그리고 새로운 요소를 벡터 끝에 추가할 경우, 현재 벡터 메모리 위치에 새로운 요소를 추가할 공간이 없다면 더 넉넉한 저장 공간을 새로 할당하고 기존 요소를 그곳으로 재배치합니다.
이 경우, 기존 요소의 참조자는 해제된 메모리를 가리키게 되기 때문에, 이러한 상황을 대여 규칙으로 막습니다.
재배치 가능성은 규칙의 이유이고, 실제 검사 기준은 겹치는 공유·독점 대여입니다. 용량을 미리 충분히 확보했더라도 예제 7-6처럼 push 뒤에 first를 다시 쓰면 대여 충돌은 남습니다. 먼저 first를 마지막으로 사용한 뒤 push하면 그 공유 대여를 더 이상 유지할 필요가 없습니다. first라는 변수의 블록이 반드시 끝나야만 하는 것은 아닙니다.
아래 다이어그램은 Vec<T> 요소 참조와 push가 만났을 때 대여 검사기가
무엇을 보호하는지 정리합니다.
Rust Vec 대여와 구조 변경 경계
first=&v[0] 생성→마지막 읽기→push(6)가 허용되는 실제 접근 상태 변화다. push 뒤 first 재사용의 거부 사본은 컴파일 검사로 분리하고 용량이 충분해도 충돌이 남는다는 조건을 보존한다.
BORROW STATE · ITERATION · REALLOCATION
요소를 빌린 동안에는 값을 읽거나 고치고, 구조는 대여가 끝난 뒤 바꾼다
직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여를 겹치지 않는 것입니다. push가 용량 부족 때 요소 주소를 옮길 수 있다는 사실은 왜 그 규칙이 필요한지를 보여 줍니다.
보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.
겹치는 사용 구간: 컴파일 오류
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");
거부 실제로 재할당되는지와 무관하게, 살아 있는 요소 참조와 벡터의 독점 변경이 겹칩니다.
마지막 사용 뒤: 구조 변경 가능
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);
허용 선언된 블록 전체가 아니라 참조의 마지막 사용까지가 핵심입니다.
| 현재 상태 | 허용되는 작업 | 구조 변경 경계 |
|---|---|---|
| 요소 대여 없음 | 읽기와 함께 push, pop, insert, remove 같은 독점 변경을 시작할 수 있습니다. | 변경이 새 대여를 만들기 전까지 가능합니다. |
| 공유 반복 | | 반복자가 벡터를 공유 대여하므로 루프 안에서 길이를 바꿀 수 없습니다. |
| 가변 반복 | | 현재 요소의 값은 바꿀 수 있지만 반복 중 벡터 구조를 다시 빌릴 수 없습니다. |
| 마지막 사용 이후 | 공유·가변 반복 또는 요소 참조의 사용이 끝났습니다. | 그 뒤에 구조 변경을 배치합니다. |
충돌을 푸는 네 가지 방법
먼저 사용
요소 참조의 마지막 사용을
push앞으로 옮깁니다.범위를 닫기
별도 블록에서 읽고 블록이 끝난 뒤 벡터를 변경합니다.
소유 값을 보관
작은
Copy값은 복사하고, 필요하다면 의미를 검토한 뒤 명시적으로 복제합니다.변경 뒤 재조회
append-only
push처럼 기존 위치 의미가 유지될 때 인덱스를 보관하고 변경 뒤 다시 빌립니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 다시 확인합니다.
겹치는 사용 구간 · 컴파일 오류
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");이후에 쓸 요소 참조와 push의 독점 대여가 겹칩니다.
마지막 사용 뒤 · 구조 변경 가능
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);먼저 참조를 사용한 뒤 벡터를 변경합니다.
공유 대여
for item in &v&v[i]나 공유 반복이 살아 있는 동안 읽을 수 있지만 길이 변경은 시작할 수 없습니다.가변 반복
for item in &mut v { *item += 50; }현재 요소는 고칠 수 있지만
push나pop으로 구조를 바꿀 수 없습니다.마지막 사용
참조의 선언 위치나 블록 전체가 아니라 실제 마지막 사용 뒤에 독점 변경을 배치합니다.
재할당 근거
용량 부족 시
push가 요소 버퍼를 옮길 수 있지만, 컴파일러가 직접 적용하는 규칙은 겹치는 공유·독점 대여 금지입니다.교정 전략
참조를 먼저 사용하거나 범위를 닫고, 소유 값을 보관하거나 의미가 유지되는 인덱스로 변경 뒤 다시 조회합니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 확인합니다.
순서: 요소 대여의 마지막 사용 → 반복 종료 → push·pop 같은 구조 변경.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 본문·주석 | 직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여가 겹치지 않게 하는 것입니다. |
| 관계·조건 설명 | 두 칼럼은 실행 시간표가 아니라 허용 또는 거부할 소스 순서다. 왼쪽은 참조의 마지막 사용 후 push한다. 오른쪽은 push 뒤 참조를 재사용하여 대여 구간이 겹친다. 테두리는 공유 참조의 사용 구간이다. 용량이 충분해도 오른쪽 소스는 거부된다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 요소 읽기 first가 v[0]의 요소를 공유 대여한다. |
|
|
| 마지막 사용 println으로 first를 마지막으로 읽는다. |
|
|
| push 허용 마지막 읽기 뒤 push(6)으로 Vec을 변경한다. |
|
|
Rust Vec 대여와 구조 변경 경계
first=&v[0] 생성→마지막 읽기→push(6)가 허용되는 실제 접근 상태 변화다. push 뒤 first 재사용의 거부 사본은 컴파일 검사로 분리하고 용량이 충분해도 충돌이 남는다는 조건을 보존한다.
BORROW STATE · ITERATION · REALLOCATION
요소를 빌린 동안에는 값을 읽거나 고치고, 구조는 대여가 끝난 뒤 바꾼다
직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여를 겹치지 않는 것입니다. push가 용량 부족 때 요소 주소를 옮길 수 있다는 사실은 왜 그 규칙이 필요한지를 보여 줍니다.
보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.
겹치는 사용 구간: 컴파일 오류
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");
거부 실제로 재할당되는지와 무관하게, 살아 있는 요소 참조와 벡터의 독점 변경이 겹칩니다.
마지막 사용 뒤: 구조 변경 가능
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);
허용 선언된 블록 전체가 아니라 참조의 마지막 사용까지가 핵심입니다.
| 현재 상태 | 허용되는 작업 | 구조 변경 경계 |
|---|---|---|
| 요소 대여 없음 | 읽기와 함께 push, pop, insert, remove 같은 독점 변경을 시작할 수 있습니다. | 변경이 새 대여를 만들기 전까지 가능합니다. |
| 공유 반복 | | 반복자가 벡터를 공유 대여하므로 루프 안에서 길이를 바꿀 수 없습니다. |
| 가변 반복 | | 현재 요소의 값은 바꿀 수 있지만 반복 중 벡터 구조를 다시 빌릴 수 없습니다. |
| 마지막 사용 이후 | 공유·가변 반복 또는 요소 참조의 사용이 끝났습니다. | 그 뒤에 구조 변경을 배치합니다. |
충돌을 푸는 네 가지 방법
먼저 사용
요소 참조의 마지막 사용을
push앞으로 옮깁니다.범위를 닫기
별도 블록에서 읽고 블록이 끝난 뒤 벡터를 변경합니다.
소유 값을 보관
작은
Copy값은 복사하고, 필요하다면 의미를 검토한 뒤 명시적으로 복제합니다.변경 뒤 재조회
append-only
push처럼 기존 위치 의미가 유지될 때 인덱스를 보관하고 변경 뒤 다시 빌립니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 다시 확인합니다.
겹치는 사용 구간 · 컴파일 오류
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");이후에 쓸 요소 참조와 push의 독점 대여가 겹칩니다.
마지막 사용 뒤 · 구조 변경 가능
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);먼저 참조를 사용한 뒤 벡터를 변경합니다.
공유 대여
for item in &v&v[i]나 공유 반복이 살아 있는 동안 읽을 수 있지만 길이 변경은 시작할 수 없습니다.가변 반복
for item in &mut v { *item += 50; }현재 요소는 고칠 수 있지만
push나pop으로 구조를 바꿀 수 없습니다.마지막 사용
참조의 선언 위치나 블록 전체가 아니라 실제 마지막 사용 뒤에 독점 변경을 배치합니다.
재할당 근거
용량 부족 시
push가 요소 버퍼를 옮길 수 있지만, 컴파일러가 직접 적용하는 규칙은 겹치는 공유·독점 대여 금지입니다.교정 전략
참조를 먼저 사용하거나 범위를 닫고, 소유 값을 보관하거나 의미가 유지되는 인덱스로 변경 뒤 다시 조회합니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 확인합니다.
순서: 요소 대여의 마지막 사용 → 반복 종료 → push·pop 같은 구조 변경.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 본문·주석 | 직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여가 겹치지 않게 하는 것입니다. |
| 관계·조건 설명 | 두 칼럼은 실행 시간표가 아니라 허용 또는 거부할 소스 순서다. 왼쪽은 참조의 마지막 사용 후 push한다. 오른쪽은 push 뒤 참조를 재사용하여 대여 구간이 겹친다. 테두리는 공유 참조의 사용 구간이다. 용량이 충분해도 오른쪽 소스는 거부된다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 요소 읽기 first가 v[0]의 요소를 공유 대여한다. |
|
|
| 마지막 사용 println으로 first를 마지막으로 읽는다. |
|
|
| push 허용 마지막 읽기 뒤 push(6)으로 Vec을 변경한다. |
|
|
Note:
Vec<T>타입의 구현 세부 사항에 대한 그 밖의 것에 대해서는 ‘러스토노미콘(The Rustonomicon)’을 보세요.
벡터 값에 대해 반복하기
벡터 내의 각 요소에 차례대로 접근하기 위해서는 인덱스를 사용하여 한 번에 하나의 값에 접근하기보다는 모든 요소에 대한 반복 처리를 합니다.
예제 7-7은 for 루프를 사용하여 i32의 벡터에 있는 각 요소에 대한 불변 참조자를 얻어서 이를 출력하는 방법을 보여줍니다.
for 루프로 벡터의 요소들에 대해
반복하여 각 요소를 출력하기
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
모든 요소를 변경하기 위해서는 가변 벡터의 각 요소에 대한 가변 참조자로 반복 작업을 할 수도 있습니다.
예제 7-8의 for 루프는 각 요소에 50을 더할 것입니다.
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
가변 참조자가 가리키는 값을 수정하려면, += 연산자를 쓰기 전에 * 역참조 연산자로 i의 값을 얻어야 합니다.
역참조 연산자는 14장 ‘포인터를 따라가서 값 얻기’에서 자세히 알아볼 예정입니다.
벡터에 대한 반복 처리는 불변이든 가변이든 상관없이 대여 검사 규칙에 의해 안전합니다.
만일 예제 7-7과 예제 7-8의 for 루프 본문에서 아이템을 추가하거나 지우는 시도를 했다면 예제 7-6의 코드에서 본 것과 유사한 컴파일 에러가 발생하게 됩니다.
for 루프가 가지고 있는 벡터에 대한 참조자는 전체 벡터의 동시다발적 수정을 막습니다.
앞선 대여 상태 다이어그램처럼 읽기, 요소 수정, 구조 변경은 서로 다른 대여 모드이며 구조 변경은 반복 대여가 끝난 뒤에 수행해야 합니다.
열거형을 이용해 여러 타입 저장하기
벡터는 같은 타입을 가진 값들만 저장할 수 있습니다.
이는 불편할 수 있습니다; 다른 타입의 아이템들에 대한 리스트를 저장해야 하는 상황도 분명히 있으니까요.
다행히도, 열거형의 배리언트는 같은 열거형 타입 내에 정의가 되므로, 벡터 내에 다른 타입의 값들을 저장할 필요가 있다면 열거형을 정의하여 사용할 수 있습니다!
예를 들어, 스프레드시트의 행으로부터 값들을 가져오고 싶은데, 여기서 어떤 열은 정수를, 어떤 열은 실수를, 어떤 열은 문자열을 갖고 있다고 해봅시다.
다양한 타입의 값을 갖는 배리언트를 보유한 열거형을 정의할 수 있고, 모든 열거형 배리언트들은 해당 열거형 타입과 같은 타입으로 간주됩니다.
그러면 해당 열거형을 담을 벡터를 생성하여 궁극적으로 다양한 타입을 담을 수 있습니다.
예제 7-9에서 이를 보여주고 있습니다.
예제 7-9: 열거형을 정의하여 벡터 내에 다른 타입의 데이터를 담을 수 있도록 하기 enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
러스트가 컴파일 타임에 벡터 내에 저장될 타입이 무엇인지 알아야 하는 이유는 각 요소를 저장하기 위해 얼마만큼의 힙 메모리가 필요한지 알아야 하기 때문입니다.
또한 이 벡터가 담을 수 있는 타입을 명시적으로 보여줘야 합니다.
만일 러스트가 어떠한 타입이든 담을 수 있는 벡터를 허용한다면, 벡터의 각 요소마다 수행되는 연산에 대해 하나 혹은 그 이상의 타입이 에러를 발생시킬 수도 있습니다.
열거형과 match 표현식을 사용한다는 것은 5장에서 설명한 것처럼 러스트가 컴파일 타임에 가능한 모든 경우를 처리함을 보장해 준다는 뜻입니다.
런타임에 프로그램이 벡터에 저장할 모든 타입 집합을 알지 못하면 열거형을 이용한 방식은 사용할 수 없을 것입니다.
대신 트레이트 객체(trait object)를 이용할 수 있는데, 이건 16장에서 다룰 예정입니다.
지금까지 벡터를 이용하는 가장 일반적인 방식 몇 가지를 논의했는데, 표준 라이브러리의 Vec에 정의된 유용한 메서드들이 많이 있으니 API 문서를 꼭 살펴봐 주시기 바랍니다.
예를 들면, push에 더해서, pop 메서드는 제일 마지막 요소를 반환하고 지워줍니다.
벡터가 버려지면 벡터의 요소도 버려집니다
struct와 마찬가지로, 예제 7-10에 주석으로 표시된 것처럼
벡터는 스코프를 벗어날 때 해제됩니다.
{
let v = vec![1, 2, 3, 4];
// v를 가지고 작업하기
} // <- 여기서 v가 스코프 밖으로 벗어나고 해제됩니다
벡터가 버려질 때 벡터의 내용물도 전부 버려집니다.
즉, 벡터가 가지고 있던 정수들의 메모리도 정리됩니다.
대여 검사기는 벡터의 내용물에 대한 참조자의 사용이 해당 벡터가 유효할 때만 발생했는지 확인합니다.
따라하기: 참조의 마지막 사용과 복사한 값을 나누기
다음은 src/main.rs 전체로 사용할 수 있습니다. 요소 참조를 먼저 출력한 뒤 벡터의 길이를 바꾸고, 그다음 가변 반복으로 요소를 수정합니다. 이 파일은 범위 밖 인덱싱을 실행하지 않고 get의 반환값으로 부재를 확인합니다.
fn main() {
let mut values = vec![100, 32, 57];
let saved = values[0];
let first = &values[0];
println!("first: {first}");
values.push(6);
for value in &mut values {
*value += 50;
}
let removed = values.pop();
println!("values: {values:?}; removed: {removed:?}");
println!("saved: {saved}; index3: {:?}", values.get(3));
}첫 줄은 first: 100입니다. push 뒤 목록은 100, 32, 57, 6이고 각각에 50을 더하면 150, 82, 107, 56입니다. pop이 마지막 56의 소유 값을 Some(56)으로 반환하므로 둘째 줄은 values: [150, 82, 107]; removed: Some(56)입니다. 정수 saved는 처음에 복사한 100이라서 변하지 않습니다. 남은 길이는 3이므로 마지막 줄은 saved: 100; index3: None입니다. println 뒤에 first를 다시 쓰는 문장을 넣으면 앞선 push와 공유 대여가 겹치는 소스로 바뀝니다.
독립 연습
별도 실험에서 목록을 [2, 4]로 만들고 첫 정수를 saved에 복사합니다. 모든 요소에 3을 곱한 뒤 8을 push하고 한 번 pop합니다. 마지막 목록, saved, pop의 반환값, get(2)의 결과를 적으세요. 또한 첫 요소를 참조한 first를 push 앞에서만 출력하는 경우와 push 뒤에서 출력하는 경우를 구분하세요. 용량은 충분히 확보해도 좋지만, 뒤에서 first를 읽는 조건은 바꾸지 않습니다.
길이와 대여 사용을 따로 추적하는 풀이
값은 [6, 12]에서 push 후 [6, 12, 8]이 되고, pop은 Some(8)을 돌려 마지막 목록을 [6, 12]로 만듭니다. saved는 2이며 get(2)는 None입니다. push 앞에서 first를 마지막으로 사용하면 구조 변경을 할 수 있지만, 뒤에서 다시 사용하면 컴파일 오류입니다. 충분한 capacity가 공유·독점 대여의 겹침을 허용하는 예외를 만들지는 않습니다. 참조를 오래 보관할 필요가 없고 T가 Copy인 현재 사례에서는 saved처럼 값을 복사해 둘 수도 있습니다.
Vec의 용량·재배치 보장은 len과 capacity가 서로 다른 값이며, 정확한 확장 배수를 가정하지 않아야 한다는 점도 설명합니다.
이제 다음 컬렉션 타입인 String으로 넘어갑시다!
벡터를 사용할 때는 “없는 인덱스를 어떻게 처리할지”, “참조를 잡은 채 수정하는지”, “여러 타입을 정말 한 목록에 넣어야 하는지”를 먼저 확인하면 대여 오류와 패닉을 줄일 수 있습니다.