Rust 정리 (2)
2021.02.20
Packages / Crates / Modules
- Packages: A Cargo feature that lets you build, test, and share crates
- Crates: A tree of modules that produces a library or executable
- Modules and use: Let you control the organization, scope, and privacy of paths
- Paths: A way of naming an item, such as a struct, function, or module
진입점
Cargo
는 패키지 디렉토리에src/main.rs
혹은src/lib.rs
가 포함된 여부에 따라 진입점을 선정해서 rustc를 통해 build 하도록 하는 convention을 따른다.- 만약 두 개 파일이 다 있다면, 두 개의 바이너리 파일이 생성된다.
- 파일을
src/bin
안에 만들어서 여러개의 바이너리 crate를 만들 수 있다.
모듈 정의
mod module_name {
mod sub_module_1_name {
fn function_1_name() {}
fn function_2_name() {}
// fn 뿐만 아니라, struct, enum, constant, trait 등등 가능
}
mod sub_module_2_name {
fn function_1_name() {}
}
}
pub fn rust_function() {
// absolute path
crate::module_name::sub_module_1_name::function_1_name();
// relative path
module_name::sub_module_1_name::function_1_name();
}
절대경로 / 상대경로 를 사용하는 느낌으로 모듈을 참조할 수 있다.
- 절대경로 :
crate
로 시작 - 상대경로 :
self
,super
로 시작 ::
를 사용하여 경로를 타고 내려감.
- 절대경로 :
module_name
과rust_function
이 같은 수준의 위치에 있기 때문에, relative path 사용 예시처럼 사용 가능하지만, 위 상태에서는
function_1_name
을 실행할 수 없다.rust에서의 privacy는 기본적으로 private 이다. 부모 모듈은 private한 자식 모듈에 접근할 수 없다.
자식은 부모 모듈로 접근 가능하다.
모듈 앞에
pub
키워드를 앞에 붙여서 public하게 만들 수 있다.
super
를 사용한 relative 접근
fn check() {} // 1
mod module {
fn check() {} // 2
fn supersuper() {
super::check(); // calls 1
check(); // calls 2
}
}
Public Structs / enums
- struct를 정의할 때,
pub
키워드를 앞에 붙이면, 해당 struct는 public이 되지만, 그 안에 내용까지 public이 되지는 않는다는 점을 기억해야한다.
mod module {
pub struct Module_struct {
pub field_1: String,
field_2: String,
}
impl Module_struct {
pub fn create(field: &str) -> Module_struct {
Module_struct {
field_1: String::from(field),
field_2: String::from("something"),
}
}
}
}
위 코드의 경우,
field_2
는 private 상태이기 때문에, 해당 필드에 접근할 수 있는 method가 필요할 것이다.(위 예시에서의
Module_struct::create
)반면에,
enum
의 경우는pub
키워드가 붙으면 내부도 public이 된다는 점을 유의해야한다.enum
의 내용물이 private 하다면... 왜 쓸까?
use
로 Path 접근하기
use
는 파일시스템에서 symbolic link를 만드는 거랑 비슷하다.
::
을 사용하면서 해당 crate에 접근하는 과정은 너무 긴 코드를 만들 가능성이 높다.
이를 다음과 같이 use
키워드로 조금은 해결할 수 있다.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
// 또는 use self::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
hosting::add_to_waitlist();
hosting::add_to_waitlist();
}
::add_to_waitlist
까지 가져올 수도 있지만, 보통 이렇게 하지 않는 이유는어디서 해당 함수가 왔는지를 알기 위해
함수 이름이 겹칠 상황을 방지하기 위해 (
fmt::Result
와io::Result
는 다르다.)
- 만약 이름이 겹칠 수 밖에 없는 상황이라면..? (
as
)
use std::fmt::Result;
use std::io::Result as IoResult;
pub use
를 사용하면, 외부 코드에서 접근할 수 있다.
pub use crate::front_of_house::hosting;
- nested path
// 1. 이랬던 것이
use std::cmp::Ordering;
use std::io;
// 이렇게 바뀔 수 있다.
use std::{cmp::Ordering, io};
// - - - - - - - - - - - - - - -
// 2. 이랬던 것이
use std::io;
use std::io::Write;
// 이렇게 바뀔 수 있다.
use std::io::{self, Write};
- test 할 때 glob operator(
*
)를 유용하게 쓸 수 있다.
use std::collections::*;
파일 분할
한 개의 파일에 모든 내용을 작성할 수도 있지만, 유지 보수 및 대형 프로젝트를 다루기 위해서는, 유사한 작업을 하는 내용끼리 코드를 분리하는 것이 정상적이다.
mod front_of_house;
src/front_of_house.rs
파일을 불러오는 느낌이다.불러온 후에,
pub use crate::front_of_house::some_module
과 같이 사용 가능하다.front_of_house
모듈에 어러개의 서브 모듈이 있다면, 해당 내용들은src/front_of_house
폴더를 만들고, 그 안에 모듈 이름과 파일이름을 동일하게 작성하면 된다.front_of_house.rs
파일과front_of_house
폴더가 둘다 존재해야함.front_of_house.rs
에는pub mod submodule_name;
과 같이src/front_of_house
내부에 있는 모듈을 불러와주어야한다.
Rust Collection (vector, string, hash map)
1. vector
: Vec<T>
- Generic이 implement되어 있기에,, Type annotation이 필요하다.(Rust 자체적으로 추론 가능하긴 하다.)
let v: Vec<i32> = Vec::new();
- 하지만.. 실제 코드에서는 위와 같이 사용하기 보다는, Rust 매크로를 사용하며, 초기값을 바로 준다.
let v = vec![1, 2, 3];
- 위와 같이 썼을 때, Rust 자체적으로 타입 추론이 가능하다.
let mut v = Vec::new();
v.push(5);
vector
값 접근하는 두 가지 방법
let v = vec![1, 2, 3, 4, 5];
// (1)
let thrid: &i32 = &v[2];
// (2)
match v.get(2) {
Some(value) => println!("The third element is {}", value);
None => println!("There is no third element.");
}
(1) 번과 같이 접근할 때, &v[100]
과 같이 존재하지 않는 곳을 참조하려고 하면 프로그램이 panic 된다는 점을 유의해야한다. 존재하지 않는 메모리로 접근할 때, panic을 내야한다면 의도적으로 사용할 수 있다.
(2) 번과 같이 .get()
메소드를 사용하면 Option<&T>
에 대해서 match
할 수 있으므로, panic 상황에 대처할 수 있다.
vector ownership
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let first = &v[0]; // immutable borrow
v.push(6); // mutable job
println!("The first element is: {}", first);
}
위 코드는 컴파일이 되지 않는다.first
는 그저 v
의 첫번째 element를 보고 있을 뿐이고, v
의 마지막에 6을 추가하는 것이 왜 오류를 내는 것인가? vector
는 새로운 element를 추가할 때, 현재 vector가 있는 위치에서의 메모리 공간이 부족하다면 기존의 element들을 새로운 메모리 공간으로 재할당하게 된다. 이 과정에서 첫번째 element가 있던 메모리 공간은 해제된 메모리 공간일 수가 있다. 프로그램의 비정상적 작동을 막기위해 borrowing 규칙(mutable한 reference와 immutable한 reference가 같은 scope내에 있으면 안됨)이 있는 것이다.
vector 반복문
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; // dereference operator
}
- reference 공간에 접근하여 값을 수정하기 위해 dereference operator(
*
) 사용
enum
을 사용하여 vector에 여러 타입을 담을 수 있다
vector에는 한 가지 타입밖에 담을 수 없다. 그 한 가지 타입이 enum 타입이 되는 것이다.
enum SpreadsheetCell {
Int(i32),
Float(f64),
Text(String),
}
let row = vec![
SpreadsheetCell::Int(3),
SpreadsheetCell::Text(String::from("blue")),
SpreadsheetCell::Float(10.12),
];
2. string
Rust에서의 string은 약간 C / C++과 비슷하면서도 또 다르게 느껴진다. 개발자 입장에서는 일반 ASCII문자와, UTF-8 문자를 똑같이 취급하면 오류가 나기 쉽다.
String
과 string slice는 모두 UTF-8 encode 된다.
String 더하기
- Rust에서 string indexing은 안된다. Rust에서 String은
Vec<u8>
이라고 보면 된다.
let hello = String::from("Hola"); // (1)
let hello = String::from("안녕하세요?"); // (2)
(1) 의 length = 4 (각 글자 당 1 byte)
(2) 의 length = 12 (각 글자 당 2 byte)
let hello = "안녕하세요?";
let s = &hello[0..4];
위의 경우, s
는 안녕
이 된다. 만약 &hello[0..1]
과 같이 사용했다면 panic이 일어나게 된다.
String iteration
// each character
for c in "안녕?".chars() {
println!("{}", c);
}
// raw byte
for c in "안녕?".bytes() {
println!("{}", c);
}
3. Hash Map
HashMap<K, V>
: 다른 언어에서 hash, map, object, hash table, dictionary, associative array 와 같은 이름으로 불린다.
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); // score = Some(&10)
for (key, value) in &scores {
println!("{} : {}", key, value);
}
- word vector example
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);
Error Handling
- recoverable 한 에러 :
Result<T, E>
/ unrecoverable 한 에러 ->panic!
Default : Rust 는 panic이 발생하면 unwinding 하는 작업을 진행한다.(지금까지 해온 작업을 거꾸로 거슬러 올라가서 아무것도 없던 최초상태로) 하지만, 이 작업에는 많은 소요가 있으므로, abort 시켜버리는게 대안이 될 수 있다. 그러면 OS에 의해서 메모리가 정리되어야 하겠다. 이 설정은 Cargo.toml
에서 변경할 수 있다.
[profile.release]
panic = 'abort'
-> release모드에서 panic이 일어날 경우 abort 한다.
- Backtrace 필요한 경우
RUST_BACKTRACE=1 cargo run
Result<T,E>
T : Ok
일 때의 데이터 타입
E : Err
일 때의 데이터 타입
- 파일 여는 예제
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file, // 파일 열기 성공!
Err(error) => match error.kind() { // 파일 열기 실패...
ErrorKind::NotFound => match File::create("hello.txt") { // 파일이 안찾아지네? 새로 만들자!
Ok(fc) => fc, // 파일 만들기 성공
Err(e) => panic!("Problem creating the file: {:?}", e), // 파일 만들기 실패..
},
other_error => { // 파일이 안찾아지는건 아닌데 다른 오류임.
panic!("Problem opening the file: {:?}", other_error)
}
}
};
}
하지만 위의 방식으로는.. match가 겹겹이 있어서 코드가 이뻐보이지 않는다.
Closure
를 사용하여 다음과 같이 코드를 짤 수 있다.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt").unwrap_or_else(|error| {
if error.kind() == ErrorKind::NotFound {
File::create("hello.txt").unwrap_or_else(|error| {
panic!("Problem creating the file: {:?}", error);
})
} else {
panic!("Problem opening the file: {:?}", error);
}
});
}
match
없이 좀 더 깔끔하게 읽을 수 있는 코드다.
unwrap
/expect
을 사용하면Ok
일 때는 해당 값을,Err
일 때는panic!
하도록 사용할 수 있다.
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
// expect로 구체적인 에러메세지 제공 가능
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
- Rust에서는 Error가 전달되는 과정에서
?
를 사용할 수 있다.
use std::fs::File;
use std::io;
use std::io::Read;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
?
는 Ok
일 경우 값을 그대로 갖는다.
위 함수는 Err
가 생길경우 해당 Err
를 return하고, 정상적으로 진행되었을 경우 Ok
에 값을 담아서 return 한다.
각 단계별로 match
를 사용하며 무자비하게 작성하는 것보다 훨씬 깔끔하다.
이를 더 짧게 만들 수 도 있다.
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
// 더 짧게도 가능
use std::fs;
use std::io;
fn read_username_from_file2() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
?
를 Return 할 수 있다
?
는 match
랑 똑같이 작동한다. 따라서 Result
타입으로 return할 수 있다.
use std::fs::File;
fn main() {
let f = File::open("hello.txt")?;
}
위 코드를 실행시키면, ?
operator는 Result
나 Option
또는 std::ops::Try
를 implement한 것을 return하는 함수에만 사용할 수 있다고 불평한다.
main
함수를 다음과 같이 작성할 수 있다.
fn main() -> Result<(), Box<dyn Error>> {
let f = File::open("hello.txt");
Ok(())
}
- main 함수의 return type은
()
이다.Box<dyn Error>
는 모든 종류의 에러를 의미한다.