트레이트 객체와 다형성
공통 트레이트를 구현한 서로 다른 타입을 dyn Trait 컬렉션에 담고 동적 디스패치로 같은 인터페이스를 호출합니다.
7장에서 벡터의 제약사항 중 하나는 하나의 타입 요소만 보관할 수 있다는 점이라고 언급했습니다.
예제 7-9에서는 정수, 부동소수점, 문자를 담기 위한 배리언트를 가진 SpreadsheetCell 열거형으로 이를 해결했습니다.
덕분에 각 칸마다 다른 타입 데이터를 저장하면서도, 그 칸 묶음을 대표하는 하나의 벡터를 유지할 수 있었습니다.
이 방식은 교환 가능한 아이템 타입 집합이 컴파일 시점에 고정되어 있을 때 특히 효과적입니다.
하지만 라이브러리 사용자가 상황에 따라 유효한 타입 집합을 확장할 수 있게 하고 싶을 때도 있습니다.
이를 보여주기 위해 아이템 리스트를 순회하며 각 아이템의 draw 메서드를 호출해 화면에 그리는 GUI 도구를 예제로 만들어 보겠습니다.
우리가 만들 라이브러리 크레이트 이름은 gui이며, Button, TextField 같은 기본 타입을 제공할 수 있습니다.
동시에 gui 사용자 스스로 Image나 SelectBox 같은 새로운 그릴 수 있는 타입을 추가할 수 있어야 합니다.
이번 예제에서 완전한 GUI 라이브러리를 구현하지는 않겠지만 이 조각들이 어떻게 결합하는지 보여주고자 합니다.
라이브러리를 작성하는 시점에서는 다른 프로그래머들이 만들고자 하는 모든 타입들을 알 수 없죠.
하지만 gui가 다양한 타입들의 많은 값을 추적해야 하고, draw 메서드가 각각의 다양한 타입의 값들에 대해 호출되어야 한다는 것은 알고 있습니다.
draw 메서드를 호출했을 때 벌어지는 일에 대해서 정확히 알 필요는 없고, 그저 그 값에 호출할 수 있는 해당 메서드가 있음을 알면 됩니다.
상속이 있는 언어로 이 작업을 하기 위해서는 draw라는 이름의 메서드를 갖고 있는 Component라는 클래스를 정의할 수 있습니다.
다른 클래스들, 이를테면 Button, Image, 그리고 SelectBox 같은 것들은 Component를 상속받고 따라서 draw 메서드를 물려받게 됩니다.
이들은 각각 draw 메서드를 오버라이딩하여 그들의 고유 동작을 정의할 수 있으나, 프레임워크는 모든 타입을 마치 Component인 것처럼 다룰 수 있고 draw를 호출할 수 있습니다.
하지만 러스트에는 상속이 없는 관계로, 사용자들이 새로운 타입을 정의하고 확장할 수 있도록 gui 라이브러리를 구조화하는 다른 방법이 필요합니다.
공통된 동작을 위한 트레이트 정의하기
gui에 필요한 동작을 구현하기 위해, draw라는 이름의 메서드가 하나 있는 Draw라는 이름의 트레이트를 정의하겠습니다.
그러면 트레이트 객체(trait object)를 담는 벡터를 정의할 수 있습니다.
트레이트 객체는 특정 트레이트를 구현한 타입의 인스턴스와 런타임에 해당 타입의 트레이트 메서드를 조회하는 데 사용되는 테이블 모두를 가리킵니다.
트레이트가 dyn 호환(dyn compatible) 조건을 만족할 때, & 참조자나 Box<T> 스마트 포인터 같은 포인터 종류로 지정한 다음 dyn 키워드와 그 트레이트를 붙여 트레이트 객체를 사용할 수 있습니다. (트레이트 객체에 포인터를 사용해야 하는 이유는 18장의 ‘동적 크기 타입과 Sized 트레이트’ 절에서 설명하겠습니다.)
제네릭 타입이나 구체 타입 대신 트레이트 객체를 사용할 수 있습니다.
트레이트 객체를 사용하는 곳이 어디든, 러스트의 타입 시스템은 컴파일 타임에 해당 컨텍스트에서 사용된 모든 값이 트레이트 객체의 트레이트를 구현할 것을 보장합니다.
결론적으로 컴파일 타임에 모든 가능한 타입을 알 필요가 없습니다.
앞서 언급했듯 러스트에서는 다른 언어의 객체와 구분하기 위해 구조체와 열거형을 ‘객체’라고 부르는 것을 자제합니다.
구조체나 열거형에서는 구조체 필드의 데이터와 impl 블록의 동작이 분리되는 반면, 다른 언어에서는 데이터와 동작이 하나의 개념으로 결합한 것을 객체라고 명명하는 경우가 많으니까요.
트레이트 객체들은 데이터와 동작을 결합한다는 의미에서 다른 언어의 객체와 더 비슷합니다.
하지만 트레이트 객체는 트레이트 객체에 데이터를 추가할 수 없다는 점에서 전통적인 객체와 다릅니다.
트레이트 객체는 다른 언어들의 객체만큼 범용적으로 유용하지는 않습니다.
그들의 명확한 목적은 공통된 동작에 대한 추상화를 가능하도록 하는 것이죠.
예제 16-3은 draw라는 이름의 메서드를 갖는 Draw라는 트레이트를 정의하는
방법을 보여줍니다.
Draw 트레이트의 정의
pub trait Draw {
fn draw(&self);
}
이 문법은 9장에 있는 트레이트를 정의하는 방법에서 다뤘으니 익숙하실 겁니다.
여기서 Draw를 구현했다는 사실과 dyn Draw를 만들 수 있다는 조건을 구분해야 합니다. 예제의 draw(&self)는 타입 매개변수가 없고, 구체 구현 타입인 Self를 인수나 반환 타입에 사용하지 않으므로 트레이트 객체를 통해 호출할 수 있습니다.
반대로 fn draw<T>(&self, value: T)처럼 메서드 자체에 타입 매개변수를 추가하거나 fn duplicate(&self) -> Self처럼 구현 타입을 반환하게 하면, 그 메서드를 그대로 둔 트레이트는 dyn 호환 조건을 만족하지 않습니다. 이런 트레이트로 Box<dyn Draw>를 만들려는 시도는 E0038 오류입니다. Draw: Sized처럼 트레이트 전체에 Self: Sized를 요구해도 트레이트 객체를 만들 수 없습니다.
구체 타입에서만 쓸 메서드에는 where Self: Sized를 붙여 동적 호출 대상에서 제외할 수 있습니다. 이때도 제외된 메서드를 dyn Draw를 통해 호출할 수는 없습니다. 슈퍼트레이트의 호환성, 연관 상수와 제네릭 연관 타입 등 나머지 조건은 Rust Reference의 dyn 호환 조건에서 확인할 수 있습니다. 도식의 “dyn-compatible 메서드”는 이러한 생성·호출 경계를 가리킵니다.
다음에 새로운 문법이 등장합니다.
예제 16-4는 components라는 벡터를 보유하고 있는 Screen이라는 구조체를 정의합니다.
Box<dyn Draw> 타입의 벡터인데, 이것이 트레이트 객체입니다; 이것은 Draw 트레이트를 구현한 Box 안의 모든 타입에 대한 대역입니다.
Draw 트레이트를 구현하는
트레이트 객체들의 벡터 components를 필드로 가지고 있는 Screen
구조체의 정의
pub struct Screen {
pub components: Vec<Box<dyn Draw>>,
}
Screen 구조체에서는 예제 16-5와 같이 components의 각 요소마다
draw 메서드를 호출하는 run 메서드를 정의합니다.
draw 메서드를
호출하는 Screen의 run 메서드
impl Screen {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
이는 트레이트 바운드가 있는 제네릭 타입 매개변수를 사용하는 구조체를 정의하는 것과는 다르게 작동합니다.
제네릭 타입 매개변수는 한 번에 하나의 구체 타입으로만 대입될 수 있는 반면, 트레이트 객체를 사용하면 런타임에 트레이트 객체에 대해 여러 구체 타입을 채워 넣을 수 있습니다.
예를 들면, 예제 16-6처럼 제네릭 타입과 트레이트 바운드를 사용하여 Screen 구조체를 정의할 수도 있을 겁니다.
Screen 구조체와 run 메서드의 대체 구현
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();
}
}
}
이렇게 하면 전부 Button 타입이거나 전부 TextField 타입인 컴포넌트의 목록을 가진 Screen 인스턴스로 제한됩니다.
동일 타입의 컬렉션만 사용한다면 제네릭과 트레이트 바운드를 사용하는 것이 바람직한데, 왜냐하면 그 정의들은 컴파일 타임에 단형성화(monomorphize)되어 구체 타입으로 사용되기 때문입니다.
반면 트레이트 객체를 사용하는 메서드를 이용할 경우, 하나의 Screen 인스턴스가 Box<Button>은 물론 Box<TextField>도 담을 수 있는 Vec<T>를 보유할 수 있습니다.
이것이 어떻게 작동하는지 살펴보고 런타임 성능에 미치는 영향에 대해 설명하겠습니다.
트레이트 객체를 선택하기 전에는 왜 구체 타입을 숨겨야 하는지, 동적 디스패치 비용을 감수할 만큼 확장성이 필요한지 확인해야 합니다.
트레이트 구현하기
이제 Draw 트레이트를 구현하는 타입을 몇 가지 추가하겠습니다.
Button 타입을 제공해보겠습니다.
다시 한번 말하지만 실제 GUI 라이브러리를 구현하는 것은 이 책의 범위를 벗어나므로, draw에는 별다른 구현을 하지 않을 겁니다.
구현이 어떻게 생겼을지 상상해 보자면, Button 구조체에는 예제 16-7에서 보는 바와 같이 width, height 그리고 label 필드들이 있을 것입니다.
Draw 트레이트를 구현하는
Button 구조체
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 트레이트를 구현하겠지만 해당 타입을 그리는 방법을 정의하기 위하여 draw 메서드 내에 서로 다른 코드를 사용하게 될 것이고, Button도 여기서 그렇게 하고 있습니다. (앞서 언급한 것처럼 실질적인 GUI 코드는 없지만요.)
예를 들어, Button 타입은 추가적인 impl 블록에 사용자가 버튼을 클릭했을 때 어떤 일이 벌어질지와 관련된 메서드들을 포함할 수 있습니다.
이런 종류의 메서드는 TextField와 같은 타입에는 적용할 수 없죠.
우리의 라이브러리를 사용하는 누군가가 width, height 및 options
필드가 있는 SelectBox 구조체를 구현하기로 했다면, 예제 16-8과 같이
SelectBox 타입에도 Draw 트레이트를 구현합니다.
gui를 사용하고 Draw 트레이트를
SelectBox 구조체에 구현한 또 다른 크레이트
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// 실제로 선택 상자를 그리는 코드
}
}
이제 라이브러리 사용자는 main 함수를 작성하여 Screen 인스턴스를 만들 수 있습니다.
Screen 인스턴스에는 SelectBox와 Button을 Box<T> 안에 넣어 트레이트 객체가 되게 하여 이들을 추가할 수 있습니다.
그러면 Screen 인스턴스 상의 run 메서드를 호출할 수 있는데, 이는 각 컴포넌트에 대해 draw를 호출할 것입니다.
예제 16-9는 이러한 구현을 보여줍니다.
예제 16-9: 트레이트 객체를 사용하여 동일한 트레이트를 구현하는 서로 다른 타입들의 값 저장하기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 타입을 추가할 수도
있다는 것을 몰랐지만, 우리의 Screen 구현체는 새로운 타입에 대해서도
동작하고 이를 그려낼 수 있는데, 그 이유는 SelectBox가 Draw 트레이트를 구현했기
때문이고, 이는 draw 메서드가 구현되어 있음을 의미합니다.
이러한 개념, 즉 값의 구체적인 타입이 아닌 값이 응답하는 메시지만 고려하는 개념은 동적 타입 언어의 오리 타이핑(duck typing)이란 개념과 유사합니다.
만약 오리처럼 걷고 오리처럼 꽥꽥거리면, 그것은 오리임에 틀림없습니다!
예제 16-5에 있는 Screen에 구현된 run을 보면, run은 각 컴포넌트가 어떤 구체 타입인지 알 필요가 없습니다.
이 함수는 컴포넌트가 Button의 인스턴스인지 혹은 SelectBox의 인스턴스인지 검사하지 않고 그저 각 컴포넌트의 draw 메서드를 호출할 뿐입니다.
components 벡터에 담기는 값의 타입을 Box<dyn Draw>로 지정하는 것으로 draw 메서드의 호출이 가능한 값을 요구하는 Screen을 정의했습니다.
트레이트 객체와 러스트의 타입 시스템을 사용하여 오리 타이핑을 사용하는 코드와 유사한 코드를 작성할 때의 장점은 런타임에 어떤 값이 특정한 메서드를 구현했는지 여부를 검사하거나 값이 메서드를 구현하지 않았는데 어쨌든 호출한 경우 에러가 발생할 것을 걱정할 필요가 없다는 겁니다.
트레이트 객체가 요구하는 트레이트를 해당 값이 구현하지 않았다면 러스트는 컴파일하지 않을 겁니다.
예를 들어, 예제 16-10은 String을 컴포넌트로 사용하여 Screen을
생성하는 시도를 하면 어떤 일이 벌어지는지 보여줍니다.
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
String이 Draw 트레이트를 구현하지 않기 때문에 아래와 같은 에러를 얻게 됩니다.
$ cargo run
Compiling gui v0.1.0 (file:///projects/gui)
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`
= note: required for the cast from `String` to the object type `dyn Draw`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `gui` due to previous error
이 에러를 해결하려면 Draw를 구현한 타입을 Screen에 넣어야 합니다. gui 크레이트 안에서는 자신이 정의한 Draw를 String에 구현할 수 있습니다. 반면 gui를 사용하는 크레이트에서는 Draw와 String이 모두 외부에 정의되어 있으므로 그렇게 구현할 수 없습니다. 이것이 고아 규칙(orphan rule)이며, 사용자가 정의한 타입에 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 뒤 벡터의 다음 객체로 진행한다. |
|
|
트레이트 객체는 동적 디스패치를 수행합니다
9장의 ‘제네릭 코드의 성능’ 절에서 제네릭에 트레이트 바운드를 사용했을 때 컴파일러에 의해 수행되는 단형성화 프로세스의 실행에 대한 논의를 상기해보세요: 컴파일러는 제네릭 타입 매개변수 대신 사용하는 각 구체 타입에 대한 함수와 메서드의 비제네릭 구현체를 생성합니다.
단형성화로부터 야기된 코드는 정적 디스패치(static dispatch)를 수행하는데, 이는 호출하고자 하는 메서드가 어떤 것인지 컴파일러가 컴파일 시점에 알고 있는 것입니다.
이는 동적 디스패치(dynamic dispatch)와 반대되는 개념으로, 동적 디스패치는 컴파일러가 호출하는 메서드를 컴파일 시점에 알 수 없을 경우 수행됩니다.
동적 디스패치의 경우, 컴파일러는 런타임에 어떤 메서드가 호출되는지 알아내는 코드를 생성합니다.
트레이트 객체를 사용할 때 러스트는 동적 디스패치를 이용해야 합니다.
컴파일러는 트레이트 객체를 사용 중인 코드와 함께 사용될 수 있는 모든 타입을 알지 못하므로, 어떤 타입에 구현된 어떤 메서드가 호출될지 알지 못합니다.
대신 런타임에서, 러스트는 트레이트 객체 내에 존재하는 포인터를 사용하여 어떤 메서드가 호출될지 알아냅니다.
이러한 조회는 정적 디스패치 시에는 발생하지 않을 런타임 비용을 만들어 냅니다.
동적 디스패치의 간접 호출은 메서드 코드를 인라인(inline)으로 펼치기 어렵게 만들 수 있지만, 컴파일러가 호출 대상을 추론할 수 있는 경우 최적화될 수도 있으므로 항상 인라인이 불가능한 것은 아닙니다.
하지만, 예제 16-5와 같은 코드를 작성하고 예제 16-9와 같은 지원을 가능하게 하는 추가적인 유연성을 얻었으므로, 고려할 만한 절충안입니다.
트레이트 객체를 사용하여 다른 타입의 값 허용하기 학습 판단 기준표
트레이트 객체로 다른 타입의 값을 허용하기 위한 마지막 점검은 실제 호출 모양과 실패 처리 경계가 자연스럽게 이어지는지에 맞춥니다.
다음은 앞의 GUI 코드에서 실제 그리기 대신 타입 이름을 출력해 호출 순서만 확인하는 독립 실행 예제입니다. Screen::run의 원리는 같고, 창을 띄우는 기능은 구현하지 않습니다.
trait Draw {
fn draw(&self);
}
struct Button;
struct SelectBox;
impl Draw for Button {
fn draw(&self) {
println!("Button");
}
}
impl Draw for SelectBox {
fn draw(&self) {
println!("SelectBox");
}
}
fn main() {
let components: Vec<Box<dyn Draw>> =
vec![Box::new(SelectBox), Box::new(Button)];
for component in &components {
component.draw();
}
}풀이: 벡터 요소의 타입은 모두 Box<dyn Draw>이지만 첫 값은 SelectBox, 둘째 값은 Button을 가리킵니다. 반복문의 같은 draw() 호출이 각 값의 구현으로 이어지므로 출력은 SelectBox, Button 순서입니다. 이종 타입을 담는 경계와 런타임 호출을 기존 도식에서 각각 찾아보세요.
독립 연습: Draw에 fn inspect<T>(&self, value: T);를 추가하면 기존 Vec<Box<dyn Draw>>를 만들 수 있을까요? inspect는 구체 타입에서만 사용하고 draw만 동적으로 호출하려면 어느 메서드에 어떤 조건을 붙여야 할까요?
답안: 타입 매개변수가 있는 inspect를 그대로 추가하면 dyn 호환 조건에 어긋납니다. inspect에 where Self: Sized를 붙이고 각 구현에서 그 메서드를 제공하면 동적 호출 대상에서 제외할 수 있습니다. 트레이트 전체에 Sized를 붙이는 것은 해결책이 아닙니다. 제외된 inspect는 구체 타입에서는 호출할 수 있지만 dyn Draw에서는 호출할 수 없습니다.