Trait 객체
Trait 객체 (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 Traitdyn Trait + Senddyn Trait + Send + Syncdyn Trait + 'staticdyn Trait + Send + 'staticdyn 'static + Traitdyn (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 + UnwindSafe와 dyn Trait + UnwindSafe + Send는 같은 타입이에요.
구현과 성능
값의 구체 타입이 무엇인지 불투명하기 때문에, 트레이트 객체는 동적 크기 타입(DST, dynamically sized type) 이에요. 좋은 트레이트 객체는 항상 어떤 포인터 뒤에서 사용돼요. 예를 들어 &dyn SomeTrait나 Box<dyn SomeTrait>처럼요.
트레이트 객체를 가리키는 포인터의 각 인스턴스는 다음 두 가지를 포함해요.
SomeTrait를 구현하는 타입T의 인스턴스를 가리키는 포인터- 가상 함수 테이블(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처럼 써요. 보통은 합리적인 기본값으로 라이프타임을 추론할 수 있게 되어 있어요.