본문 바로가기

프로그래밍/RUST

[RUST] 다른 언어와 다른 부분, 특징 정리 중....

러스트를 전체적으로 살펴보다가 좀 특이한 것들만 정리.
일단 대략 파악만 하고... 좀더 세부적으로 살펴봐야 할 내용들이 많음.

 

엔트리포인트

크레이트 루트
바이너리 : src/main.rs
라이브러리 : src/lib.rs

모듈 선언

루트와 같은 파일에 있는 경우 모듈 정의

mod module {
}


별도 파일로 모듈 정의

src/module.rs
or
src/module/mod.rs

별도 파일로 모듈을 구성한 경우 크레이트 루트에 모듈 정의

mod module;



서브모듈 선언 : 모듈안에 선언된 모듈
서브모듈을 가진 모듈도 모듈 폴더 아래에 위치해야 함.

// 모듈
src/module/mod.rs

// 서브모듈
src/module/submodule.rs or src/module/submodule/mod.rs



코드참조

직접참조 : crate::module::submodule::something
단축참조 : use crate::module:submodule::something


공개 : 모듈내의 코드는 부모 모듈에 비공개가 기본, 공개하려면 pub mod 로 선언



모듈에서 크레이트 루트에 정의된 항목 사용

use crate::CustomEnum

 

파괴자

객체는 해제시에 Drop 트레잇의 drop 메쏘드를 호출

impl Drop for MyObject {
    fn drop(&mut self) {
    }
}

 

소유권

변수 할당할때 소유권이 넘어감
특히, 두 변수를 합쳐 새로운 변수를 사용하는 경우에도 소유권이 넘어가 기존 변수는 해제 되므로 주의
함수 호출시에도 소유권이 넘어가므로 리터값으로 소유권을 돌려 받거나 레퍼런스 타입으로 전달




구조체

struct Data {
    name: String,
    id: u64,
    flag: bool,
}

let data = Data {
    name: String::from("name"),
    id: 0,
    flag: true,
};



튜플 구조체

각 필드가 이름이 없는 구조체

struct Color(i32, i32, i32);

let data = Color(0, 0, 0);

각 항목은 튜플과 동일하게 object.0, object.1, object.2 과 같이 인덱스로 접근

 

구조체 인스턴스 생성

모든 필드 초기값 지정

let data = Data {
    name: String::from("data"),
    id: 1,
    flag: true,
};




구조체 값 변경

생성 시 mut 키워드 추가

let mut data = Data {
    name: String::from("data"),
    id: 1,
    flag: true,
};




필드이름 생략

인스턴스 생성시 변수로 값을 지정하는 경우 필드 이름과 변수명이 같으면 필드명 없이 초기화 가능

let name = String::from("data");
let id = 1;
let flag = true;

let data = Data {
    name,
    id,
    flag
};




기존 데이터로 새 인스턴스 생성

let data2 = Data {
    name: String::from("data2"),
    ..data
};




구조체 메소드

첫번째 인자는 자신의 참조

impl Data {
    fn something(&self) -> u64 {
        self.id
    }

    fn setter(&mut self, flag: bool) -> bool {
        self.flag = flag
    }
}



연관함수 : String::from 메소드와 같이 구조체 인스턴스 없이 호출 가능한 함수
첫번째 self 없이 메쏘드를 선언하면 연관함수로 선언
사용시 구조체::함수() 형태로 직접 호출

 

 

 

제네릭

대부분 다른 언어와 비슷한데.. 구조체 선언시 제네릭이 선언된 경우 impl<T> 로 구현이 이루어져야 함

struct Point<T> {
}

impl<T> Point<T> {
    fn something(&self) -> &T {
    }
}

 

오류 처리 열거형

Result<T, E>

// std::result
enum Result<T, E> {
    Ok(T),
    Err(E),
}


// match example
fn example() -> Result<i32, String> {
    Err("error")
}

