모듈

cargo new --lib adder

sijin@Sijin:~/rust/adder$ tree
.
├── Cargo.toml
└── src
    └── lib.rs

2 directories, 2 files
  • rust에서 모듈은 cargo new --lib [name] 명령어로 만들 수 있다
pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}
  • 이전과 달리 main함수가 없고 대신 test 코드가 생성된다
  • cfg(test)는 코드가 test 환경에서만 컴파일되도록 한다
cargo new calc

# calc 패키지의 Cargo.toml
[package]
name = "calc"
version = "0.1.0"
edition = "2024"

[dependencies]
adder = {path = "../adder"}
  • 생성한 adder 라이브러리를 사용하기 위해서 adder를 사용하는 모듈 calc의 Cargo.toml에 dependency를 추가하면
fn main() {
    let ret = adder::add(1, 2);
    println!("{}", ret);
}
  • calc에서 위 처럼 사용할 수 있다

모듈 파일 단위 분리

adder/
├── Cargo.lock
├── Cargo.toml
├── src
│   ├── business
│   │   ├── mod.rs
│   │   └── user.rs
│   ├── database
│   │   ├── mod.rs
│   │   └── user_dao.rs
│   └── lib.rs
  • 위 파일 구조처럼 디렉토리마다 mod.rs 파일을 만들어 서브모듈을 각각의 디렉토리, 파일 단위로 분리할 수 있다
# database/user_dao.rs
pub fn create() {
    println!("database::user_dao::create");
}

# database/mod.rs
pub mod user_dao;
  • database의 경우 user_dao 서브 모듈을 user_dao.rs에 만들고
  • 이를 mod.rs에서 export 시킬 수 있다
# business/user.rs
pub fn create() {
    println!("database::user::create");
}

# business/mod.rs
pub mod user;
  • business의 경우에도 동일하게 user 서브모듈을 user.rs에서 만들어서
  • mod.rs에서 export 시킬 수 있다
pub mod business;
pub mod database; 

#[test]
fn it_works() {
    business::user::create();
    database::user_dao::create();
}
  • lib.rs에서는 노출된 business, database를 다시 export 시키고
  • test code에서 호출할 수 있다

use 사용하기

  • 현재 프로젝트의 서브모듈에서 다른 서브모듈을 사용해보자
  • user::create가 호출될 때 user_dao도 create 되도록 수정해보자
pub fn create() {
    println!("database::user::create");
    super::super::database::user_dao::create();
}
  • super를 통해 상위폴더의 모듈에 접근할 수 있다
  • 그러나 폴더 depth가 깊어지는 상황에서 super는 부적절해보인다
pub fn create() {
    println!("database::user::create");
    crate::database::user_dao::create();
}
  • crate를 통해 크레이트를 기준으로 모듈을 찾을 수 있다

가시성 제어

  • rust에서 public private같은 가시성 제어는 pub 키워드를 사용해 적용할 수 있다
제한자내용
pub정보를 모두 노출
pub(in 대상 모듈)제한된 경로에 한정해서 정보를 노출
pub(crate)현재 crate에 한정하여 정보를 노출
pub(super)상위 모듈에 한정해서 정보를 노출
생략 or pub(self)정보를 외부에 노출하지 않음
mod my_module {
    pub fn public_fn() { ... };
    fn public_fn() { ... };
    pub(crate) public_fn {};
    pub(super) public_fn {};
}

오류처리

  • 러스트는 오류를 복구 가능한 오류, 복구 불가능한 오류 두가지로 나누어 처리한다
    • 복구 가능한 오류는 오류 발생했을 시 예외 로직을 실행한다
    • 복구 불가능한 오류는 메모리를 더이상 할당받지 못하거나, 크래스가 발생한 상황을 의미한다

복구 가능한 오류

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • 러스트에서는 Result 열거형을 사용해서 복구 가능한 오류를 제어한다
  • Ok(T)는 함수의 실행시 성공한 경우 반환,
  • Err(E)는 함수가 실패했을 때 반환된다
use core::panic;
use std::fs::File;

fn main() {
    let res = File::open("test.txt");

    let _f = match res {
        Ok(f) => f,
        Err(err) =>{
            panic!("file open fail: {:?}", err);
        }
    };

    println!("file open success");
}
  • File::open의 반환형은 Result<std::fs::File, std::io::Error>이다
  • 그래서 match 절에서 file open이 성공한 경우 Ok(f)가, 실패한 경우 Err(err) 분기를 탄다
  • 그러나 매번 match 절을 쓰는건 코드가 장황해진다. unwrap, expect, ?를 사용해 쉽게 오류를 대응할 수 있다

unwrap

use std::fs::File;

fn main() {
    let res = File::open("test.txt").unwrap();
    println!("file open success");
}
  • unwrap은 Result가 Ok인 경우 그 값을 반환하고, Err이면 panic을 발생시킨다

expect

use std::fs::File;

fn main() {
    let res = File::open("test.txt").expect("!!my custom error message!!");
    println!("file open success");
}


=> 오류 발생 시 아래처럼 출력됨
thread 'main' panicked at src/main.rs:4:38:
!!my custom error message!!: Os { code: 2, kind: NotFound, message: "No such file or directory" }
  • expect는 unwrap과 비슷하게 Result가 Ok인 경우 그 값을, Err인 경우 Panic을 발생시킨다
  • 여기서 expect는 추가로 panic 시 지정된 오류 메세지를 출력시킬 수 있다

? 키워드로 오류 전파

fn read_file() -> Result<String, io::Error> {
    let mut str = String::new();
    let mut file = File::open("test.txt")?;
    file.read_to_string(&mut str)?;
    return Ok(str);
}
  • ? 키워드는 Result 타입의 값이 Ok면 그 값을 반환하고, Err이면 그 즉시 함수에서 Err를 반환한다
  • 위 예시에서는 File::open, file.read_to_string 각각 함수에서 실패 시 즉시 read_file 함수에서 Err를 리턴한다

복구 불가능한 오류

복구 불가능한 오류는 아래와 같은 예시가 있다

  • 잘못된 메모리 접근
  • 잘못된 배열 인덱스 접근
  • 잘못된 파일/소켓/파이프 접근
  • 시스템 메모리 부족
  • 0으로 나누기

0으로 나누기

fn div(a: i32, b: i32) -> i32 {
    a / b
}
fn main() {
    div(1, 0);
}
  • 위 코드의 경우 0으로 나누기를 시도하다가 panic이 발생했다

BT 출력하기

RUST_BACKTRACE=1 cargo run
  • Backtrace를 출력하기 위해서는 RUST_BACKTRACE 환경변수를 선언하면 된다

복구 불가능한 오류 발생시키기

fn div(a: i32, b: i32) -> i32 {
    if(b == 0) {
        panic!("devide with 0")
    }
    a / b
}
  • panic!으로 복구 불가능한 오류를 발생시킬 수 있다

panic이 발생할 수 있는 경우 설명 추가하기

  • 복구 불가능한 오류를 발생시키는 경우 함수 명세, 가이드 페이지에 panic!이 발생할 수 있다고 명시하는 것이 좋다
  • 이러한 공지가 없다면 함수 호출자는 영문도 모른채 panic!을 마주할 수 있다

Vec

fn main() {
    let mut v: Vec<i32> = Vec::new();

    for i in 1..10 {
        v.push(i);
    }

    for d in &v {
        print!("{}", d);
    }
}
  • Vec를 생성하고 삽입, 순회하는 예제이다
fn main() {
    let v: Vec<i32> = vec![0, 1, 2, 3, 4];

    for d in &v {
        print!("{}", d);
    }
}
  • Vec::newvec![] 매크로로 대체할 수 있다
fn main() {
    let v: Vec<i32> = vec![0, 1, 2, 3, 4];

    let one = v[0];
    let two = v.get(1);
    let nine = v.get(9);

    println!("{} {:?} {:?}", one, two, nine); // => 0 Some(1) None
}
  • get 메서드로 단일값을 가져오면 Option이 반환된다
