[0]

Rust에서는 기본적으로 오버로딩을 지원하지 않는다. 물론 generic type을 지원하기 때문에 함수 혹은 구조체가 여러 타입을 받도록 할 수는 있다. 하지만 타입 별로 동작을 다르게 하기 위해서는 trait object를 사용해야 한다.

 

[1]

trait object는 사실 처음에 굉장히 어색하게 느껴지는 개념이다. 아무래도 이름 때문에 그런데, 조금 더 직관적으로 표현하자면 어떤 trait을 구현(impl)한 구조체라고 할 수 있다.

struct Foo{
    x:i32,
}

trait Simple{
    fn check(&self);
}

impl Simple for Foo{
    fn check(&self){
        println!("check : {}",self.x);
    }
}

예를 들어 위의 Foo 구조체는 Simple trait을 구현하므로, Simple trait obejct로 취급될 수 있다.

 

[2]

이제 trait object를 이용해서 오버로딩을 구현하는 코드를 보자. 다음과 같이 Simple trait object를 매개변수로 받는 함수에 Foo 구조체를 넘겨줄 수 있다.

fn run(input: &dyn Simple){
    input.check();
}

fn main(){
    let obj = Foo{x:1};
    run(&obj);
}

dyn 키워드는 생략 가능하지만, 컴파일러에서는 가독성을 위해 붙일 것을 권장하고 있다. 아마도 dynamic dispatch의 약자일듯 싶다. 여기서 주의 깊게 봐야 할 점은 trait object를 직접 넘겨주는 것이 아니라 레퍼런스로 넘겨준다는 점이다. 그 이유는 &를 빼고 컴파일해보면 명확하게 알 수 있다. 

error[E0277]: the size for values of type `(dyn Simple + 'static)` cannot be known at compilation time
  --> src\main.rs:22:5
   |
22 |     run(&obj);
   |     ^^^ doesn't have a size known at compile-time
   |
   = help: the trait `std::marker::Sized` is not implemented for `(dyn Simple + 'static)`
   = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
   = note: all function arguments must have a statically known size
   = help: unsized locals are gated as an unstable feature

컴파일 타임에 파라미터의 사이즈를 알 수 없다고 에러가 발생한다. 같은 Simple trait object라고 하더라도  Foo struct가 아닌 다른 struct가 얼마든지 올 수 있으므로 사이즈를 알아낼 수 없기 때문이다. 따라서 레퍼런스나 Box 등의 스마트 포인터를 사용하여야 한다.

 

[3]

그럼 이를 이용해서 파라미터로 받은 타입에 따라 다르게 동작하는 오버로딩을 구현해보자. 여러 타입을 하기에는 귀찮으니 i32와 String 두 가지에 대해서만 구현해본다. 

trait Simple{
    fn check(&self);
}

impl Simple for i32{
    fn check(&self){
        println!("this is i32 : {}",self);
    }
}

impl Simple for String{
    fn check(&self){
        println!("this is String : {}",self);
    }
}

fn run(input: &dyn Simple){
    input.check();
}

fn main(){
    let num = 10 as i32;
    let my_string = "hello trait object!".to_string();

    run(&num as &dyn Simple);		// same as run(&num);
    run(&my_string as &dyn Simple);	// same as run(&my_string);
}
this is i32 : 10
this is String : hello trait object!

위와 같이 primitive type에 대해서도 trait을 구현하여 trait object로 취급하는 게 가능하다. 

 

[4]

trait object는 위에서 본 것처럼, 여러가지 타입을 하나의 trait object 타입으로 표현할 수 있게 해 준다. 때문에 오버로딩 외의 상황에서도 사용할 수 있다. 예를 들어 VecDeque를 사용한다고 할 때, trait object를 이용하여 여러 가지 타입이 하나의 큐에 들어가게 할 수 있다. enum을 통해서도 가능하지만, 이는 코딩 시에 알고 있는 타입에 대해서만 가능하다. api 유저가 정의한 구조체들을(즉 라이브러리 개발자 입장에서는 알 수 없는 타입들을) enum에 미리 포함시킬 수는 없기 때문이다. 

use std::collections::VecDeque;

fn main(){
    let mut queue:VecDeque<&dyn Any> = VecDeque::new();
    let num = 10 as i32;
    let my_string = "hello trait object!".to_string();

    queue.push_back(&num);
    queue.push_back(&my_string);
}

하지만 trait object를 이용하면 어떤 타입도(개발시에 미리 알지 못한 타입도 포함해서) 하나의 VecDeque에 넣을 수 있다. 단, 해당하는 타입이 Simple trait을 구현해야만 한다. 근데 api 유저에게 trait을 명시해줄 것을 요구하는 것도 피하고 싶다면?

 

[5]

rust std에서는 이를 위해 Any라는 trait을 제공한다. 이 trait은 'static lifetime을 가진 모든 타입에 대해 자동적으로 impl된다. Any가 어떻게 자동적으로 모든 타입에 대해 impl되는지와, 'static lifetime에 대해서는 다른 포스팅에서 다루도록 하겠다. 우선 지금은 lifetime을 유저가 따로 명시하지 않았으면, Any trait이 모두 적용된다고 생각해도 무방하다. 

use std::any::Any;
use std::collections::VecDeque;

struct Foo{
    x: i32,
}

fn main(){
    let mut queue:VecDeque<&dyn Any> = VecDeque::new();
    let num = 10 as i32;
    let my_string = "hello trait object!".to_string();
    let obj = Foo{x:10};

    queue.push_back(&num);
    queue.push_back(&my_string);
    queue.push_back(&obj);
}

Foo struct에서 impl Any를 명시하지 않았음에도, Any trait object로 취급되어 queue에 push가능함을 확인할 수 있다. 

 

Any trait은 또한 몇가지 더 유용한 기능을 제공하는데 다음의 코드를 보자. 

use std::any::Any;

fn check_if_string(input:&dyn Any)
{
    match input.downcast_ref::<String>() {
        Some(as_string) => {
            println!("This is string : {}", as_string);
        }
        None => {
            println!("not a string");
        }
    }
}

fn main(){
    let num = 10 as i32;
    let my_string = "hello trait object!".to_string();
    check_if_string(&num);
    check_if_string(&my_string);
}
not a string
This is string : hello trait object!

downcast_* 메소드를 통해 다운캐스팅을 할 수 있다. Any에서 다운캐스팅을 어떻게 구현하는지는 다른 포스팅에서 다루도록 하겠다.

 

[참고]

https://doc.rust-lang.org/book/ch17-02-trait-objects.html

 

Using Trait Objects That Allow for Values of Different Types - The Rust Programming Language

In Chapter 8, we mentioned that one limitation of vectors is that they can store elements of only one type. We created a workaround in Listing 8-10 where we defined a SpreadsheetCell enum that had variants to hold integers, floats, and text. This meant we

doc.rust-lang.org

 

 

'Computer Science > Rust' 카테고리의 다른 글

Rust의 Cell와 RefCell 코드 분석  (0) 2020.12.06
Unsafe Rust  (0) 2020.09.27
Rust의 async/await와 Future  (0) 2020.07.21
Rust의 Copy trait와 Clone trait  (2) 2020.06.30
Rust의 스마트 포인터  (0) 2020.05.20

+ Recent posts