javascript를 사용하며 가장 많이 사용하는 데이터 구조는 object literal, map, set, array 입니다. 그리고 object literal을 array로 바꾸거나 map을 Iteration으로 바꾸거나 하는 작업을 매우 능숙하게 할 수 있다면 데이터를 다루는데 있어 큰 어려움을 겪지 않습니다.
오늘 배울 러스트의 collections는 standard library에 포함된 데이터 구조들입니다.
벡터는 연속적으로 같은 타입의 값을 저장할 수 있는 콜렉션 타입입니다.
pub struct Vec<T, A = Global>
where
A: Allocator,
{ /* private fields */ }
벡터 타입을 선언할 때 초기 값을 함께 사용하지 않으면 type annotation 을 사용해 :Vec<32>
와 같이 어떤 타입의 값을 벡터가 가지고 있을지 러스트 컴파일러에게 알려줘야 합니다.
use std::vec::Vec;
fn main() {
let v: Vec<i32> = Vec::new();
}
러스트 컴파일러에게 타입을 추론하게 할 수도 있습니다. 다음 벡터 타입 선언을 보세요.
let v = vec![1, 2, 3];
i32 타입의 값들을 이용해 패턴 바인딩 했기 때문에 러스트는 v가 Vec<i32>
라는 사실을 잘 추론할 수 있습니다.
벡터 타입 v를 업데이트 하려면 mut
키워드를 사용해 변수를 선언한 뒤 push
메소드를 사용해야 합니다.
fn main() {
let mut v: Vec<i32> = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
}
아래 코드에서는 get 메소드를 이용해 벡터의 세번째 엘리먼트의 값을 가져왔습니다.
&v[2]
를 보면 &
와 []
문법을 통해 엘리먼트의 reference를 가져올 수 있다는 사실을 알 수 있습니다.
코드를 한번 읽어보신 후에 get 메소드에 대해 이야기해보겠습니다.
use std::vec::Vec;
fn main() {
let v = vec![1, 2, 3, 4, 5];
let third = &v[2];
println!("the third element is {}", third);
let third = v.get(2);
match third {
Some(third) => println!("The third element is {third}"),
None => println!("There is no third element")
}
}
get
메소드를 사용하면 Option<&T>
타입을 얻을 수 있고 패턴 매칭을 이용할 수 있습니다.
러스트에서 인덱싱 참조와 get 메소드 두 가지 방법을 제공하는 이유는 범위 외부의
값을 조회할 때 프로그램이 어떻게 행동할지에 대한 선택지를 주기 위해서입니다.
fn main() {
let v = vec![1, 2, 3, 4, 5];
let does_not_exist = &v[100];
let does_not_exist = v.get(100);
}
첫 does_not_exist
와 같이 값을 참조할 시에 panic이 발생합니다.
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 100', src/main.rs:6:27
get 메소드를 사용해 out of bound 참조를 하게 되면 None
을 리턴 하고 panic이 발생하지 않습니다.
사용자에게 값을 입력받아 벡터 엘리먼트를 참조한다고 했을 때 index 참조를 사용하게 되면 프로그램에 에러가 발생할 수 있겠죠?
값을 삭제하기 위해서는 remove
, 없앤 값을 expression으로 사용하기 위해서는 swap_remove
를 사용합니다.
두 메소드 모두 벡터의 마지막 엘리먼트를 지울 때는 O(1) 시간복잡도를, 다른 위치의 엘리먼트를 삭제할 때는 O(n)의 시간복잡도를 가집니다.
또한 여러 개의 값을 삭제하기 위해서는 drain
메소드를 사용합니다.
fn main() {
let mut numbers = vec![1,2,3,4,5];
numbers.remove(1);
assert_eq!(numbers, [1,3,4,5]);
let removed_element = numbers.swap_remove(1);
let removed_elements: Vec<i32> = numbers.drain(1..3).collect();
println!("{}", removed_element);
}
아래 코드에서 for loop를 사용해 i32 타입의 엘리먼트의 immutable reference를 가져오고 있습니다.
fn main() {
let v = vec![100, 32, 57];
for i in &v {
println!("{i}")
}
}
다음과 같이 mutable vector를 순회하며 모든 element를 변경할 수도 있습니다.
아래에서 사용한 코드의 *
는 dereference operator로 += 연산을 하기 전 i의 value를 가져오기 위함입니다.
fn main() {
let mut v = vec![1000, 32, 57];
for i in &mut v {
*i += 50;
}
}
벡터에 항상 같은 타입의 값을 저장해야 한다는 사실은 벡터를 다루는데 불편한 점을 가져다줍니다. 우리는 이것을 enum을 사용함으로 해결할 수 있습니다.
fn main() {
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String)
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
}
가능한 한 많은 오류를 노춣하려는 러스트의 경향, 다른 프로그래밍 언어에 비해 복잡한 데이터 구조, UTF-8은 러스트를 처음 공부하는 많은 사람들이 String을 어려워하는 이유입니다.
String은 byte들의 collection이며 각 바이트가 text로 해석되었을 때 함께 사용할 수 있는 유용한 기능들을 제공하는 타입입니다.
러스트에서 String literal ("hello world"
와 같은)는 string slice 타입, 즉 &str
를 의미하며 러스트에서 작성하는 문자열은 모두 &str
입니다. 그리고 read only하고 immutable 합니다.
String
타입은 러스트의 standard library에 의해 제공되며 mutable하고 growable 합니다. 그리고 UTF-8 encoded 한 값입니다.
let hello = String::from("hello");
let mut hello = String::new();
hello.push_str("he");
hello.push('l');
hello.push('l');
hello.push('o');
아래에서 String::from()
와 to_string
메소드는 동일한 역할을 합니다.
let s = String::from("initial contents);
let data = "initial contents";
let s = data.to_string();
string은 UTF-8 인코딩이기 때문에 아래 의 값은 모두 적합한 값입니다.
let hello = String::from("السلام عليكم");
let hello = String::from("Dobrý den");
let hello = String::from("Hello");
let hello = String::from("שָׁלוֹם");
let hello = String::from("नमस्ते");
let hello = String::from("こんにちは");
let hello = String::from("안녕하세요");
let hello = String::from("你好");
let hello = String::from("Olá");
let hello = String::from("Здравствуйте");
let hello = String::from("Hola");
mutable한 String 타입을 이용합니다.
String 타입은 Vec<T>
벡터 타입과 동일하게 사이즈가 늘어날 수 있고 값이 변경될 수 있습니다.
let mut s = String::from("foo");
s.push_str("bar");
또환 +
, format!
macro를 사용해 String 값을 붙일 수있습니다.
let s1 = String::from("hello");
let s2 = String::from("world");
// Concatenate two strings using the + operator
let s3 = s1 + &s2;
// Use the format! macro to concatenate strings
let s4 = format!("{}-{}", s1, s2);
+
operator는 add 메소드를 사용합니다.
이 메소드의 시그니처는 다음과 유사합니다.
fn add(self, s: &str) -> String {
standard library에서 add 메소드는 generic,그리고 associated type을 사용합니다.
생각이 안날 수 있어 빨리 정리해보겠습니다.
associated type은 trait 선언 시 사용하는 타입 플레이스 홀더입니다.
trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
실제 Iterator를 구현할 때 아래와 같이 Item 타입을 i32로 구체화 시켜 선언했습니다.
struct MyIterator {
// implementation details go here
}
impl Iterator for MyIterator {
type Item = i32;
fn next(&mut self) -> Option<i32> {
// implementation goes here
}
}
s2
는 &
를 사용했는데요, second string의 reference를 first string에 더한다는 뜻입니다. add
함수의 s argument는 &str 이기 때문에 String value를 넘기는게 아니라 &str를 넘겨야 합니다. 근데 &s2는 &String인데 어떻게 이 연산이 가능할까요?
argument 타입이 &str
임에도 불구하고 &String
타입을 넘길 수 있는 이유는 add 함수를 호출할 때 러스트 컴파일러는 deref coercion
을 사용하기 때문입니다.
deref coercion은 reference와 smart pointer 간의 자동 형변환을 의미하며 러스트 고유의 언어 특성입니다.
특정 값이 reference나 smart pointer이고 이 값을 특정 함수의 reference / smart pointer 타입의 argument로 넘겨야 할때 이러한 자동 형변환이 가능하게 됩니다.
deref coercion은 Deref trait의 deref 메소드를 자동으로 호출해 값을 reference로 변경하거나 DerefMut trait의 deref_mut
메소드를 호출해 값을 mutable reference로 변경합니다.
다음 예제는 Box<T>
값을 함수의 &T reference로 넘기는 것을 보여줍니다.
use std::ops::Deref;
fn foo(x: &i32) {
// implementation goes here
}
let x = Box::new(5);
foo(&x);
러스트 컴파일러는 자동으로 Deref::deref
메소드에 Box<i32>
값을 사용해 &i32 reference 타입으로 자동 형변환합니다.
아래 예제는 mutable references와 smart pointer간의 형변환을 보여줍니다.
use std::ops::{Deref, DerefMut};
fn bar(x: &mut i32) {
// implementation goes here
}
let mut x = Box::new(5);
bar(&mut x);
&mut x 는 Box<i32>
를 가르키는데, 함수 bar의 x argument 타입은 &mut i32 타입이 들어오는 것을 기대하고 있습니다. 러스트 컴파일러는 DerefMut::deref_mut
메소드를 사용해 Box<i32>
를 &mut i32 reference로 변경합니다.
String 타입을 indexing하는 것은 불가능합니다.
let s1 = String::from("hello");
let h = s1[0];
$ cargo run
Compiling collections v0.1.0 (file:///projects/collections)
error[E0277]: the type `String` cannot be indexed by `{integer}`
--> src/main.rs:3:13
|
3 | let h = s1[0];
| ^^^^^ `String` cannot be indexed by `{integer}`
|
= help: the trait `Index<{integer}>` is not implemented for `String`
= help: the following other types implement trait `Index<Idx>`:
<String as Index<RangeFrom<usize>>>
<String as Index<RangeFull>>
<String as Index<RangeInclusive<usize>>>
<String as Index<RangeTo<usize>>>
<String as Index<RangeToInclusive<usize>>>
<String as Index<std::ops::Range<usize>>>
<str as Index<I>>
For more information about this error, try `rustc --explain E0277`.
error: could not compile `collections` due to previous error
String 타입은 내부적으로 Vec<u8>
타입으로 wrapping 되어 있습니다.
아래 경우에서 "Hola"는 4 바이트 이기 때문에 len은 4입니다.
let hello = String::from("Hola");
하지만 아래에서 글자는 12자임에도 불구하고 Rust는 아래의 len을 24라고 해석합니다.
왜냐하면 "Здравствуйте"
를 UTF-8로 인코딩 했을 때의 총 바이트 수가 24이기 때문입니다. 각 unicode scalar 값이 각 글자 당 2바이트씩 차지하기 때문이죠. 따라서 string의 인덱싱이 유효한 UTF-8 인코딩 값을 가르킨다고 보장할 수 없습니다.
let hello = String::from("Здравствуйте");
다음 코드에서 answer은 3이 아닙니다. 글자가 UTF-8로 인코딩 되었을 떄 3의 첫 바이트는 200
이며 두번쨰 바이트는 151
입니다. 따라서 answer은 200
이어야 하나 그자체로 유효한 문자열이 아닙니다.
let hello = "Здравствуйте";
let answer = &hello[0];
러스트 컴파일러는 이러한 상황을 사전에 방지해 버그를 예방하게 하기 위해 이런 인덱싱을 허용하지 않습니다.
String 타입은 mutable하고 string slice인 &str은 immutable합니다.
strint slice를 변경하고 싶으면 String 타입으로 먼저 변경하거나 (to_string) 새로운 String 값을 선언해서 사용해야 합니다.
다음 코드를 보면 string 타입을 순회하는 것을 볼 수 있습니다.
// Create a new, empty String
let mut s = String::new();
// Create a String from a string slice
let s1 = String::from("hello");
// Concatenate two strings using the + operator
let s2 = String::from("world");
let s3 = s1 + &s2;
// Use the format! macro to concatenate strings
let s4 = format!("{}-{}", s1, s2);
// Push characters onto an existing String
s.push('h');
s.push_str("ello");
// Access the characters in a String using indexing
let c = s[0];
// Get a string slice from a String using slicing syntax
let slice = &s[0..2];
// Iterate over the characters in a String
for c in s.chars() {
println!("{}", c);
}
for b in "Зд".bytes() {
println!("{b}");
// 208
// 151
// 208
// 180
}
보시듯이 러스트의 String 타입은 생각보다 간단하지 않습니다. 다른 언어와 다르게 러스트 컴파일러는 String 데이터를 다룸으로 일어날 수 있는 가능한 버그와 에러를 최소화 하기 위해 String 데이터를 다루는데 있어 복잡한 규칙을 추가했습니다 .
마지막 콜렉션 타입은 해쉬맵입니다.
HashMap<K ,V>
은 키 타입 K
와 값 타입 V
를 매핑한 것으로 hashing function
을 사용합니다. 타입스크립트의 Record<K,V
와 유사한 형태죠.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
해쉬 맵은 Vector 타입과 다르게 간편하게 사용할 수 있는 매크로가 제공되지 않습니다.
다만 벡터와 마찬가지로 heap에 데이터가 저장됩니다. 위의 예제에서 해쉬맵은 String 타입 키와 i32 타입의 값으로 이뤄져 있습니다. 벡터와 마찬가지로 동일 타입의 키/값만 가질 수 있습니다.
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);
위의 코드에서 score는 Blue 팀에 대한 스코어 값을 가질 수 있습니다. get 메소드는 Option<&V>
타입이므로 조회하는 키 값이 HashMap에 존재하지 않는다면 None을 리턴합니다.
.copied()
메소드 호출을 통해 Option<&i32>
대신 Option<i32>
값을 가져올 수 있고 unwrap_or
메소드 호출을 통해 값이 없으면 0이라는 값을 패턴바인딩했습니다.
각 키/밸류를 다음 처럼 순회할 수도 있습니다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
for (key, value) in &scores {
println!("{}: {}", key, value);
}
// Yellow: 50
// Blue: 10
Copy trait을 구현한 i32
와 같은 타입에 대해서 해쉬맵에 매핑되는 각 값들은 복사되어 해쉬맵에 매핑됩니다. String 값과 같이 힙에 저장되고 누군가에게 소유되는 값들 Owned values
copy가 아니라 move
되어 해쉬맵이 해당 String 값에 대한 오너쉽을 가집니다.
use std::collections::HashMap;
fn main() {
let field_name = String::from("Favorite color");
let field_value = String::from("Blue");
let mut map = HashMap::new();
map.insert(field_name, field_value);
println!("String: {}", field_name);
}
field_name이 moved 된 이후에 borrowed 되었기 때문에 위 코드에서의 println!
매크로 문은 러스트 컴파일러에 의해 에러로 표기 됩니다.
error[E0382]: borrow of moved value: `field_name`
--> src/main.rs:10:28
|
4 | let field_name = String::from("Favorite color");
| ---------- move occurs because `field_name` has type `String`, which does not implement the `Copy` trait
...
8 | map.insert(field_name, field_value);
| ---------- value moved here
9 |
10 | println!("String: {}", field_name);
| ^^^^^^^^^^ value borrowed here after move
|
= note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0382`.
해쉬맵에는 중복 키 값을 허용하지 않으므로 특정 키에 대한 write 연산이 시도될 때 기존 값을 덮어씌울지 아니면 새로운 value를 무시할지 정의해야 합니다.
중복 키값에 대한 insert 메소드를 여러 번 호출하면 다음 처럼 해당 키 값이 덮어씌워집니다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Blue"), 25);
println!("{:?}", scores);
위의 예제에서 코드는 {"Blue": 25}
로 출력됩니다.
값 10이 25로 덮어씌여진 것입니다.
러스트의 해쉬맵은 entry
라고 하는 특별한 api를 제공합니다. 키 값을 검사하여 해당 값이 이미 해쉬맵에 존재하는지 아닌지를 Entry 타입으로 표현하는 메소드입니다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("Blue"), 10);
scores.entry(String::from("Yellow")).or_insert(50);
scores.entry(String::from("Blue")).or_insert(50);
println!("{:?}", scores);
// {"Blue": 10, "Yellow": 50}
or_insert
메소드의 리턴 타입인 Entry
타입은 mutable한 reference 이며 Entry key가 존재하는지 안하는지에 대한 값을 가지고 있습니다.
다음 코드는 특정 조건에 따라 값을 업데이트 하는 로직을 보여줍니다.
use std::collections::HashMap;
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;
}
println!("{:?}", map);
or_insert
메소드는 mutable reference &mut V
타입을 리턴하며 해당 reference에 값을 할당하기 위해서는 dereference *
를 먼저 사용해야 합니다.
해쉬 맵은 별도 설정 없이 기본적으로 SipHash
라는 DDos 공격을 방지할 수 있는 함수를 사용합니다.
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use std::hash::SipHasher;
fn main() {
// Create a new HashMap with a custom hasher
let mut map: HashMap<&str, i32, BuildHasherDefault<SipHasher>> = Default::default();
// Insert some key-value pairs into the map
map.insert("red", 1);
map.insert("green", 2);
map.insert("blue", 3);
// Look up the value associated with a key
let color = map.get("red");
println!("The value for red is {}", color.unwrap());
// The value for red is 1
}
오늘은 Vector, String, HashMap 타입에 대해 알아보았습니다. 수고 많으셨습니다!