Dev/Language

impl Trait과 Box<dyn Trait> - Rust 프로그래밍

TaeGit 2022. 12. 19. 23:54

Rust Programming Language

 

목차

  • 들어가며
  • impl Trait
  • Box<dyn Trait>

 

 

1. 들어가며

객체지향 언어들에서는 흔히 추상화를 위해 InterfaceAbstract Class 같은 것들을 제공합니다. Rust에서는 이것들과 유사하지만 다른 Trait이라는 것을 제공하는데요. 이전 글 트레잇(Trait)과 트레잇 바운드(Trait Bound) - Rust 프로그래밍에서도 기본적인 내용들을 설명했었습니다.

 

Rust-Book에서도 찾아볼 수 있지만, Generic TypeTrait Bound를 함께 사용하면 일부 추상화 기능을 구현할 수 있습니다. 이번 글에서 설명하는 impl Trait도 마찬가지 입니다. 이렇게 컴파일 타임에 구현되는 다형성을 정적 다형성 또는 컴파일 타임 다형성이라고 합니다.

 

Generic TypeTrait Bound를 함께 사용하는 것과 impl Trait을 사용하는 것은 결과적으로 정적 디스패치를 구현하는 것입니다. 즉, 컴파일러가 컴파일 타임에 타입들을 검사하고 내부적으로 명시된 타입들에 대한 코드를 구현하는 것입니다. 이는 컴파일러에게 정확한 타입들을 알아내라고 시키는 것과 같으며, 컴파일 타임 비용증가시킵니다. Rust에서 동적 디스패치를 구현하기 위해서는 Box<dyn Trait>을 사용해야 하며, 이는 런타임 비용을 증가시킵니다.

 

이번 글에서는 impl TraitBox<dyn Trait>에 대해 알아보겠습니다.

 

 

2. impl Trait

우선, impl Trait에 대한 몇 가지 규칙이 존재하는데, 현재 Rust의 stable 버전에서 impl Trait은 아래 두 위치에서만 사용할 수 있습니다.

  • 함수의 인자 위치
  • 함수의 반환 타입 위치

Trait의 함수에서는 사용할 수 없으며, 변수 바인딩에도 사용할 수 없습니다. impl Trait에 대한 자세한 내용은 아래 References에서 확인할 수 있습니다.

 

이번 글에서는 앞서 언급한 두 위치에서의 사용에 대해 알아보겠습니다.

 

예제 코드

먼저, 설명을 위해 Car TraitCar Trait을 구현하는 SedanSuv Struct를 구현해보겠습니다.

trait Car {
    fn drive(&self);
}

struct Sedan;

impl Car for Sedan {
    fn drive(&self) {
        println!("drvie Sedan");
    }
}

struct Suv;

impl Car for Suv {
    fn drive(&self) {
        println!("drvie Suv");
    }
}

Car Traitdrive 메소드를 갖고 있고, SedanSuv Struct는 Car Trait을 구현하고 있습니다.

 

함수의 인자 위치

이제 drvie_car 함수를 구현해서 인자 위치에 impl Car를 사용해보겠습니다.

fn drive_car(car: impl Car) {
    car.drive();
}

drive_car 함수는 impl Car 타입의 인자 car를 받습니다.

 

위의 impl Trait을 이용한 코드는 아래 Generic Type + Trait Bound를 사용한 코드로 바꿀 수 있습니다.

fn drive_car<T: Car>(car: T) {
    car.drive();
}

이는 이전 글 트레잇(Trait)과 트레잇 바운드(Trait Bound) - Rust 프로그래밍에 좀 더 자세히 설명되어 있습니다.

 

이제 main 함수에서 drive_car의 인자로 SedanSuv를 넘겨보겠습니다.

fn main() {
    let sedan = Sedan;
    let suv = Suv;

    let is_sedan = true;

    if is_sedan {
        drive_car(sedan);
    } else {
        drive_car(suv);
    }
}

main 함수는 is_sedan 값이 true이면 drive_car의 인자로 sedan을 넘기고, false이면 suv를 넘깁니다.

 

impl Trait - argument position