fn main() {
    let result = example();
    match result {
        Ok(value) => println!("{}, value),
        Err(msg) => println!("{}, msg),
    }
}

 

Option<T>

enum Option<T> {
    Some(T),
    None,
}

 

옵션 체크

match option {
    Some(value) => println!("ok: {}", value),
    None => println!("none"),
}

if let Some(value) = option {
    println!("ok: {}", value);
} else {
    println!("none");
}

 

옵션에 따라 변경

// unwrap : none 일 경우 패닉 발생
option.unwrap();

// expect : none 일 경우 패닉 발생, 메시지 출력
option.expect("message");

// map: 클로저로 새로운 Some 생성
option.map( |value| value );

// and_then : 새로운 타임의 Some 생성
option.and_then( |value| Some(true) );

// unwrap_or : none인 경우 0 반환
option.unwrap_or(0);

// unwrap_or_else : none인 경우 클로저 결과 반환
option.unwrap_or_else( || 0 );

 

에러리턴

Result, Optional 함수를 호출해서 그 결과를 다시 리턴하는 함수의 경우 ? 로 결과를 간략하게 리턴할 수 있다.
물음표('?') 연산자는 Result와 Optional에서 사용 가능하다.
호출한 함수가 Result::Ok(_) 이거나 Optional::Some(_) 이면, 해당 값을 unwrap 해주고, 에러인 경우 해당 에러를 그대로 리턴해 준다. 따라서, 함수내에서 결과를 받은 함수와 내가 리턴할 타입이 동일해야 한다. 만약, 리턴할 타입이 다르면 `?` couldn't convert the error to 'type' 이라는 에러가 발생한다.

fn something_a(flag: bool) -> Result<String, String> {
    match flag {
    	true => Ok(String::from("ok")),
        false => Err(String::from("error")),
    };
}

fn something_b() -> Result<String, String> {
    let result = something_a()?;
    println!("result:{}",result);
    Ok(String::from("ok"))
}

위의 something_b() 함수에서 호출한 something_a() 의 리턴값은 '?' 때문에 Ok(String) 이 아닌 String 값이 바로 전달된다. 만약 에러가 발생하면 에러를 그대로 리턴하게 된다.

 

 

Trait

타언어의 인터페이스와 비슷한 기능

트레잇 정의

pub trait MyTrait {
     fn function(&self);
}

 

트레잇 구현

기본 구현

trait MyTrait {
    fn something(&self) {
        println!("default trait function");
    }
}

 

특정 객체 구현

impl MyTrait for SomeObject {
    fn function(&self) {
        println!("SomeObject trait function");
    }
}

 

트레잇 바운드

함수 파라미터나 리턴값에 트레잇을 사용하기 위해 impl 키워드, 제레릭, where 를 사용할 수 있다.

// 트레잇 바운딩
// impl
fn something(param: impl MyTrait) {
}

// generic
fn something<T: MyTrait>(param: T) {
}

// 여러 트레잇을 가진 파라미터 바운딩
// impl
fn something(param: (impl MyTrait + OtherTrait) ) {
}

// generic
fn something<T: MyTrait + OtherTrait>(param: T) {
}


// where
fn something<T>(param: T) where T: MyTrait {
}

 

 

연관타입

swift 의 프로토콜 associatedtype과 같이 trait에도 연관타입을 사용할 수 있다.

trait Something {
    type AssociatedTypeName;
    
    fn count(&self, &Self::AssociatedTypeName) -> i32;
}


impl Something for OtherObject {
    type AssociatedTypeName = Vec<i32>; 
    
    fn count(&self, values: &Vec<i32>) -> i32 {
    	values.len()
    }
}

 

 

기타 Trait 들

From trait

From 은 타입을 변경해 새로운 타입을 리턴하는 트레잇이다.
String::from("hello") 와 같이 문자열을 String 객체로 반환하도록 From 트레잇의 from 함수가 구현되어 있다.

 

Debug trait

구조체 선언에 Debug trait derive 를 추가하면 해당 trait 구현이 추가된다.

#[derive(Debug)]
struct Data {
    .
    .
}

// 출력
println!("{:?}", data);
println!("{:#?}, data);

 

Display trait

디버그 메시지가 아닌 메시지를 표준출력으로 하려면 Display trait을 구현한다.

impl fmt::Display for Data {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write(f, "hello world")
    }
}


// 출력
fn main() {
    let data: Data = Data();
    println!("{}", data);
}

 

 

 

함수타입, 클로저

함수 타입

일반적인 함수를 전달하거나 저장하기 위해서는 fn 타입을 사용한다.

// 함수 타입 구성
fn(param_type) -> return_type


// ex
// 함수 정의
fn something(value: i32) -> bool {
    true
}

// 함수타입에 저장
let function: fn(i32) -> bool = something;

 

클로저

클로저는 동적 함수이고, 스코프내의 값을 캡처할 수 있다. 클로저내에서 정의된 스코프의 변수 접근이 가능하며, 캡처시 별도의 메모리를 사용하게된다.

// 클로저 구성
|parameter: type| -> return_type {
    return_value
};

각 타입은 추론 가능하므로 생략 가능

한줄로 이루어진 클로저는 간략화해서 사용 가능

|param| return_value

 

클로저는 함수타입이 아니므로, 별도 구조체 트레잇을 사용해 저장해야 한다.
캡처 방식에 따라 Fn, FnOnce, FnMut 등의 트레잇을 사용한다.

Fn(type) -> type

 

만약 클로저를 반환하는 경우 Box<Fn() -> ()> 과 같이 별도의 스마트포인터 트레잇을 사용해 반환해야 한다.

 

 

비동기(async/await)

함수 선언 앞에 asyc 가 붙으면 비동기 함수로 정의(블록에 asyc를 붙어 비동기 블럭도 정의 가능)
async 함수의 리턴값은 Future

async fn something() {
    .
    .
}

 

await는 async 메쏘드나 블럭에서만 호출 가능

async fn something() {
    // async 함수 await
    other_something().await;
    
    // async 블럭 자체를 await    
    async {
    
    }.await;
}


main과 같은 비동기함수에서는 await 불가하므로, 별도의 executor가 필요.
features, futures, tokio 와 같은 라이브러리를 사용거나 excutor를 직접 구현해 주어야 한다.

features 모듈 block_on

use features::executor::block_on;

async fn something() {
}

fn main() {
    let future = something();
    block_on(future);
}

 

 

 

 

 

 

라이프타임 파라미터

라이프타임은 댕글링 포인터등의 참조를 제한하기 위한 기법.
러스트의 모든 참조자(&) 는 라이프타임을 가지고 있으며, 라이프타임은 코드를 통해 추론됨.
명시적으로 라이프타임을 정의할 수 있으며, 라이프타임 이름이 어퍼스트로피로 시작
라이프타임은 각종 제네릭에서 사용되므로, 일단 해당 형식만 알아두고 별도 정리 필요.

제네릭과 비슷하게 아래처럼 정의한다.

정의 : <'name>
파라미터 : &'name type

 

예를 들어 Owner 에게 Item목록이 항상 유효하기를 원한다면 Owner와 items항목에 동일한 라이프타임을 명시한다.
이 경우 Owner가 해제되기전 items 가 해지되면 오류가 발생한다.

struct Owner<'a> {
    items: &'a Vec<Item>,
}

impl<'a> Owner<'a> {

}



함수 호출과 리턴 시의 라이프타임을 맞추려면 함수내의 모든 참조자가 동일한 라이프 타임을 갖도록 정의한다.

fn something<'a>(x: &'a str, y: &'a str) -> &'a str {
}

 

라이프타임이 다른 라이프타임과 연관되도록 정의하는 경우 'a: 'b 와 같은 문법 사용

// a, b 두개의 라이프타임을 정의하는데, b는 a보다 오래 유지되어야 한다.
struct Something<'a, 'b: 'a> {
}


// a 라이프타임을 정의하고, T 객체는 a 보다 오래 유지되어야 한다.
struct Something<'a, T: 'a> {
}

 

 

 

스마트 포인터

c++과 마찬가지로 데이터를 힙에 할당하며, 스택에 그에 대한 포인터만 저장하거나 참조나 삭제를 자체적으로 수행해 준다는 점은 동일하다. 단, 참조, 삭제를 위해 필수 Trait을 구현해 주어야 하며, 소유권 이동시 구현된 Drop trait으로 해제가 일어나게 된다. (unique_ptr과 비슷한 느낌....)
String::from("hello") 의 경우 문자열을 힙에 할당하게 되는데, String도 스마트 포인터이다. 

Box<T>

클로저를 힙에 저장

struct Something {
    closure: Box<dyn F()>,
}

let object = Something {
    closure: Box::new(|| println!("closure")),
};