fn main() {
    let mut v: Vec<i32> = vec![0, 1, 2, 3, 4];

    v[1] = 100;

    for i in &mut v {
        *i = *i * 100;
    }

    for d in &v {
        print!("{} ", d);
    }
}
  • vector 값의 변환은 위 처럼 할 수 있다

LinkedList

use std::collections::LinkedList;

fn main() {
    let mut linkedlist: LinkedList<i32> = LinkedList::new();

    for i in 1..10 {
        linkedlist.push_back(i);
    }

    println!("{:?}", linkedlist.iter().nth(7));
}
  • LinkedList는 위 예제와 같이 삽입 및 값을 획득할 수 있다
  • random access를 지원하지 않으므로 전체를 순회하거나 iter()로 데이터를 찾아야 한다
use std::collections::LinkedList;

fn main() {
    let mut linkedlist: LinkedList<i32> = LinkedList::new();

    for i in 1..10 {
        linkedlist.push_back(i);
    }

    for d in linkedlist.iter_mut() {
        *d = *d * 10;
    }

    for d in linkedlist.iter() {
        print!("{} ", d);
    }
}
  • LinkedList는 위 처럼 값을 변경할 수 있다

HashMap

use std::collections::HashMap;

fn main() {
    let mut hashmap: HashMap<i32, String> = HashMap::new();

    hashmap.insert(10, String::from("aaa"));
    hashmap.insert(20, String::from("bbb"));

    let aaa = hashmap.get(&10); // key는 빌림 형식

    for (key, value) in &hashmap {
        println!("{} {}", key, value);
    }
}
  • key:value 형식의 HashMap은 위 처럼 사용할 수 있다

HashSet

use std::collections::{HashSet};

fn main() {
    let mut hashset: HashSet<String> = HashSet::new();

    hashset.insert(String::from("aaa"));
    hashset.insert(String::from("bbb"));
    hashset.insert(String::from("aaa"));

    for data in &hashset {
        println!("{}", data);
    }

    if hashset.contains("aaa") == false {
        println!("aaa가 없음");
    }
}
  • HashSet은 집합 내 중복을 허용하지 않고 고유한 값만 유지한다
  • 중복 제거에 유용하다

BinaryHeap

use std::collections::{BinaryHeap};

fn main() {
    let mut heap: BinaryHeap<i32> = BinaryHeap::new();

    heap.push(1);
    heap.push(3);
    heap.push(2);

    while heap.is_empty() == false {
        println!("{:?}", heap.pop());
    }
}
  • BinaryHeap은 우선순위큐이다
  • 위 예제에서는 Some(3), Some(2), Some(1) 순으로 출력된다

String

use std::fmt::format;

fn main() {
    let mut str1 = String::new();
    str1.push_str("hello");
    let str1 = format!("{} {}", str1, String::from("world"));
    println!("{}", str1);

    for c in str1.chars() {
        print!("{} ", c);
    }
}
  • String은 문자들을 내부적으로 Vector를 활용해서 관리한다
  • rust는 기본 자료형으로 &str을 가지고 있다
    • &str은 문자들을 배열로 관리해서 크기를 늘리거나 줄이는 것이 불가능하다
    • 또한 &str과 같이 빌림 형식을 취하는 것이 일반적이다
    • String은 소유권을 가지고 있으며 크기를 동적으로 늘리고 줄일 수 있다

컬렉션의 소유권

  • 소유권에는 iter와 into_iter로 반복자를 생성한다
    • 두 메서드의 작동 방식은 약간 다르다

iter

fn main() {
    let vec = vec![1, 2, 3];

    for item in vec.iter() {
        println!("{}", item);
    }

    println!("{:?}", vec); // [1, 2, 3] 출력
}
  • iter는 사용해도 vec의 소유권이 그대로 남아있다

into_iter

fn main() {
    let vec = vec![1, 2, 3];

    for item in vec.into_iter() {
        println!("{}", item);
    }

    println!("{:?}", vec); // 컴파일 오류
}
  • into_iter는 사용하면 소유권을 반복자로 이동한다
  • 불편해보이지만, 불필요한 변환 작업 없이 효율적으로 작동한다

0개의 댓글