컬렉션(collection)은 표준 라이브러리에 포함된 데이터 구조로, 벡터, 문자열과 같이 다수의 값을 담을 수 있으며, 힙에 저장되어 동적으로 할당된다.
벡터
같은 타입의 값만을 저장하는 구조이다.
let v: Vec<i32> = Vec::new();
let v = vec![1, 2, 3];
벡터는 제네릭을 이용하여 구현되었다. 따라서 어떤 타입의 값을 저장할 것인지를 angle bracket ‘<>’으로 감싸서 명시했다(type annotation).
초깃값을 함께 지정하는 경우 러스트에서 타입을 유추할 수 있으므로 생략할 수 있다. 이 때는 매크로 vec!
을 사용한다.
벡터 활용
let mut v = Vec::new();
v.push(5);
v.push(6);
let first = &v[0];
let second: Option<&i32> = v.get(1);
벡터에 값을 추가할 때는 push
로 추가한다.
벡터의 요소에 접근할 때는 인덱스를 사용하거나, get
메서드를 사용한다. get
메서드는 Option<&T>
타입을 반환한다.
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6);
println!("The first element is: {first}");
위처럼 벡터에 대한 불변 참조자가 존재하는 상태에서 벡터를 변경하려 한다면 에러가 발생한다.
push
는 벡터의 마지막에 요소를 추가하는데, 만약 공간이 부족하다면 할당을 해제하고 다른 넉넉한 공간에 새로 할당한다. 그러면 기존의 참조자는 해제된 공간을 가리키기 때문에 이런 상황을 막기 위해 에러가 발생하는 것이다.
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
벡터의 요소에 대해서는 for
루프로 반복하여 접근할 수 있다.
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
가변 참조자를 이용해 벡터 내용을 변경할 수도 있다.
가변참조자가 가리키는 값은 역참조 *
연산자를 사용해 얻을 수 있다.
문자열
러스트 핵심 기능에서의 문자열 타입은 &str
형태로 사용하는 문자열 슬라이스다.
String
타입은 핵심 기능이 아니라 표준 라이브러리를 통해 제공된다.
//비어 있는 문자열 생성
let mut s = String::new();
//메서드를 사용해 String 생성
let s = "initial contents".to_string();
//문자열 리터럴로 String 생성
let s = String::from("initial contents");
문자열 슬라이스든 String
이든 모두 UTF-8로 인코딩되어 있으므로, 한글, 한자 등의 문자도 입력할 수 있다.
//add 메서드의 시그니처
fn add(self, s: &str) -> String {...}
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;
문자열은 +
기호를 사용해 서로 더할 수 있다.
&s2
에 참조자를 사용하는 이유는 add
메서드가 그렇게 정의되어 있기 때문이다.
그런데, &s2
는 &String
타입이지 &str
타입은 아니다. 그럼에도 컴파일 에러는 발생하지 않는다. &String
이 역참조 강제(deref coercion)에 의해 &str
로 강제될 수 있기 때문이다.
s3
는 s1
의 소유권을 가져와서 s2
의 복사본을 추가한 다음, 결과물의 소유권을 반환한 결과다. 즉, s1
은 이 구문이 실행된 이후 유효하지 않다.
이런 까닭에 여러 개의 문자열을 +
로 접합하기는 쉽지 않다.
러스트에서는 format!
매크로를 이용해 간편하게 문자열을 조합할 수 있다.
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
다른 언어에서는 일반적으로 문자열을 인덱싱해서 사용할 수 있다.
s1이 어떤 문자열을 저장하고 있다면 s1[0]
은 첫 번째 문자를 나타낸다.
하지만 러스트에서는 문자열에 인덱싱을 허용하지 않는다.
러스트가 UTF-8을 기본적으로 사용하기 때문에, 문자열의 각 문자를 1바이트로 처리할 때와는 다른 메커니즘이 적용되기 때문이다. &"hello"[0]
는 h
가 아니라 104
를 반환한다.
굳이 인덱스를 통해 문자열의 일부를 추출해야 한다면, 슬라이싱 문법을 사용한다.
let hello = "Здравствуйте";
let s = &hello[0..4];
각 문자는 2바이트를 차지하기 때문에, 첫 번째 글자가 0, 1 인덱스를, 두 번째 글자가 2, 3 인덱스를 차지한다. 따라서 s는 Зд가 된다.
문자열을 추출하는 가장 좋은 방법은 원하는 것이 문자인지 바이트인지 지정하는 것이다.
for c in "Зд".chars() {
println!("{c}");
}
//result: З д
for b in "Зд".bytes() {
println!("{b}");
}
//result: 208 151 208 180
해시맵
해시맵은 key와 value 쌍 값을 해시 함수로 매핑해 저장한 것이다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
let team_name = String::from("Blue");
let score = scores.get(&team_name).copied().unwrap_or(0);
for (key, value) in &scores {
println!("{key}: {value}");
}
해시맵의 get
메서드는 Option<&V>
를 반환한다. 해시맵에 해당 키에 대한 값이 없으면 None
을 반환한다.
여기서는 copied
로 Option<i32>
를 얻어 unwrap_or
로 해당 키에 대한 아이템이 없으면 score
를 0으로 설정한다.
for
루프로 반복할 수 있다.
i32
처럼 Copy
트레이트가 구현되어 있다면 이 타입의 값은 해시맵에 복사된다. String
처럼 소유권이 있는 값이라면 소유권이 해시맵 안으로 이동한다.
해시맵에 값을 추가할 때, 키가 이미 존재하면 값을 그대로 두고, 키가 없다면 새로 추가하는 경우가 많다.
entry
함수는 키를 매개변수로 받아 해당 키가 있는지 없는지를 나타내는 Entry
열거형을 반환한다. Entry
의 or_insert
메서드는 키가 존재하면 연관된 값을 반환하고, 그렇지 않으면 제공된 값을 새 값으로 삽입한 후 수정된 Entry
에 대한 값을 반환한다.
'프로그래밍 > Rust' 카테고리의 다른 글
[Rust] 제네릭과 트레이트 (0) | 2024.05.19 |
---|---|
[Rust] 오류 처리 (0) | 2024.05.18 |
[Rust]프로젝트 모듈 관리 (0) | 2024.05.17 |
[Rust] 열거형 (0) | 2024.05.17 |
[Rust] 구조체 (0) | 2024.05.16 |