Chap.8 common-collections
러스트의 표준 라이브러리에는 컬렉션
이라 불리는 여러 개의 매우 유용한 데이터 구조들이 포함되어 있습니다.
대부분의 다른 데이터 타입들은 하나의 특정한 값을 나타내지만, 컬렉션은 다수의 값을 담을 수 있습니다.
내장 된 배열(build-in array)와 튜플 타입과는 달리, 이 컬렉션들이 가리키고 있는 데이터들은 힙에 저장되는데, 이는 즉 데이터량이 컴파일 타임에 결정되지 않아도 되며 프로그램이 실행될 때 늘어나거나 줄어들 수 있다는 의미입니다.
각각의 컬렉션 종류는 서로 다른 용량과 비용을 가지고 있으며, 여러분의 현재 상황에 따라 적절한 컬렉션을 선택하는 것은 시간이 지남에 따라 발전시켜야 할 기술입니다.
이번 장에서는 러스트 프로그램에서 굉장히 자주 사용되는 세 가지 컬렉션에 대해 논의해 보겠습니다:
- 벡터(vector) 는 여러 개의 값을 서로 붙어 있게 저장할 수 있도록 해줍니다.
- 스트링(string) 은 문자(character)의 모음입니다.
String
타입은 이전에 다루었지만, 이번 장에서는 더 깊이 있게 이야기해 보겠습니다. - 해쉬맵(hash map 은 어떤 값을 특정한 키와 연관지어 주도록 해줍니다. 이는 맵(map) 이라 일컫는 좀더 일반적인 데이터 구조의 특정한 구현 형태입니다.
표준 라이브러리가 제공해주는 다른 종류의 컬렉션에 대해 알고 싶으시면, the documentation를 봐 주세요.
벡터
우리가 보게될 첫번째 콜렉션은 벡터
라고도 알려진 Vec<T>
입니다.
- 벡터는 메모리 상에 서로 이웃하도록 모든 값을 집어넣는 단일 데이터 구조 안에 하나 이상의 값을 저장하도록 해줍니다.
- 벡터는 같은 타입의 값만을 저장할 수 있습니다.
새 벡터 만들기
비어있는 새 벡터를 만들기 위해서는, 아래의 Listing 8-1과 같이 Vec::new
함수를 호출해 줍니다:
let v: Vec<i32> = Vec::new();
- 여기에 타입 명시(type annotation)를 추가한 것을 주목하세요.
- 이 벡터에 어떠한 값도 집어넣지 않았기 때문에, 러스트는 우리가 저장하고자 하는 요소의 종류가 어떤 것인지 알지 못합니다.
- 벡터는 제네릭(generic)을 이용하여 구현되었습니다;
- 표준 라이브러리가 제공하는
Vec
타입은 어떠한 종류의 값이라도 저장할 수 있다는 것, - 그리고 특정한 벡터는 특정한 타입의 값을 저장할 때, 이 타입은 꺾쇠 괄호
(<>)
안에 적는다는 것만 알아두세요.
- 표준 라이브러리가 제공하는
- 일단 우리가 값을 집어넣으면 러스트는 우리가 저장하고자 하는 값의 타입을 대부분 유추할 수 있으므로, 좀 더 현실적인 코드에서는 이러한 타입 명시를 할 필요가 거의 없습니다.
- 초기값들을 갖고 있는
Vec<T>
을 생성하는 것이 더 일반적이며, 러스트는 편의를 위해vec!
매크로를 제공합니다. - 이 매크로는 우리가 준 값들을 저장하고 있는 새로운
Vec
을 생성합니다. 1
,2
,3
을 저장하고 있는 새로운Vec<i32>
을 생성할 것입니다:
let v = vec![1, 2, 3];
Listing 8-2: 값을 저장하고 있는 새로운 벡터 생성하기
초기 i32
값들을 제공했기 때문에, 러스트는 v
가 Vec
타입이라는 것을 유추할 수 있으며, 그래서 타입 명시는 필요치 않습니다.
벡터 갱신하기
벡터를 만들고 여기에 요소들을 추가하기 위해서 push
메소드를 사용할 수 있습니다:
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
Listing 8-3: push
메소드를 사용하여 벡터에 값을 추가하기
어떤 변수에 대해 그 변수가 담고 있는 값이 변경될 수 있도록 하려면, mut
키워드를 사용하여 해당 변수를 가변으로 만들어 줄 필요가 있습니다.
우리가 집어넣는 숫자는 모두 i32
타입이며, 러스트는 데이터로부터 이 타입을 추론하므로, 우리는 Vec<i32>
명시를 붙일 필요가 없습니다.
벡터를 드롭하는 것은 벡터의 요소들을 드롭시킵니다
struct
와 마찬가지로, Listing 8-4에 달려있는 주석처 럼 벡터도 스코프 밖으로 벗어났을 때 해제됩니다:
{
let v = vec![1, 2, 3, 4];
// v를 가지고 뭔가 합니다
}
// <- v가 스코프 밖으로 벗어났고, 여기서 해제됩니다
벡터의 요소들 읽기
지금까지 벡터를 만들고, 갱신하고, 없애는 방법에 대해 알아보았으니, 벡터의 내용물을 읽어들이는 방법을 알아보는 것이 다음 단계로 좋아보입니다. 벡터 내에 저장된 값을 참조하는 두 가지 방법이 있습니다.
{
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>
를 얻은 것입니다.
러스트가 벡터 요소를 참조하는 두가지 방법을 제공하는 이유는 여러분이 벡터가 가지고 있지 않은 인덱스값을 사용하고자 했을 때 프로그램이 어떻게 동작할 것인지 여러분이 선택할 수 있도록 하기 위해서입니다.
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
에 대해 다루는 로직을 갖추어야 합니다.
유효 하지 않은 참조자
프로그램이 유효한 참조자를 얻을 때, 빌림 검사기(borrow checker)
가 소유권 및 빌림 규칙을 집행하여 이 참조자와 벡터의 내용물로부터 얻은 다른 참조자들이 계속 유효하게 남아있도록 확실히 해줍니다.
같은 스코프 내에서 가변 참조자와 불변 참조자를 가질 수 없다는 규칙을 상기하세요.
이 규칙은 아래 예제에서도 적용되는데, Listing 8-7에서는 벡터의 첫번째 요소에 대한 불변 참조자를 얻은 뒤 벡터의 끝에 요소를 추가하고자 했습니다:
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
// error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
Listing 8-7의 코드는 동작을 해야만 할것처럼 보일 수도 있습니다:
- 왜 첫번째 요소에 대한 참조자가 벡터 끝에 대한 변경을 걱정해야 하죠?
- 이 에러에 대한 내막은 벡터가 동작하는 방법 때문입니다:
- 새로운 요소를 벡터의 끝에 추가하는 것은
- 새로 메모리를 할당하여 예전 요소를 새 공간에 복사하는 일을 필요로 할 수 있는데,
- 이는 벡터가 모든 요소들을 붙여서 저장할 공간이 충분치 않는 환경에서 일어날 수 있습니다.
- 이러한 경우, 첫번째 요소에 대한 참조자는 할당이 해제된 메모리를 가리키게 될 것입니다.
- 빌림 규칙은 프로그램이 이러한 상황에 빠지지 않도록 해줍니다.
노트:
Vec<T>
타입의 구현 세부사항에 대한 그밖의 것에 대해서는 https://doc.rust-lang.org/stable/nomicon/vec.html 에 있는 노미콘(The Nomicon)을 보세요:
벡터 내의 값들에 대한 반복처리
만일 벡터 내의 각 요소들을 차례대로 접근하고 싶다면, 하나의 값에 접근하기 위해 인덱스를 사용하는것 보다는, 모든 요소들에 대해 반복처리를 할 수 있습니다.
let v = vec![100, 32, 57];
for i in &v {
println!("{}", i);
}
Listing 8-8: for
루프를 이용한 요소들에 대한 반복작업을 통해 각 요소들을 출력하기
만일 모든 요소들을 변형시키길 원한다면 가변 벡터 내의 각 요소에 대한 가변 참조자로 반복작업을 할 수도 있습니다.
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
Listing 8-9: 벡터 내의 요소에 대한 가변 참조자로 반복하기
가변 참조자가 참고하고 있는 값을 바꾸기 위해서, i
에 +=
연산자를 이용하기 전에 역참조 연산자 (*
)를 사용하여 값을 얻어야 합니다.
열거형을 사용하여 여러 타입을 저장하기
이 장의 시작 부분에서, 벡터는 같은 타입을 가진 값들만 저장할 수 있다고 이야기했습니다. 이는 불편할 수 있습니다; 다른 타입의 값들에 대한 리스트를 저장할 필요가 있는 상황이 분명히 있지요. 다행히도, 열거형의 variant는 같은 열거형 타입 내에 정의가 되므로, 백터 내에 다른 타입의 값들을 저장할 필요가 있다면 열거형을 정의하여 사용할 수 있습니다!
예를 들어, 스프레드시트의 행으로부터 값들을 가져오고 싶은데, 여기서 어떤 열은 정수를, 어떤 열은 실수를, 어떤 열은 스트링을 갖고 있다고 해봅시다. 우리는 다른 타입의 값을 가지는 variant가 포함된 열거형을 정의할 수 있고, 모든 열거형 variant들은 해당 열거형 타입, 즉 같은 타입으로 취급될 것입니다. 따라서 우리는 궁극적으로 다른 타입을 담은 열거형 값에 대한 벡터를 생성할 수 있습니다. Listing 8-10에서 이를 보여주고 있습니다:
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
으로 넘어갑시다!