JS, TS랑 비교하며 Rust 공부하기 (5)
컬렉션
러스트의 표준 라이브러리인 컬렉션에는 벡터, 문자열, 해시맵과 같은 데이터 구조들이 포함되어 있으며, 이 컬렉션들이 가리키고 있는 데이터들은 힙에 저장된다.
벡터
벡터는 메모리에 모든 값을 서로 이웃하도록 배치하는 단일 데이터 구조이며 하나 이상의 값을 저장할 수 있다. TS에서 하나의 타입으로 선언한 배열과 유사하다.
벡터를 만드는 방법은 new
함수를 호출하거나 vec!
매크로를 사용하면 된다.
let v: Vec<i32> = Vec::new(); // 타입 명시 필수
let v = vec![1, 2, 3];
push
메서드를 사용해서 벡터에 요소를 추가할 수 있다.
let mut v = Vec::new();
v.push(5); // 타입 추론을 해준다.
v.push(6);
v.push(7);
v.push(8);
벡터의 요소를 참조하는 방법은 인덱싱과 get
메서드가 있다.
&
와 []
를 사용하면 인덱스 값에 위치한 요소의 참조자를 얻게 된다.
get
메서드에 인덱스를 인수로 넘기면, Option<&T>
를 얻게 된다.
참조가 아닌 방법으로 값을 가져오면 이동이 발생할 수도 있기 때문에 주의해야 한다.
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."),
}
벡터에 새로운 요소를 추가할 공간이 없다면, 넉넉한 메모리를 새로 할당하고 기존 요소를 새로 할당한 공간에 복사한다. 그렇기 때문에 벡터의 참조자를 가지고 있는 상태에서 새로운 요소를 추가하면 에러가 발생한다.
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0];
v.push(6); // error
println!("The first element is: {first}");
for
문을 사용하여 벡터 값의 모든 요소에 대한 반복 처리를 할 수 있다.
let v = vec![100, 32, 57];
for i in &v {
println!("{i}");
}
let mut v = vec![100, 32, 57];
for i in &mut v {
*i += 50;
}
함수 시그니처에 벡터의 타입은 Vec<타입>
으로 표시하며 참조해서 가져올 경우엔 &[타입]
으로 나타내면 된다.
fn largest(list: &[i32]) -> &i32 {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
벡터 내에 다른 타입의 값들을 저장할 필요가 있다면 열거형을 사용하면 된다.
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
문자열
러스트에서 문자열 타입은 str
이다. str
은 UTF-8로 인코딩된 문자열 데이터로, 크기를 컴파일 타임에 알 수 없는 동적 크기 타입이다. 일반적으로는 &str
처럼 참조 형태로 사용되며, 이를 문자열 슬라이스라고 부른다. 이는 어딘가에 저장된 문자열 데이터를 가리키는 읽기 전용 참조이다. JS의 문자열은 UTF-16 기반이므로 인코딩 단위에서 차이가 있다.
반면, String
은 표준 라이브러리에서 제공하는 구조체로, str
데이터를 힙에 저장하고 소유권을 갖고 있으며 가변적으로 다룰 수 있도록 만든 래퍼 타입이다. 내부적으로는 Vec<u8>
을 기반으로 하며, 문자열로서의 유효성을 보장하고 다양한 문자열 조작 기능을 제공한다. JS의 String 객체와 유사하다.
String::new
함수, to_string
메서드, String::from
함수를 통해 String
을 생성할 수 있다.
let mut s = String::new();
let s = "initial contents".to_string();
let s = String::from("initial contents");
push_str
메서드의 매개변수는 문자열 슬라이스이며 문자열을 추가할 때 사용한다.
let mut s1 = String::from("foo");
let s2 = "bar";
s1.push_str(s2);
println!("s2 is {s2}"); // s2 is bar
push
메서드의 매개변수는 문자이며 String
에 추가한다.
let mut s = String::from("lo");
s.push('l');
+
연산자를 사용해 두 개의 문자열을 조합할 수 있지만 JS처럼 사용할 수 없다.
+
연산자는 add
메서드를 사용하며 fn add(self, s: &str) -> String
와 같은 시그니처를 갖는다.
러스트의 역참조 강제 변환을 통해 &String
인수가 &str
로 강제되기 때문에, &String
을 add
호출에 사용할 수 있다.
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2; // s1은 여기로 이동되어 더 이상 사용 불가
복잡한 문자열 조합에는 format!
매크로를 사용하면 된다.
format!
매크로는 참조자를 이용하므로 인수의 소유권을 가져가지 않고 String
을 반환한다.
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
format!
매크로는 숫자를 특정 형식으로 문자열 변환이 필요할 때 사용할 수 있다.
let number = 42;
let binary = format!("{:b}", number); // 2진수로 변환
println!("{} is {}", number, binary); // 42 is 101010
JS와는 다르게 러스트는 인덱스를 이용한 문자열 참조를 허용하지 않는다.
러스트는 문자열을 바이트, 스칼라 값, 문자소 클러스터로 나눠서 접근하기 때문에 문자열의 바이트 안의 인덱스는 유효한 유니코드 스칼라 값과 항상 대응되지 않는다. 그렇기 때문에 범위를 지정하여 문자열 슬라이스를 생성하는 것도 프로그램을 죽게 만들 수도 있기 때문에 주의해야 한다.
let s1 = String::from("hello");
let h = s1[0]; // error
문자열 조각에 대한 연산을 하는 가장 좋은 방법은 명시적으로 문자를 원하는 것인지 아니면 바이트를 원하는 것인지 지정하는 것이다.
for c in "Зд".chars() {
println!("{c}");
}
for b in "Зд".bytes() {
println!("{b}");
}
해시맵
러스트의 해시맵은 삽입 순서를 보장하지 않으며, 모든 키는 같은 타입이고 모든 값도 같은 타입이어야 한다. JS의 Map
은 삽입 순서를 보장한다는 점에서 차이가 있다.
해시맵은 자동 목록(프렐루드)에 포함되어 있지 않아서 use
로 불러와야 한다.
new
함수를 사용하여 빈 해시맵을 생성한 뒤 insert
메서드를 이용하여 요소를 추가한다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
from
함수를 사용하여 초기값을 줄 수 있다.
let map = HashMap::from([('a', 1), ('b', 2)]);
get
메서드의 인수에 키값의 참조자를 할당하면 Option<&V>
를 반환한다.
해시맵에 해당 키에 대한 값이 없다면 None
을 반환한다.
let team_name = String::from("Blue");
// copied 메서드를 통해 참조자가 아닌 실제 값을 가져온다
// unwrap_or 메서드를 통해 None일 경우 0을 값으로 가져온다
let score = scores.get(&team_name).copied().unwrap_or(0);
for
문을 사용하여 해시맵 내의 키/값 쌍에 대한 반복 작업을 수행할 수 있다.
해시맵에 대한 반복 처리는 임의의 순서로 일어난다.
for (key, value) in &scores {
println!("{key}: {value}");
}
소유권이 있는 값은 해시맵의 키와 값으로 사용될 때 이동된다.
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
값을 덮어쓰고 싶으면 같은 키에 다른 값을 삽입하면 된다.
map.insert(String::from("foo"), 0);
키가 없을 때만 키와 값을 추가하고 싶다면, Entry
열거형의 or_insert
메서드를 사용하면 된다.
entry
메서드의 반환 값인 Entry
를 통해 해당 키가 있는지 혹은 없는지를 알 수 있다.
map.entry(String::from("foo")).or_insert(50);
키에 대한 값을 찾아서 이전 값을 업데이트하고 싶다면, 역참조를 통해 해당 값을 가져와 갱신해야 한다. 혹은 and_modify
메서드를 사용하면 된다.
let text = "hello world wonderful world";
let mut map = HashMap::new();
for word in text.split_whitespace() {
let count = map.entry(word).or_insert(0); // 가변 참조자 반환
*count += 1;
// map.entry(word).and_modify(|count| *count += 1).or_insert(0);
}