Rust

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_namerust_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_2private 상태이기 때문에, 해당 필드에 접근할 수 있는 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 까지 가져올 수도 있지만, 보통 이렇게 하지 않는 이유는

    1. 어디서 해당 함수가 왔는지를 알기 위해

    2. 함수 이름이 겹칠 상황을 방지하기 위해 (fmt::Resultio::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는 ResultOption 또는 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> 는 모든 종류의 에러를 의미한다.

 

출처