들어가며에서 언급했지만, 이 코드는 정적 다형성을 구현한 것입니다. 컴파일 타임Rust 컴파일러는 drive_car 함수의 인자로 sedan과 suv를 넘기는 것을 확인하고, sedan과 suv가 Car Trait을 구현했는지 검사합니다.

 

최종적으로 컴파일러는 아래와 같이 SedanSuv를 위한 함수를 추가로 생성합니다.

fn drive_car_sedan(car: Sedan) {
    car.drive();
}

fn drive_car_suv(car: Suv) {
    car.drive();
}

결과적으로, 명확한 타입을 갖는 함수들이 추가되었고, 컴파일 타임에 어떤 함수를 호출할지가 결정되는 정적 디스패치가 구현된 것입니다. Rust에서는 런타임 비용을 증가시키지 않는 이런 방식의 추상화를 선호합니다.

 

함수의 반환 타입 위치

impl Trait을 함수의 반환 타입 위치에서 사용할 때는 조금 다른데요.

fn get_car(is_sedan: bool) -> impl Car{
    if is_sedan {
        Sedan
    } else {
        Suv
    }
}

get_car 함수는 is_sedan이 true이면 Sedan을 반환하고, false이면 Suv를 반환합니다.


안타깝게도, 이 코드는 컴파일이 되지 않습니다. 반환 타입의 impl CarCar를 구현하는 유일한 타입 하나를 리턴하겠다는 것을 의미합니다. 하지만, 우리는 지금 Car를 구현하는 Sedan 타입 혹은 Car를 구현하는 Suv 타입을 리턴하겠다고 하고 있습니다.


즉, 현재 Rust 컴파일러가 컴파일 타임에 반환 타입을 결정할 수 없기 때문에 에러를 발생시키고 아래와 같이 Box<dyn Car>를 사용하라는 도움말이 출력되는 것을 볼 수 있습니다.

Error of impl Trait - return type position

이제 이를 동적 다형성으로 해결하기 위해 Box<dyn Trait>을 사용하는 방법을 알아보겠습니다.

 

 

3. Box<dyn Trait>

함수의 리턴 타입으로 동적 타입을 사용하기 위해서는 Box<dyn Trait>을 사용해야 합니다.

fn get_car(is_sedan: bool) -> Box<dyn Car>{
    if is_sedan {
        Box::new(Sedan)
    } else {
        Box::new(Suv)
    }
}

이렇게 하면 get_car 함수는 다수의 타입을 리턴할 수 있게 됩니다. dyn 키워드는 Trait 타입 앞에 오고 메서드가 동적 디스패치 될 것을 나타내기 위해 사용됩니다. dyn Trait의 참조자는 인스턴스 객체를 위한 포인터와 vtable을 가리키는 포인터 총 두 개의 포인터를 갖습니다. 그리고 런타임에 이 함수가 필요해지면 vtable을 참조해 포인터를 얻게 됩니다.

 

fn main() {

    let is_sedan = false;

    let car = get_car(is_sedan);

    car.drive();

}

main 함수는 get_car 함수를 통해 Car Trait을 구현한 Sedan 혹은 Suv 객체를 반환받고 drive 함수를 호출합니다.

 

그러면, 아래와 같이 is_sedan이 false일 때 Suv 가 출력되는 것을 볼 수 있습니다.

Box<dyn Trait> - return position

이런 식으로 동적 다형성을 사용하면 컴파일러는 컴파일 타임에 명확한 타입을 알 수 없고, 런타임 비용을 증가시킵니다. 앞서 말했듯이 Rust에서는 정적 다형성을 선호하지만, 분명 정적 다형성만으로 해결하기 까다로운 상황이 존재할 것입니다. 이는 트레이드오프의 관점에서 상황에 맞게 사용하는 것이 좋겠습니다.

 

 

 

References

 

👋 Welcome - Impl trait initiative

This page tracks the work of the Impl trait initiative! To learn more about what we are trying to do, and to find out the people who are doing it, take a look at the charter. This is an umbrella initiative and, as such, it covers a number of subprojects. K

rust-lang.github.io

 

 

dyn - Rust

dyn is a prefix of a trait object’s type. The dyn keyword is used to highlight that calls to methods on the associated Trait are dynamically dispatched. To use the trait this way, it must be ‘object safe’. Unlike generic parameters or impl Trait, the

doc.rust-lang.org