러스트 18 - Collections #1 : Vector
언제나 그렇듯 대부분의 언어는 컬렉션이라 불리는 데이터 구조를 제공한다.
우리가 공부했던 <변수와 타입 #2>튜플과 배열과는 다르게, 힙 영역에 저장된다.
즉, Compile Time에 데이터 크기를 알 필요가 없다는 뜻이 되며, 크기 확장 혹은 축소가 가능하다는 이야기다.
(우리가 공부할 각 컬렉션의 성격이 조금씩 다르니, 적절하게 사용할 수 있는 능력을 기르는 것이 옳다고 생각된다.)
이번 장에서 다룰 컬렉션은 Vector(벡터)다.
- 메모리상에서 이웃되도록 모든 값을 넣는 단일 데이터 구조에 하나 이상의 값을 저장할 수 있도록 한다.
- 같은 타입의 값만을 저장 가능하다.
벡터 정의 및 사용
fn main() {
// vector 정의
let mut vector: Vec<i32> = Vec::new();
// vector에 값 삽입
vector.push(1);
vector.push(2);
vector.push(3);
// vector 출력
println!("vector : {:?}", vector);
// vector에서 값 제거
vector.pop();
println!("vector : {:?}", vector);
vector.pop();
println!("vector : {:?}", vector);
vector.pop();
println!("vector : {:?}", vector);
}
처음 언급한 바와 같이, i32타입을 저장할 것이라는 타입 명시만 있을 뿐, 크기는 지정하지 않았다.
사실 코드를 더 줄일 수 있다.
fn main() {
// vector 정의
let mut vector = vec![1, 2, 3];
// vector 출력
println!("vector : {:?}", vector);
// vector에서 값 제거
vector.pop();
println!("vector : {:?}", vector);
vector.pop();
println!("vector : {:?}", vector);
vector.pop();
println!("vector : {:?}", vector);
}
러스트는 편리함을 제공해주기 위해 매크로를 제공한다. 같은 역할을 하는 코드지만, 상당히 축소된 것을 볼 수 있다.
이번엔 특정 값에 접근하는 방법 두 가지를 다뤄보자.
fn main() {
// vector 정의
let vector = vec![1, 2, 3];
// vector 출력
println!("vector : {:?}", vector);
// vector 특정 값 접근 #1
let var1: &i32 = &vector[2];
// let var1: &i32 = &vector[100]; // Error 발생!!!
println!("var1 : {}", var1);
// vector 특정 값 접근 #2
let var2: Option<&i32> = vector.get(2);
// let var2: Option<&i32> = vector.get(100); // None 반환
println!("var2 : {:?}", var2);
}
[idx]를 이용한 방법(#1)과 Vec.get(int idx)를 이용한 방법(#2)이 있다.
#1의 경우 out of index가 발생할 경우 error를 떨구기 때문에, 좋지 않은 접근 방법이라고 볼 수 있다.
#2의 경우 None이 리턴되어 어떻게든 비벼볼 수 있는 상황이 된다.
즉, 항상, index로 무언가 접근할 경우 None을 적극적으로 활용하여 ERROR발생을 최소화 하자!!!
<러스트 13 - 구조체 #1>에서 다루었던 Borrowing에 대해 기억이 나지 않아서 다시 공부하고 왔다... 왜냐하면 지금 다시 나올 예정이니까...ㅎㅎㅎㅎㅎ
프로그램이 유효한 레퍼런스를 가지고 있을 경우 borrow checker는 이 레퍼런스와 벡터 내용에 대한 다른 레퍼런스가 유효하도록 소유권 및 빌림 규칙을 시행한다.
상위 코드의 #1방법으로 접근 한 뒤, vector를 사용해보자.
fn main() {
// vector 정의
let mut vector = vec![1, 2, 3];
// vector 출력
println!("vector : {:?}", vector);
// vector 특정 값 접근 #1
let var1: &i32 = &vector[2];
// let var1: &i32 = &vector[100]; // Error 발생!!!
println!("var1 : {}", var1);
vector.push(99);
// vector 특정 값 접근 #2
// let var2: Option<&i32> = vector.get(2);
// let var2: Option<&i32> = vector.get(100); // None 반환
// println!("var2 : {:?}", var2);
}
빌림을 다루는 장에서 이것저것 많은 것을 해보면 이 에러 메시지를 많이 보았을 것 같다.
이 메시지는 rust가 "야 너 이거 빌려줘놓고 왜 사용하려고 해? 안돼. 쓰지마."라고 이야기 해주는 것.
for문을 이용한 벡터의 모든 값 접근.
fn main() {
let mut vector = vec![1, 2, 3, 50, 100];
// 벡터의 값 일정하게 변경
for i in &mut vector {
*i *= 3;
}
// 각 값 출력 #1
println!("vector : {:?}", vector);
// 각 값 출력 #2
// for i in &vector {
// println!("vector : {}", i);
// }
}
(개인적으로 출력에 대해서는 #1이 내 입맛에 맞다.
#2의 경우, 출력 만을 위해 for문이 들어가는 것이 맘에 들지 않는다. 만일, for문 안에서 값 변경이나 혹은 특정한 로직이 시행 된 후에 출력을 함께 하는 것이라면 #2가 맞지만, 출력만을 위해서 존재하는 것이라면 #1이 맞다고 본다.)
구조체와 같은 데이터 구조와 달리, 같은 타입의 데이터만을 저장할 수 있는 벡터는 불편하게 느낄 수 있을지 모르겠다.
우리는 어렵게 다루었던 열거형을 이용하여 이 제약을 풀 수 있을지 모른다!
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
정수와 실수, 문자열을 담을 수 있게 되었다.
이와 같이 러스트가 Compile Time에 벡터 내에 저장되는 타입을 알아야할 필요가 있기 때문에 각 요소를 저장하기 위해 힙 메모리 영역이 얼마나 필요한지 알 수 있다.
(코드에서 이 벡터가 어떤 유형을 담는지 명시적으로 알 수 있다는 이점도 있다.)