트레이트 객체와 다형성
공통 트레이트를 구현한 서로 다른 타입을 dyn Trait 컬렉션에 담고 동적 디스패치로 같은 인터페이스를 호출합니다.
벡터와 값 목록에서는 벡터가 한 타입의 원소만 저장할 수 있다는 한계를 이야기했습니다. 그때는 정수, 실수, 텍스트를 담는 배리언트를 가진 SpreadsheetCell 열거형을 정의해 이 한계를 우회했습니다. 그래서 각 칸에 서로 다른 타입의 데이터를 저장하면서도, 칸들의 한 줄을 나타내는 벡터를 가질 수 있었습니다. 코드를 컴파일할 때 서로 바꿔 쓸 항목들이 정해진 타입들의 집합이라면, 이것은 아주 좋은 해결책입니다.
하지만 라이브러리의 사용자가 특정 상황에 맞는 타입의 집합을 넓힐 수 있기를 바랄 때도 있습니다. 이것을 어떻게 이룰 수 있는지 보기 위해, 항목들의 목록을 돌며 각 항목에 draw 메서드를 불러 화면에 그리는 그래픽 사용자 인터페이스(GUI) 도구의 예를 만듭니다. 이것은 GUI 도구의 흔한 기법입니다. GUI 라이브러리의 구조를 담은 gui라는 라이브러리 크레이트를 만듭니다. 이 크레이트에는 사람들이 쓸 수 있는 Button이나 TextField 같은 타입이 들어 있을 수 있습니다. 또 gui의 사용자는 그릴 수 있는 자기만의 타입을 만들고 싶어 할 것입니다. 예를 들어 어떤 프로그래머는 Image를, 다른 프로그래머는 SelectBox를 더할 수 있습니다.
라이브러리를 만들 때는 다른 프로그래머가 만들고 싶어 할 모든 타입을 알고 정의할 수 없습니다. 하지만 gui가 서로 다른 타입의 여러 값을 추적해야 하고, 서로 다른 타입의 이 값들 각각에 draw 메서드를 불러야 한다는 것은 압니다. draw 메서드를 불렀을 때 정확히 무슨 일이 일어날지는 몰라도 되고, 그 값에 부를 수 있는 그 메서드가 있다는 것만 알면 됩니다.
상속이 있는 언어에서 이것을 하려면, draw라는 메서드를 가진 Component라는 클래스를 정의할 것입니다. Button, Image, SelectBox 같은 다른 클래스는 Component를 상속해 draw 메서드를 물려받습니다. 각 클래스는 자기만의 동작을 정의하도록 draw 메서드를 덮어쓸 수 있지만, 프레임워크는 그 모든 타입을 Component 인스턴스처럼 다루며 draw를 부를 수 있습니다. 하지만 러스트에는 상속이 없으므로, 사용자가 라이브러리와 호환되는 새 타입으로 라이브러리를 넓힐 수 있게 하려면 gui 라이브러리를 다른 방식으로 구조화해야 합니다.
공통된 동작을 위한 트레이트 정의하기
gui에 바라는 동작을 구현하려면, draw라는 메서드 하나를 가진 Draw라는 트레이트를 정의합니다. 그다음 트레이트 객체(trait object)를 받는 벡터를 정의할 수 있습니다. 트레이트 객체는 지정한 트레이트를 구현하는 타입의 인스턴스와, 런타임에 그 타입의 트레이트 메서드를 찾는 데 쓰는 표를 함께 가리킵니다. 트레이트 객체는 & 참조나 Box<T> 스마트 포인터 같은 어떤 종류의 포인터를 지정한 뒤, dyn 키워드와 관련된 트레이트를 적어 만듭니다. 트레이트 객체에 포인터를 써야 하는 까닭은 고급 타입에서 이야기합니다. 제네릭이나 구체적인 타입 대신 트레이트 객체를 쓸 수 있습니다. 트레이트 객체를 쓰는 곳에서는 러스트의 타입 시스템이 컴파일 시점에, 그 맥락에서 쓰이는 모든 값이 트레이트 객체의 트레이트를 구현하도록 보장합니다. 그래서 컴파일 시점에 가능한 모든 타입을 알 필요가 없습니다.
러스트에서는 구조체와 열거형을 다른 언어의 객체와 구분하려고 객체라고 부르지 않는다고 했습니다. 구조체나 열거형에서는 구조체 필드의 데이터와 impl 블록의 동작이 나뉘어 있지만, 다른 언어에서는 데이터와 동작을 하나로 합친 개념을 흔히 객체라고 부릅니다. 트레이트 객체는 데이터와 동작을 합친다는 점에서 다른 언어의 객체에 더 가깝습니다. 하지만 트레이트 객체에는 데이터를 더할 수 없다는 점에서 전통적인 객체와 다릅니다. 트레이트 객체는 다른 언어의 객체만큼 일반적으로 쓸모 있지는 않으며, 그 구체적인 목적은 공통된 동작에 대한 추상화를 가능하게 하는 것입니다.
다음은 draw라는 메서드 하나를 가진 Draw 트레이트를 정의하는 방법입니다.
pub trait Draw {
fn draw(&self);
}이 문법은 트레이트와 공통 동작에서 트레이트를 정의하는 방법을 이야기할 때 본 것과 같습니다. 다음은 새 문법입니다. 다음 코드는 components라는 벡터를 가진 Screen이라는 구조체를 정의합니다. 이 벡터의 타입은 Box<dyn Draw>이며, 이것이 트레이트 객체입니다. Box 안에서 Draw 트레이트를 구현하는 어떤 타입이든 대신할 수 있습니다.
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}Screen 구조체에는 다음 코드처럼 components 각각에 draw 메서드를 부르는 run 메서드를 정의합니다. components의 원소는 저마다 다른 타입일 수 있지만 모두 Draw를 구현하므로, 반복문은 각 원소의 실제 타입을 몰라도 draw를 부를 수 있습니다.
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}이것은 트레이트 바운드가 있는 제네릭 타입 매개변수를 쓰는 구조체를 정의하는 것과는 다르게 동작합니다. 제네릭 타입 매개변수는 한 번에 하나의 구체적인 타입으로만 대체될 수 있지만, 트레이트 객체는 런타임에 여러 구체적인 타입이 트레이트 객체를 채울 수 있게 해 줍니다. 예를 들어 다음 코드처럼 제네릭 타입과 트레이트 바운드로 Screen 구조체를 정의할 수도 있었습니다.
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where
T: Draw,
{
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}이렇게 하면 Screen 인스턴스는 모두 Button 타입이거나 모두 TextField 타입인 컴포넌트 목록만 가질 수 있습니다. 같은 타입의 컬렉션만 다룬다면 제네릭과 트레이트 바운드를 쓰는 편이 낫습니다. 정의가 컴파일 시점에 구체적인 타입을 쓰도록 단형성화되기 때문입니다.
반면 트레이트 객체를 쓰는 방법에서는, Screen 인스턴스 하나가 Box<Button>과 Box<TextField>를 함께 담은 Vec<T>를 가질 수 있습니다. 이것이 어떻게 동작하는지 보고, 그다음 런타임 성능에 미치는 영향을 이야기합니다.
트레이트 구현하기
이제 Draw 트레이트를 구현하는 타입을 더합니다. Button 타입을 제공합니다. 실제로 GUI 라이브러리를 구현하는 것은 이 교재의 범위를 벗어나므로, draw 메서드의 본문에는 쓸모 있는 구현이 없습니다. 구현이 어떤 모습일지 상상해 보자면, Button 구조체에는 다음 코드처럼 width, height, label 필드가 있을 수 있습니다. 다음은 지금까지의 src/lib.rs 전체입니다.
pub trait Draw {
fn draw(&self);
}
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// 실제로 버튼을 그리는 코드
}
}Button의 width, height, label 필드는 다른 컴포넌트의 필드와 다를 것입니다. 예를 들어 TextField 타입에는 같은 필드에 더해 placeholder 필드가 있을 수 있습니다. 화면에 그리고 싶은 타입은 저마다 Draw 트레이트를 구현하지만, Button이 여기서처럼 draw 메서드에 서로 다른 코드를 써서 그 타입을 그리는 방법을 정의합니다. 실제 GUI 코드는 이 교재의 범위를 벗어납니다. 예를 들어 Button 타입에는 사용자가 버튼을 눌렀을 때 무슨 일이 일어날지에 관한 메서드가 담긴 impl 블록이 더 있을 수 있습니다. 이런 메서드는 TextField 같은 타입에는 해당하지 않습니다.
라이브러리를 쓰는 누군가가 width, height, options 필드를 가진 SelectBox 구조체를 구현하기로 했다면, 다음 코드처럼 SelectBox 타입에도 Draw 트레이트를 구현할 것입니다.
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// 실제로 선택 상자를 그리는 코드
}
}이제 라이브러리의 사용자는 main 함수에서 Screen 인스턴스를 만들 수 있습니다. Screen 인스턴스에는 각각을 Box<T>에 넣어 트레이트 객체로 만든 SelectBox와 Button을 더할 수 있습니다. 그다음 Screen 인스턴스에 run 메서드를 부르면, 각 컴포넌트에 draw를 부를 것입니다. 다음 코드가 이 구현을 보여 줍니다.
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}라이브러리를 만들 때는 누군가 SelectBox 타입을 더할지 몰랐지만, SelectBox가 Draw 트레이트를 구현하므로 Screen 구현은 그 새 타입으로 동작해 그것을 그릴 수 있었습니다. SelectBox가 draw 메서드를 구현한다는 뜻이기 때문입니다.
값의 구체적인 타입이 아니라 값이 응답하는 메시지에만 관심을 두는 이 개념은, 동적 타입 언어의 덕 타이핑(duck typing) 개념과 비슷합니다. 오리처럼 걷고 오리처럼 꽥꽥거린다면 그것은 오리라는 것입니다. 앞의 Screen의 run 구현에서, run은 각 컴포넌트의 구체적인 타입이 무엇인지 알 필요가 없습니다. 컴포넌트가 Button의 인스턴스인지 SelectBox의 인스턴스인지 확인하지 않고, 그냥 컴포넌트에 draw 메서드를 부릅니다. components 벡터의 값의 타입으로 Box<dyn Draw>를 지정해, Screen이 draw 메서드를 부를 수 있는 값을 필요로 한다고 정의했습니다.
트레이트 객체와 러스트의 타입 시스템으로 덕 타이핑을 쓰는 코드와 비슷한 코드를 쓰는 것의 장점은, 런타임에 값이 특정 메서드를 구현하는지 확인하거나, 값이 메서드를 구현하지 않았는데 그래도 불러 오류가 날까 걱정하지 않아도 된다는 것입니다. 값이 트레이트 객체가 필요로 하는 트레이트를 구현하지 않으면, 러스트는 코드를 컴파일하지 않습니다.
예를 들어 다음 코드는 컴포넌트로 String을 써서 Screen을 만들려 할 때 무슨 일이 일어나는지 보여 줍니다.
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}String이 Draw 트레이트를 구현하지 않으므로 다음 오류가 납니다. vec! 안의 Box<String>을 components의 원소 타입인 Box<dyn Draw>로 바꿀 수 없다는 뜻이며, 오류 끝의 note 줄이 바로 이 변환을 가리킵니다.
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is not implemented for `String`
|
help: the trait `Draw` is implemented for `Button`
--> src/lib.rs:23:1
|
23 | impl Draw for Button {
| ^^^^^^^^^^^^^^^^^^^^
= note: required for the cast from `Box<String>` to `Box<dyn Draw>`
For more information about this error, try `rustc --explain E0277`.이 오류는 넘기려던 것이 아닌 무언가를 Screen에 넘기고 있으니 다른 타입을 넘겨야 한다거나, Screen이 String에 draw를 부를 수 있도록 String에 Draw를 구현해야 한다고 알려 줍니다. src/lib.rs:23:1은 앞의 src/lib.rs 전체에서 impl Draw for Button이 있는 줄입니다. 아래 그림은 트레이트 객체로 draw를 부를 때 무엇이 컴파일 시점에, 무엇이 런타임에 정해지는지 보여 줍니다.
트레이트 객체 디스패치 경계
본문 mixed Vec의 각 component.draw에서 현재 데이터/vtable에 따라 SelectBox 또는 Button 구현이 선택되는 호출 경계를 추적할 수 있다. generic 정적 대안과 String 컴파일 오류는 별도 비교로 유지한다.
ARCHITECTURE · DYNAMIC DISPATCH
Draw 구현 여부는 컴파일 때, 실제 draw 선택은 런타임에 확인한다
trait object는 포인터 뒤에서 사용하는 동적 크기 값이다. &dyn Trait 또는 Box<dyn Trait> 포인터는 데이터 포인터와 vtable 메타데이터를 가지며 dyn-compatible 메서드를 런타임에 선택한다.
구체 구현
Button, SelectBox 같은 서로 다른 로컬 타입이 Draw를 구현합니다.
이질 컬렉션
Vec<Box<dyn Draw>>은 구현 타입이 다른 값을 한 컬렉션에 담습니다.
포인터 경계
Box<dyn Draw>은 DST를 가리키는 데이터 포인터와 vtable 메타데이터를 가집니다.
런타임 dispatch
dyn-compatible draw 메서드는 vtable을 통해 실제 구현으로 연결됩니다.
정적 대안
Screen<T: Draw>는 인스턴스마다 한 T를 사용해 compile-time dispatch를 합니다.
본문의 Screen::run · 같은 draw 호출, 다른 구현
for component
in self.components.iter()
{
component.draw();
}본문 컬렉션의 SelectBox와 Button은 각각 Draw를 구현합니다. 각 Box가 가리키는 값과 연결된 vtable을 통해 해당 draw를 호출합니다. Draw 구현이 없는 String을 넣는 시도는 컴파일 때 E0277 오류입니다.
Vec<Box<dyn Draw>>은 이질 구현을 담지만 Screen<T>는 한 인스턴스에서 동질적이다. dynamic dispatch의 간접화와 보통의 inline 제한은 tradeoff이지 모든 상황의 성능 판정이 아니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 본문·주석 | Box<dyn Draw>는 DST를 가리키는 데이터 포인터와 vtable 메타데이터를 가집니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 현재 component 혼합 Vec에서 현재 trait object를 선택한다. |
|
|
| draw 선택 현재 객체의 구현이 draw 호출 대상이 된다. |
|
|
| 다음 component 현재 draw 뒤 벡터의 다음 객체로 진행한다. |
|
|
트레이트 객체 디스패치 경계
본문 mixed Vec의 각 component.draw에서 현재 데이터/vtable에 따라 SelectBox 또는 Button 구현이 선택되는 호출 경계를 추적할 수 있다. generic 정적 대안과 String 컴파일 오류는 별도 비교로 유지한다.
ARCHITECTURE · DYNAMIC DISPATCH
Draw 구현 여부는 컴파일 때, 실제 draw 선택은 런타임에 확인한다
trait object는 포인터 뒤에서 사용하는 동적 크기 값이다. &dyn Trait 또는 Box<dyn Trait> 포인터는 데이터 포인터와 vtable 메타데이터를 가지며 dyn-compatible 메서드를 런타임에 선택한다.
구체 구현
Button, SelectBox 같은 서로 다른 로컬 타입이 Draw를 구현합니다.
이질 컬렉션
Vec<Box<dyn Draw>>은 구현 타입이 다른 값을 한 컬렉션에 담습니다.
포인터 경계
Box<dyn Draw>은 DST를 가리키는 데이터 포인터와 vtable 메타데이터를 가집니다.
런타임 dispatch
dyn-compatible draw 메서드는 vtable을 통해 실제 구현으로 연결됩니다.
정적 대안
Screen<T: Draw>는 인스턴스마다 한 T를 사용해 compile-time dispatch를 합니다.
본문의 Screen::run · 같은 draw 호출, 다른 구현
for component
in self.components.iter()
{
component.draw();
}본문 컬렉션의 SelectBox와 Button은 각각 Draw를 구현합니다. 각 Box가 가리키는 값과 연결된 vtable을 통해 해당 draw를 호출합니다. Draw 구현이 없는 String을 넣는 시도는 컴파일 때 E0277 오류입니다.
Vec<Box<dyn Draw>>은 이질 구현을 담지만 Screen<T>는 한 인스턴스에서 동질적이다. dynamic dispatch의 간접화와 보통의 inline 제한은 tradeoff이지 모든 상황의 성능 판정이 아니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 본문·주석 | Box<dyn Draw>는 DST를 가리키는 데이터 포인터와 vtable 메타데이터를 가집니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 현재 component 혼합 Vec에서 현재 trait object를 선택한다. |
|
|
| draw 선택 현재 객체의 구현이 draw 호출 대상이 된다. |
|
|
| 다음 component 현재 draw 뒤 벡터의 다음 객체로 진행한다. |
|
|
Button, SelectBox, 그리고 사용자가 만든 CustomWidget 같은 서로 다른 타입이 Draw를 구현하면, Vec<Box<dyn Draw>>는 이 값들을 한 컬렉션에 담습니다. 각 값이 Draw를 구현하는지는 컴파일 시점에 확인됩니다. Box<dyn Draw>는 크기가 정해지지 않은 값을 가리키는 데이터 포인터와, 메서드를 찾는 표인 vtable의 메타데이터를 함께 가집니다. Screen::run의 component.draw()는 이 vtable을 통해 런타임에 실제 구현으로 이어지며, 이 글의 컬렉션에서는 SelectBox의 draw가 먼저, Button의 draw가 다음에 불립니다. 그림 아래쪽의 Screen<T: Draw>는 별개의 정적 경로로, 인스턴스마다 한 타입 T만 쓰며 단형성화된 호출은 인라인될 수 있습니다. 트레이트 객체로 동적으로 부를 수 있는 것은 dyn 호환(dyn-compatible) 규칙을 만족하는 메서드뿐이며, 이 규칙은 Rust Reference의 dyn 호환성에 나와 있습니다.
트레이트 객체는 동적 디스패치를 수행합니다
제네릭 데이터 타입에서 제네릭을 쓰는 코드의 성능을 이야기할 때, 컴파일러가 제네릭에 단형성화를 한다고 했습니다. 컴파일러는 제네릭 타입 매개변수 자리에 쓰는 구체적인 타입마다, 제네릭이 아닌 함수와 메서드의 구현을 만듭니다. 단형성화로 만들어진 코드는 정적 디스패치(static dispatch)를 합니다. 정적 디스패치는 컴파일 시점에 컴파일러가 어느 메서드를 부르는지 아는 것입니다. 이것은 동적 디스패치(dynamic dispatch)와 대비됩니다. 동적 디스패치는 컴파일 시점에 컴파일러가 어느 메서드를 부르는지 알 수 없는 것입니다. 동적 디스패치에서는 컴파일러가 런타임에 어느 메서드를 부를지 알아내는 코드를 내보냅니다. C++의 가상 함수 호출도 많은 컴파일러가 vtable로 처리하며, 다형성과 가상 함수에서 비교해 볼 수 있습니다.
트레이트 객체를 쓰면 러스트는 동적 디스패치를 써야 합니다. 컴파일러는 트레이트 객체를 쓰는 코드에 쓰일 수 있는 모든 타입을 알지 못하므로, 어느 타입에 구현된 어느 메서드를 불러야 하는지 모릅니다. 대신 런타임에 러스트는 트레이트 객체 안의 포인터로 어느 메서드를 부를지 알아냅니다. 이 찾기는 정적 디스패치에서는 생기지 않는 런타임 비용을 만듭니다. 동적 디스패치는 컴파일러가 메서드의 코드를 인라인하지 못하게 하며, 그러면 몇몇 최적화도 막힙니다. 또 러스트에는 동적 디스패치를 쓸 수 있는 곳에 관한 몇 가지 규칙, 곧 dyn 호환성이 있습니다. 하지만 앞의 코드에서는 추가적인 유연함을 얻었으므로, 이것은 고려해 볼 만한 트레이드오프입니다.
더 깊이: 트레이트 객체로 쓸 수 있는 트레이트의 조건
본문에서 이름만 말한 dyn 호환성(dyn compatibility)은 예전에 객체 안전성(object safety)이라고 부르던 규칙입니다. Reference의 트레이트 설명에 따르면 트레이트를 dyn Trait로 쓰려면 슈퍼트레이트가 모두 dyn 호환이어야 하고, Sized를 슈퍼트레이트로 요구하지 않아야 하며, 연관 상수와 제네릭을 가진 연관 타입이 없어야 합니다. 또 연관 함수는 모두 트레이트 객체로 디스패치할 수 있거나, 명시적으로 디스패치하지 않는다고 표시되어 있어야 합니다.
디스패치할 수 있는 메서드는 타입 매개변수를 갖지 않고, 받는 쪽 말고는 Self를 쓰지 않으며, 받는 쪽의 타입이 &Self, &mut Self, Box<Self>, Rc<Self>, Arc<Self>나 이것들을 감싼 Pin<P>여야 합니다. async fn이나 반환 자리의 impl Trait처럼 숨은 반환 타입을 가진 메서드도 디스패치할 수 없습니다. 트레이트 객체로 다루는 순간 구체적인 타입은 잊히므로, Self를 돌려주는 메서드는 어떤 타입의 값을 돌려줄지 알 수 없고, 제네릭 메서드는 타입 매개변수를 무엇으로 채울지 알 수 없기 때문입니다. Self를 돌려주는 clone 같은 메서드가 꼭 필요하다면 그 메서드에 where Self: Sized를 붙여 트레이트 객체에서는 부를 수 없는 메서드로 표시하고, 트레이트 자체는 dyn 호환으로 남겨 둘 수 있습니다.
Rust 1.86부터는 트레이트 객체를 슈퍼트레이트의 트레이트 객체로 바꾸는 업캐스팅도 됩니다. Rust 1.86 발표의 예처럼 trait Trait: Supertrait라면 &dyn Trait를 그대로 &dyn Supertrait로 돌려줄 수 있어서, 예전처럼 변환용 메서드를 따로 두지 않아도 됩니다.
정리
- 트레이트 객체
dyn Trait는Box<dyn Draw>나&dyn Draw처럼 포인터 뒤에 두며, 그 트레이트를 구현한 어떤 타입의 값이든 가리킬 수 있습니다. Vec<Box<dyn Draw>>에는 서로 다른 타입의 값을 함께 담을 수 있지만, 제네릭Screen<T: Draw>는 인스턴스마다 한 타입만 담습니다.- 트레이트를 구현하지 않은 값을 트레이트 객체로 쓰려 하면 E0277 오류가 나므로, 런타임에 메서드가 있는지 확인할 필요가 없습니다.
- 트레이트 객체는 vtable을 통해 런타임에 메서드를 고르는 동적 디스패치를 하며, 그만큼 비용이 들고 인라인 같은 최적화가 막힙니다.
- 트레이트 객체로 부를 수 있는 메서드는 dyn 호환 규칙을 따라야 합니다.
연습 문제
1. 무엇을 쓸까
(가) 모든 원소가 Circle인 도형 목록, (나) Circle, Square, Triangle이 섞인 도형 목록, (다) 라이브러리 사용자가 새 도형 타입을 더할 수 있어야 하는 목록을 저장하려 합니다. 각각 제네릭, 열거형, 트레이트 객체 가운데 무엇이 알맞나요?
해설 보기
(가)는 한 타입만 담으므로 Vec<Circle>이나 제네릭 T: Shape로 충분하며 정적 디스패치를 씁니다. (나)는 타입의 종류가 컴파일 시점에 정해져 있으므로, 세 배리언트를 가진 열거형으로 만들고 match로 다루는 방법이 알맞습니다. (다)는 라이브러리를 만들 때 모든 타입을 알 수 없으므로 Vec<Box<dyn Shape>>처럼 트레이트 객체를 씁니다.
2. 오류를 고치기
이 글의 String을 컴포넌트로 넣는 코드를 컴파일되게 하려면 어떤 방법이 있나요?
해설 보기
gui 라이브러리 바깥의 코드에서는 외부 트레이트 Draw를 외부 타입 String에 직접 구현할 수 없습니다. 그래서 struct Label(String);처럼 String을 감싼 자기 타입을 만들고 impl Draw for Label을 구현한 뒤 Box::new(Label(String::from("Hi")))를 넣습니다. gui 라이브러리 안이라면 impl Draw for String을 직접 구현할 수도 있습니다.
다음 글 객체 지향 디자인 패턴에서는 트레이트 객체로 상태 패턴을 구현하고, 러스트의 타입으로 상태를 나타내는 방식과 비교합니다.