Trait 객체

Trait 객체 (Trait Objects)

다형성을 다룰 때 "여러 타입들이 공통 트레이트 하나로 묶여서 동작하게 하고 싶다"는 요구가 생겨요. 그럴 때 트레이트 객체가 해답이 돼요. 트레이트 객체는 분명한 구체 타입이 무엇인지는 숨긴 채, 어떤 트레이트들을 구현한다는 사실만으로 값을 다루는 불투명한 값이에요.

출처: Rust Reference - Trait objects

본문

구문 (Syntax)

TraitObjectType → Bounds¹ | dyn² Bounds?

TraitObjectTypeOneBound → TraitBound¹ | dyn² TraitBound?

트레이트 객체는 "어떤 트레이트 집합을 구현하는 다른 타입의 불투명한 값"이에요. 이 트레이트 집합은 dyn 호환 기본 트레이트(dyn compatible base trait) 하나와, 그 위에 붙는 여러 개의 auto 트레이트들로 이루어져요. 트레이트 객체는 기본 트레이트와 그 auto 트레이트, 그리고 기본 트레이트의 모든 슈퍼트레이트를 구현해요.

여기에는 몇 가지 규칙이 있어요.

  • 트레이트 바운드가 최소 하나는 있어야 해요.
  • non-auto 트레이트는 하나를 넘을 수 없어요.
  • 라이프타임도 하나를 넘을 수 없어요.
  • opt-out 바운드(예: ?Sized)와 use<..> 바운드는 허용되지 않아요.

예를 들어 트레이트 Trait가 있다고 하면, 다음은 모두 유효한 트레이트 객체예요.

  • dyn Trait
  • dyn Trait + Send
  • dyn Trait + Send + Sync
  • dyn Trait + 'static
  • dyn Trait + Send + 'static
  • dyn 'static + Trait
  • dyn (Trait)

에디션 차이

  • 2021 에디션 이전에는 dyn 키워드를 생략할 수 있었어요. 2021 에디션부터는 dyn의미상 필수가 되어요.
  • 2015 에디션에서는 dyn이 그다음에 PathIdentSegment, LIFETIME_TOKEN, for, ( 또는 ?가 올 때만 키워드로 해석되고, 그 외에는 일반 식별자처럼 취급됐어요. 그래서 dyn, dyn::T, dyn<T>는 모두 타입 경로로 처리됐어요. 만약 ::module::Trait 트레이트를 가진 트레이트 객체를 원한다면 괄호로 감싸 dyn (::module::Trait)처럼 써야 했죠.
  • 2018 에디션부터 dyn은 진짜 키워드가 되어 경로에 쓸 수 없게 됐고, 그래서 괄호가 필요 없어졌어요.

별칭 관계

두 트레이트 객체 타입은 기본 트레이트가 서로 별칭 관계이고, auto 트레이트 집합이 같고, 라이프타임 바운드가 같을 때 서로를 별칭한다고 봐요. 예를 들어 dyn Trait + Send + UnwindSafedyn Trait + UnwindSafe + Send는 같은 타입이에요.

구현과 성능

값의 구체 타입이 무엇인지 불투명하기 때문에, 트레이트 객체는 동적 크기 타입(DST, dynamically sized type) 이에요. 좋은 트레이트 객체는 항상 어떤 포인터 뒤에서 사용돼요. 예를 들어 &dyn SomeTraitBox<dyn SomeTrait>처럼요.

트레이트 객체를 가리키는 포인터의 각 인스턴스는 다음 두 가지를 포함해요.

  1. SomeTrait를 구현하는 타입 T의 인스턴스를 가리키는 포인터
  2. 가상 함수 테이블(vtable)SomeTrait와 그 슈퍼트레이트의 각 메서드에 대해 T의 구현(즉 함수 포인터)을 담고 있는 테이블

트레이트 객체의 목적은 메서드의 지연 바인딩(late binding) 을 허용하는 거예요. 트레이트 객체에 메서드를 호출하면 런타임에 가상 디스패치(virtual dispatch) 가 일어나요. 즉 트레이트 객체의 vtable에서 함수 포인터를 꺼내 간접적으로 호출하는 방식이에요. 각 vtable 항목의 실제 구현은 객체마다 다를 수 있어요.

예시를 볼게요.

trait Printable {
    fn stringify(&self) -> String;
}

impl Printable for i32 {
    fn stringify(&self) -> String { self.to_string() }
}

fn print(a: Box<dyn Printable>) {
    println!("{}", a.stringify());
}

fn main() {
    print(Box::new(10) as Box<dyn Printable>);
}

이 예시에서 Printable 트레이트는 print 함수의 타입 시그니처와 main의 캐스트 표현식, 두 곳 모두에서 트레이트 객체로 등장해요.

트레이트 객체의 라이프타임 바운드

트레이트 객체는 참조를 포함할 수 있으므로, 그 참조들의 라이프타임을 트레이트 객체의 일부로 표현해야 해요. 이 라이프타임은 Trait + 'a처럼 써요. 보통은 합리적인 기본값으로 라이프타임을 추론할 수 있게 되어 있어요.

더 알아보기 (Learn more)