[Rust] 고급 타입

7 minute read

뉴타입의 유용성, 타입 별칭, ! 타입, 동적 크기를 가진 타입

타입 안전성과 추상화를 위해 뉴타입 패턴을 사용한다.

뉴타입 패턴은 값의 단위를 정확하게 사용하는 데 유용하다.

MillimetersMeters는 둘 다 u32를 감싼 뉴타입으로 표현할 수 있다.

이를 통해 Millimeters를 파라미터로 받아야 하는 곳에 Meters 타입의 값이 들어오는 것을 막을 수 있다.

뉴타입으로 API 공개 수준을 조절할 수도 있고, 내부 구현사항을 숨길 수도 있다.

예를 들어, HashMap<i32, String>을 감싸는 People 타입을 제공할 수 있다.

People을 사용하는 코드는 공개 API만을 통해 상호작용하고 내부적으로 i32가 id로 사용된다는 것을 알 필요가 없다.

타입 별칭은 타입의 동의어를 만든다.

다음과 같이 type 키워드를 사용해서 타입 별칭을 선언할 수 있다.

type Kilometers = i32;

다만, 타입 별칭은 새로운 타입이 아니기 때문에 Kilometersi32와 동일하게 취급된다.

type Kilometers = i32;

let x: i32 = 5;
let y: Kilometers = 5;

println!("x + y = {}", x + y);

타입 별칭은 아래와 같이 긴 타입의 반복을 줄일 때 자주 사용된다.

Box<Fn() + Send + 'static>

아래와 같은 코드보다는

let f: Box<Fn() + Send + 'static> = Box::new(|| println!("hi"));

fn takes_long_type(f: Box<Fn() + Send + 'static>) {
    // --snip--
}

fn returns_long_type() -> Box<Fn() + Send + 'static> {
    // --snip--
}

아래와 같은 코드가 보기도 쉽고 관리하기도 편하다.

type Thunk = Box<Fn() + Send + 'static>;

let f: Thunk = Box::new(|| println!("hi"));

fn takes_long_type(f: Thunk) {
    // --snip--
}

fn returns_long_type() -> Thunk {
    // --snip--
}

표준 라이브러리 std::io 모듈을 보면 Result<..., Error> 타입이 참 많이 나온다.

use std::io::Error;
use std::fmt;

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
    fn flush(&mut self) -> Result<(), Error>;

    fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
    fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

아래와 같이 타입 별칭을 선언하고

type Result<T> = Result<T, std::io::Error>;

std::io 모듈의 코드를 다음과 같이 줄일 수 있다.

pub trait Write {
    fn write(&mut self, buf: &[u8]) -> Result<usize>;
    fn flush(&mut self) -> Result<()>;

    fn write_all(&mut self, buf: &[u8]) -> Result<()>;
    fn write_fmt(&mut self, fmt: Arguments) -> Result<()>;
}

Result<T>Result<T, E> 그 자체이기에 Result<T, E>의 메서드 뿐만아니라 ? 같은 특수 문법도 사용할 수 있다.

결코 반환하지 않는 ! 부정 타입

어떤 함수가 절대 값을 반환하지 않을 때, 반환 타입의 자리를 !가 대신한다.

아래 코드는 함수 bar는 절대 값을 반환하지 않는다라고 읽을 수 있다.

발산 함수라고도 부른다.

fn bar() -> ! {
    // --snip--
}

각 갈래값의 타입이 다른 아래 코드는 컴파일이 안돼도,

let guess = match guess.trim().parse() {
    Ok(_) => 5,
    Err(_) => "hello",
}

한 갈래가 continue로 끝나는 아래 코드는 컴파일이 된다.

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

continue! 값을 갖기 때문이다.

! 타입은 값을 반환하지 않으니 match 갈래값의 타입이 같아야 하는 제약이 적용되지 않는 것이다.

부정타입은 panic! 매크로에도 유용하게 쓰인다.

Option<T>에서 값을 생성하거나 패닉을 일으키는 unwrap 함수에도 부정 타입 이 쓰인다.

impl<T> Option<T> {
    pub fn unwrap(self) -> T {
        match self {
            Some(val) => val,
            None => panic!("called `Option::unwrap()` on a `None` value"),
        }
    }
}

panic!! 타입이므로 위의 match문은 유효하다.

마지막으로, loop! 타입을 갖는다.

print!("forever ");

loop {
    print!("and ever ");
}

break가 없는 이상 루프는 끝나지 않으므로 ! 타입을 갖는다.

동적 크기를 갖는 타입과 Sized

DST(dynamically sized type)는 런타임에서만 크기를 알 수 있는 값을 사용하는 코드를 작성할 수 있게 해준다.

DST크기가 없는 타입(unsized type)이라고도 불린다.

동적 크기 타입인 str을 세부적으로 알아본다.

문자열의 길이는 런타임에서만 알 수 있다.

// 작동 안되는 코드임
let s1: str = "Hello there!";
let s2: str = "How's it going?";

위의 코드를 보면 두 str 값은 타입이 같지만 다른 크기를 갖는다.

위의 코드를 작동시키기 위해서는 str 대신에 &str을 사용해야 한다.

문자열 그 자체가 아닌, 문자열 슬라이스의 시작 위치와 길이만 저장한다.

&strstr의 주소와 길이를 갖는 두 개의 값을 나타낸다.

str의 크기를 몰라도 &str의 크기는 알 수 있으니 동적 크기를 갖는 타입을 사용할 수 있다.

트레잇 객체를 사용해서 서로 다른 타입의 값을 한번에 사용하려고 했을 때도, &Trait이나 Box<Trait> 같이 값을 포인터에 넣어서 사용했다.

러스트는 Sized 트레잇으로 DST를 다룬다.

Sized 트레잇은 어떤 타입의 크기를 컴파일 타임에 알 수 있는지 결정한다.

러스트는 암묵적으로 모든 제네릭 함수에게 Sized 트레잇 바운드를 추가한다.

다음과 같은 코드는

fn generic<T>(t: T) {
    // --snip--
}

다음과 같이 취급된다.

fn generic<T: Sized>(t: T) {
    // --snip--
}

?SizedSized의 반대 개념이다.

TSized일 수도 있고 아닐 수도 있음을 나타낸다.

아래와 같이 함수를 정의하면 컴파일 타입에 크기를 알 수 없는 타입을 사용할 수 있다.

fn generic<T: ?Sized>(t: &T) {
    // --snip--
}

크기를 알 수 없으므로 파라미터는 포인터를 사용해서 T 값을 다룬다.

Categories:

Updated: