해당 게시판에 올라오는 포스트는 The Rust Programming Language 의 한글 텍스트 버전을 보며 작성된 학습 게시판입니다.
전체 목차
[코딩/Rust] - [Rust] 1장 안정성 높은 언어
[코딩/Rust] - [Rust] 2장 일반적인 프로그래밍 개념
[코딩/Rust] - [Rust] 3장 러스트만의 특이함
[코딩/Rust] - [Rust] 4장 열거형(Enum), 모듈화(Module)
[코딩/Rust] - [Rust] 5장 컬렉션(Collection) - 벡터(Vector), 문자열(String), 해시맵(HashMap)
글 목차
1. 벡터(Vector)
배열(array), 리스트(List) 등 과 같이 서로 이웃하도록 메모리에 단일 데이터 구조로 저장하는 타입니다.
벡터 생성
Vec::new 함수를 사용하려면 타입을 명시(type annotation)해야한다.
let v: Vec<i32> = Vec::new();
만약 생성과 동시에 초기화를 하고 싶다면, 다음과 같이 vec! 메크로를 사용하도록 하자. 안에 들어가는 요새에 맞게 자동으로 타입을 지정해준다.
let v = vec![1, 2, 3];
벡터 업데이트
만약에 생성한 벡터에 요소를 추가하기 위해서는 반드시 가변(mutable) 설정을 해야한다.
let mut v = Vec::new();
v.push(5);
v.push(6);
v.push(7);
v.push(8);
여기서도 타입 명시를 하지 않았는 데, 집어 넣은 숫자가 모두 i32 타입인 점을 통해서 러스트가 자동으로 v 의 타입을 추론하기 때문이다.
벡터 요소 접근
인덱싱으로 접근하는 방법과 get 메소드를 사용하는 방법이 있다.
let v = vec![1, 2, 3, 4, 5];
let third: &i32 = &v[2];
println!("세번째 요소는 {}이다.",third);
let third: Option<&i32> = v.get(2);
match third {
Some(third) => println!("세번째 요소는 {}이다.",third),
None => println!("악! 패닉! 세번째 요소는 없어!"),
}
인덱스를 통해서 접근한다면 요소의 참조자를 얻게 되며, get 함수를 통해서 접근한다면 Option<&T> 즉, Null 체크를 한 결과를 얻게 된다.
인덱스를 통한 접근의 문제는 벡터 범위를 벗어난 인덱스가 주어지면 패닉을 일으킨다. 존재하지 않는 요소를 참조하기 때문이다.
다만 get 함수에서 벡터 범위를 벗어난 인덱스가 주어지면 패닉 없이 None 이 반환되어, 일반적인 상황에서 벡터의 범위 밖에 있는 요소에 접근하는 일이 종종 발생할 수 도 있다면 이 방법을 사용할 만 하다.
벡터 요소 접근 중 가변 문제
불변과 가변 항목에서 설명했다 싶이, 당연하게도 불변 참조자를 얻게 된 상태에서는 가변 참조자를 사용할 수 없다.
아래 코드는 컴파일 에러가 발생한다.
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // 불변 참조자 획득
v.push(6); // 불변 참조자가 생긴 이후로는 가변 참조자를 통해 변수를 수정할 수 없다.
println!("악! 패닉! 패닉! 소유권 및 대여 규칙 위반!")
벡터 요소 반복
당연하게도 for 문에서 벡터를 활용할 수 있다. 벡터 내의 각 요소를 차례대로 접근할 때에 인덱스 대신 요소에 대한 반복으로 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; // 역참조 연산자 '*'
}
역참조 연산자는 대충 참조자가 가지고 있는 값을 가져온다는 뜻으로 생각하면 된다.
에이 C 언어 했으면 알겠지 이정도는
벡터에 열거형 쓰기
벡터는 같은 타입을 저장할 수 있는 데, 열거형에서 정의된 배리언트들은 같은 타입으로 간주된다.
enum Cell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
Cell::Int(3),
Cell::Text(String::from("blue")),
Cell::Float(10.12),
];
다양한 타입을 담을 수 있으나, 굳이 이렇게 까지 할 필요가 있나 싶기는 하다.
벡터의 라이프타임
당연히 스코프를 벗어나면 벡터 내에있는 요소들 또한 전부 해제된다.
{
let v = vec![1, 2, 3, 4];
// v를 가지고 작업하기
} // <- 여기서 v가 스코프 밖으로 벗어나고 해제된다
2. 문자열(String)
기본적인 러스트의 문자열 인코딩은 UTF-8 이다. 문자열의 뜻은 문자를 요소로 가진 배열 이다. 여기서의 String 은 구조체 래퍼(struct wrapper) 로서 대충 JAVA 의 String Class 라고 생각하면 된다.
문자열 생성
let mut s = String::new();
해당 코드와 같이 비어있는 새로운 String 을 생성할 수 있다. 만약 변경 불가능한 &str 타입의 문자열 리터럴 또한 String 으로 변경할 수 있다.
let data = "아따 나는 수정 불가라고";
let s = data.to_string();
// 이 메서드는 리터럴에서도 바로 작동합니다:
let s = "나도 수정 불가하지".to_string();
혹은 다음과 같은 코드를 활용해 바로 문자열 리터럴로 String 을 생성할 수 있다.
let s = String::from("나도 수정은 못하지");
문자열 업데이트
String 은 + 연산자나 format! 매크로를 사용하여 편리하게 값들을 이어붙일 수 있다.
push_str 과 push
let mut s = String::from("후");
s.push_str("하");
// s == 후하
let mut s = String::from("lo");
s.push('l');
// s == lol
+ 연산자 와 format! 매크로
둘 다 정상 작동 한다.
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = s1 + "-" + &s2 + "-" + &s3;
let s1 = String::from("tic");
let s2 = String::from("tac");
let s3 = String::from("toe");
let s = format!("{s1}-{s2}-{s3}");
문자열 요소 접근
결론부터 말하자면 불가능하다.
정말로 인덱스로 접근하여 아스키 문자를 보고 싶다면 Vec<u8> 에 직접 하나하나 문자를 집어넣도록 하자.
아니면 as_bytes 라는 함수를 통해 문자열을 바이트 배열로 변경할 수 도 있다.
유니코드 문자를 인덱스로 접근하고 싶다면 Vec<char> 라던지 as_chars 라는 함수라던지 여러 대체 방법이 있다.
대부분의 많은 프로그래밍 언어에서, 인덱스를 이용한 참조를 통해 문자열 내부의 개별 문자에 접근하는 것은 유효하고 범용적인 연산에 속한다. 그런데 러스트에서는 String 의 부분에 인덱싱을 사용하게 되면 컴파일 에러를 발생시킨다.
그 이유는 인덱스 연산이 언제나 상수 시간(O(1))에 실행 될 것으로 기대받기 때문이다. 러스트에서는 String 을 가지고 그러한 성능을 보장하는 것이 불가능한데, 문자열 내에 유효한 문자가 몇 개 있는 지 알아내기 위해 내용물을 시작 지점부터 인덱스로 지정된 곳까지 훑어야 하기 때문이다.
문자열 슬라이싱
특정 바이트들이 담고 있는 문자열 슬라이스는 만들 수 있다. 하지만 범위를 지정하여 문자열 슬라이스를 생성하는 것은 프로그램을 죽게 할 수 도 있으니 주의 깊게 사용해야 한다.
let hello = "안녕하십니까";
let s = &hello[0..4]; // [include..exclude]
여기서 s 는 문자열의 첫 4바이트를 담고있는 &str 가 된다. 한국어는 유니코드로 되어있기 때문에 이 글자들이 각각 2바이트를 차지한다는 것을 알고 있으므로, s 가 '안녕' 이라는 문자열을 가질 것 이다.
만약에 &hello[0..1] 과 같이 문자 바이트 일부를 슬라이스하려고 한다면, 러스트는 벡터 내에 유효하지 않은 인덱스에 접근했을 때와 동일한 방식으로 패닉을 발생시킨다.
문자열 요소 반복
대충 두 가지 방법을 많이 사용한다고 생각하면 된다.
for c in "안녕".chars() {
println!("{c}");
}
// 출력 결과
// 안
// 녕
for b in "안녕".bytes() {
println!("{b}");
}
// 출력 결과
// 236
// 149
// 136
// 235
// 133
// 149
3. 해시맵(HashMap)
키와 값을 저장하는 딕셔너리(Dictionary) 타입이라고 생각하자. 거기다 해시 함수(hashing function)를 사용하여 매핑한 것을 저장한다.
해시맵 생성
use std::collections::HashMap;
let mut scores:HashMap<String, i32> = HashMap::new();
해시맵 업데이트
해시맵과 소유권
값의 소유권이 해시맵으로 넘어간다.
use std::collections::HashMap;
let field_name = String::from("좋아하는 색상");
let field_value = String::from("파랑");
let mut map = HashMap::new();
map.insert(field_name, field_value);
// field_name과 field_value는 이 시점부터 유효하지 않는 다.
값을 덮어쓰기
아래 코드는 {"파랑팀":25} 를 출력할 것이다. 원래 값 10은 덮어써졌다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("파랑팀"), 10);
scores.insert(String::from("파랑팀"), 25);
println!("{:?}", scores);
키가 없을 때만 키와 값 추가하기
아래 코드는 {"노랑팀": 50, "파랑팀": 10} 를 출력할 것이다.
Entry 의 or_insert 메소드는 해당 키가 존재할 경우 Entry 키에 대한 연관된 값을 반환하도록 정의되어 있고, 그렇지 않은 경우 매개변수로 제공된 값을 해당 키에 대한 새 값으로 삽입하고 수정된 Entry에 대한 값을 반환한다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("파랑팀"), 10);
scores.entry(String::from("노랑팀")).or_insert(50);
scores.entry(String::from("파랑팀")).or_insert(50);
println!("{:?}", scores);
예전 값에 기초하여 값을 업데이트 하기
해시맵에 대한 또 다른 일반적인 사용 방식은 키에 대한 값을 찾아서 예전 값에 기초하여 값을 업데이트 하는 것이다.
예를 들어, 아래의 코드는 어떤 텍스트 내에 각 단어가 몇 번이나 나왔는 지를 세는 코드를 보여준다. 단어를 키로 사용하는 해시맵을 이용하여 해당 단어가 몇 번이나 나왔는 지 추적하기 위해 값을 증가 시켜준다. 처음 본 단어라면, 값 0을 삽입할 것이다.
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);
이 코드는 {"world": 2, "hello": 1, "wonderful": 1} 을 출력할 것이다.
해시맵은 반복의 처리가 임의의 순서로 일어나기 때문에 키/값 쌍의 출력 순서가 다를 수 도 있다는 것을 명심하자.
해시맵 요소 접근
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("파랑팀"), 10);
scores.insert(String::from("노랑팀"), 50);
let team_name = String::from("파랑팀");
let score = scores.get(&team_name).copied().unwrap_or(0);
get 함수는 Option<&i32> 를 반환한다. 만일 이 해시맵에 해당 키에 대한 값이 없다면 get 은 None 을 반환할 것이다. 이 프로그램에서는 copied 를 호출하여 Option<&i32> 가 아닌 Option<i32> 를 얻어온 다음, unwrap_or 를 사용하여 scroes 가 해당 키에 대한 아이템을 가지고 있지 않을 경우 score 에 0 을 설정하도록 처리 했다.
해시맵 요소 반복
벡터에서와 유사한 방식으로 for 루프를 사용해서 해시맵 내의 키/값 쌍에 대한 반복 작업을 수행할 수 있다.
use std::collections::HashMap;
let mut scores = HashMap::new();
scores.insert(String::from("파랑팀"), 10);
scores.insert(String::from("노랑팀"), 50);
for (key, value) in &scores {
println!("{key}: {value}");
}
'코딩 > Rust' 카테고리의 다른 글
[Rust] 4장 열거형(Enum), 모듈화(Module) (1) | 2024.03.15 |
---|---|
[Rust] 3장 러스트만의 특이함 (1) | 2024.03.08 |
[Rust] 2장 일반적인 프로그래밍 개념 (1) | 2024.03.06 |
[Rust] 1장 안정성 높은 언어 (2) | 2024.03.05 |