Rust Chapter11: Trait
- 프로그래밍 언어 / Rust
- 2025. 1. 25.
- 들어가기 전
- 요약
- Trait 기본
- 트레이드 객체 (Trait Object)
- 제네릭 함수와 타입 매개변수
- 제네릭 함수 / Trait Object 중 어떤 것을 써야할까?
- Trait의 정의와 구현
- Trait Default Method
- Trait 구현의 법칙
- Trait와 Self
- Sub Trait
- 타입 연관 함수
- 타입 간의 관계를 정의하는 Trait
- 바운드 역설계
반응형
들어가기 전
이 글은 프로그래밍 러스트 책을 공부하고 정리한 글입니다.
요약
- Trait은 인터페이스 의미를 가짐. Type은 Struct, i32 같은 것들을 의미함.
- 디스패치 방법
- 정적 디스패치 : impl Trait, 제네릭은 정적 디스패치임. Call Site를 확인하고 필요한 타입에 대한 함수 코드를 모두 생성한다.
- 동적 디스패치 : Trait Object를 사용하는 경우. V Table은 컴파일 시점에 한번만 생성되고, Trait Object는 런타임 시점에 타입에 맞는 V Table의 주소와 함께 Fat Pointer로 생성된다. 그리고 VTable을 참조해서 메서드를 호출한다.
- dyn 키워드는 dynamic의 줄임말임.
- dyn Type은 허용되지 않음. dyn은 동적 타입이 오는데, 구현체는 실제로 여러 필드를 가지고 있을 수 있어서 컴파일 타임에 크기가 결정되지 않는다. 따라서 컴파일 에러가 발생한다.
- &dyn Type만 허용된다. 참조자는 항상 크기가 정해지기 때문이다.
- 함수에서 Trait Object 타입을 원할 때 참조자를 넘기면, 참조자는 자동으로 Trait Object로 변환된다.
- 라이프타임도 제네릭이다. 그러나 컴파일 시점에 생성되는 정적 디스패치용 코드 생성에는 영향을 미치지 않는다. 오로지 타입들만 컴파일 시점에 생성되는 함수만(Monomorphization) 영향을 미친다.
- 제네릭의 이점 (Trait Object 대비)
- 정적 디스패치를 하기 때문에 메서드 실행 속도가 빠름.
- 모든 Trait이 Trait Object를 지원하지는 않음. (예를 들어 연관 함수는 제네릭에서만 사용할 수 있음)
- 제네릭은 타입 매개변수에 여러 Trait을 Bound해서 타입을 좁힐 수 있음. (Trait Object는 지원하지 않음)
- Trait Object
- 다양한 타입의 집합으로 구성된 값을 만들 때 유용함.
- 런타임에는 구체 타입에 대한 어떠한 정보도 들어가있지 않다. Trait Object는 기존 구체 데이터를 담은 Data Pointer, 그리고 VTable을 가리기는 VPtr만 가진다.
- VTable은 컴파일 타임에 생성되고, 각 Trait Object가 사용될 Vtable은 컴파일 타임에 구체 타입을 보고 이미 결정되어있다.
- Trait도 Default Method를 지원함.
- Trait 구현 시, Impl<W: Write + ...> MyTrait for W 같은 형식으로 범용적으로 구현할 수도 있음.
- 내가 만들지 않은 타입 + 내가 정의하지 않은 Trait인 경우, 그 타입을 위한 Trait은 내가 정의할 수 없다.
- trait Child: Parent에서 Child는 Parent의 Sub Trait이다.
- 이것은 trait Child where Self: Parent와 동일한 의미를 가진다. 즉, Self(Child)는 Parent Trait에 Bound 된 것을 의미함.
Trait 기본
- Trait은 자바의 Interface와 유사 의미
- Struct는 자바의 Class와 유사 의미
trait Write {
fn write(&mut self, but: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
}
- Write Trait을 구현하는 Struct는 write(), flush() 메서드를 구현해야함.
// 제네릭 T는 Ord Trait에 Bound됨.
fn min<T: Ord>(value1: T, value2: T) -> T {
if value1 <= value2 {
value1
} else {
value2
}
}
- Generic T는 Ord Trait에 Bound 됨. 이것은 T는 Ord Trait을 구현한 타입을 의미함. 자바 Generic에서는 T extends Ord 같은 느낌
트레이드 객체 (Trait Object)
- 트레이트 객체는 런타임에 호출될 실제 메서드를 V Table을 통해서 결정함. 컴파일 되는 코드의 양은 줄어드는데, 런타임에서 그만큼 성능 열화가 있음.
pub fn hello() {
let mut buf: Vec<u8> = vec![];
// 아래 코드에서 컴파일 에러 발생.
// dyn Write는 컴파일 시점에 크기를 알 수 없다.
// 구현체는 여러 필드를 가지고 있어서, 스택 프레임에 배정된 크기가 다를 수 있기 때문이다.
// let writer: dyn Write = buf;
// Reference는 고정된 크기를 가지므로 문제가 없다.
let writer: &mut dyn Write = &mut buf;
}
- dyn 키워드는 Dynamic의 줄임말로 런타임에 뭔가 결정된다고 봐도 될 것 같음.
- dyn Write는 ' 런타임에 결정되는 Write Trait을 구현한 Struct인데'로 이해할 수 있음. 그러나 Struct는 내부 필드를 여러 개 가질 수 있고, 따라서 Struct 구현체마다 필요한 스택 프레임의 크기가 바뀔 수 있음. 이 때문에 dyn Write는 컴파일 타임에 메모리를 결정할 수 없기 때문에 컴파일 에러가 발생함.
- &dyn Write는 '런타임에 결정되는 Write Trait을 구현한 Struct의 참조자'다.
- 참조자의 크기는 일정하기 때문에 컴파일 타임에도 스택 프레임의 크기를 정할 수 있음. (따라서 문제 없음)
- V Table은 컴파일 타임에 타입별로 하나씩 생성되고, Trait Object가 생성될 때 참조자와 V Table 주소를 넘겨서 메서드 호출 시 동적 Dispatch가 일어나도록 한다.

- 위 이미지는 &mut dyn Write라는 Trait Object의 메모리 구조다.
- 스택 프레임에는 데이터 + vptr이 있음. 데이터는 힙에 저장된 Buf를 가리키고, Vptr은 동적 디스패치에 사용될 메서드를 모아둔 VTable을 가리키는 형태임.
// 함수가 Trait Object를 원할 때, 참조자를 넘기면 암묵적으로 Trait Object로 변환됨.
fn need_trait_object(writer: &mut dyn Write) { }
fn other_function() {
let mut buf: Vec<u8> = vec![];
need_trait_object(&mut buf);
}
함수가 Trait Object 타입을 원할 때, 참조자를 넘기면 참조자는 암묵적으로 Trait Object로 변환된다.
Trait Object에서 VTable이 생성되는 시점, 결정되는 시점
- 필요한 VTable은 컴파일 타임에 생성된다.
- Trait Object 캐스팅할 때 사용할 VTable은 컴파일 시점에 이미 결정된다. (아래 코드에서 자세히 확인)
- 컴파일 타임
- 컴파일러는 Trait Object로 변환되는 객체의 구체 타입을 알고 있기 때문에 Trait Object 전환 시, 어떤 VTable을 만들어야 하고 쓰여야 할지 알고 있다.
- 런타임에는 어떠한 타입 정보도 없다. (기본적으로 컴파일 타임에 모든 타입 관련이 정의됨)
- 그러나 Trait Object를 사용하는 함수 내부에서는 컴파일 타임에 구체적인 타입을 알 수 없다. 따라서 동적 디스패치가 일어난다.
- 대부분의 경우는 제네릭을 이용한 정적 디스패치로 해결할 수 있다. 그런데 하나의 Collection에서 이종 타입을 사용할 경우에 Trait Object를 사용하는데, 이런 Collection을 처리하는 함수는 결국 Trait Object를 인자로 받을 수 밖에 없다.
struct T1{}
trait Hello {
fn hello(&self) -> String; }
impl Hello for T1 {
fn hello(&self) -> String { todo!() } }
fn main() {
let k = T1 {};
// 컴파일 타임 -> 여기서 &k가 &dyn Hello로 Trait Object 변환되는 것을 확인함.
// 컴파일러는 이 때, VTable for (Hello, T1)를 생성한다.
// 컴파일 타임에 이미 &k가 &dyn Hello로 전환되는 시점에 어떤 VTable이 포함되어야 할지 안다.
// 컴파일 시점에 필요한 VTable이 생성되고, 어떤 VTable이 사용될지 결정된다.
// 런타임에는 Trait Object 변환 시, 단순히 VTable의 주소만 붙인 Fat Pointer가 생성되는 정도다.
do_it(&k)
}
fn do_it(trait_object: &dyn Hello) {
// 그러나 trait_object 관점에서는 어떤 타입이 오는지 알 수 없다.
// 따라서 이때 동적 디스패치가 일어난다.
trait_object.hello();
}
제네릭 함수와 타입 매개변수
- 타입 매개변수는 <T> 같은 것들을 의미한다.
- 타입 매개변수를 가지는 함수를 제네릭 함수라고 한다.
- 타입 매개변수에 원하는 Trait을 바운드해서 타입을 제한할 수 있음.
// 상위 10개의 값을 출력하고 싶다.
pub fn top_ten<T: Debug + Clone + Ord>(values: &Vec<T>) {
let mut k = values.to_vec();
k.sort();
println!("{:}", k);
}
- 상위 10개의 값을 출력하는 제네릭 함수를 정의하면, 다음 타입 바운드가 필요함.
- Clone : 값을 복사하기 때문.
- Ord : 정렬하기 때문
- Debug: 값을 출력하기 때문.
pub fn top_ten_with_where<T>(values: &Vec<T>)
where T: Debug + Clone + Ord
{
...
}
- 타입 매개변수에 Trait Bound가 많아지면 가독성이 떨어짐. 이를 개선하기 위해 Where 절로 뺄 수 있음.
제네릭 함수 / Trait Object 중 어떤 것을 써야할까?
- 제네릭 함수와 Trait Object는 기본적으로 다음 차이가 있다.
- 제네릭 함수는 정적 디스패치를 한다. 따라서 성능이 빠르나, 컴파일 타임 + 컴파일 용량이 더 필요할 수 있음.
- Trait Object는 동적 디스패치를 한다. 성능은 희생되나 컴파일 타임 + 컴파일 용량이 줄어듦.
- Trait Object는 다양한 타입으로 된 값들의 집합체가 필요할 때 사용하는 것이 적합함.
struct Carrot {}
struct Apple {}
trait Vegetable {}
impl Vegetable for Carrot {}
impl Vegetable for Apple {}
// 이렇게 작성된 경우, Salad의 Veggis는 한 가지 타입만 가능함.
struct Salad<V: Vegetable> {
veggies: Vec<V>
}
fn only_one_salad() {
let apples: Vec<Apple> = vec![];
let salad = Salad { veggies: apples };
}
- 위는 Trait Object가 필요한 상황임.
- 현재 구현에서 각 Vegetable Trait의 구현체는 서로 다른 크기를 가질 수 있음. 따라서 Vec<Vegetable> 내부에는 단 하나의 타입만 들어갈 수 있음.
- 하지만 Salad가 여러 Vegetable로 구현되어야 한다는 요구사항이 있다면, 이 때는 Trait Object를 사용할 수 있음.
struct SaladDynamic { veggies: Vec<Box<dyn Vegetable>> }
fn only_multiple_salad() {
let mut veggies: Vec<Box<dyn Vegetable>> = vec![];
veggies.push(Box::new(Apple {}));
veggies.push(Box::new(Carrot {}));
let salad = SaladDynamic { veggies: veggies };
}
- Box<dyn Vegetable>은 Vegetable의 Trait Object를 선언하는 문법이다. 따라서 Vec에는 여러 형태의 Vegetable이 한꺼번에 올 수도 있게 된다.
// 이건 불가능함, Trait Object는 타입 매개변수로 들어갈 수 없음.
struct SaladDynamic<V: &dyn Vegetable> { veggies: Vec<V> }
// 이것도 불가능함. dyn Vegetable은 struct 그 자체를 의미하기 때문에 크기가 다를 수 있음.
struct SaladDynamic { veggies: Vec<dyn Vegetable> }
- 참고로 위 타입은 사용할 수 없음.
Trait의 정의와 구현
struct Canvas{}
trait Visible {
fn draw(&self, canavas: &mut Canvas);
}
struct Broom{}
impl Visible for Broom {
// Impl ... For ... 에서는 Trait 메서드만 구현 가능.
// 다른 메서드는 다른 Impl Block에서 생성.
// Impl ... For ... 에서 Impl Block에 있는 메서드 호출 가능함.
fn draw(&self, canavas: &mut Canvas) { hello() }
}
impl Broom {
fn hello() { }
}
- Visible Trait을 구현하기 위해서는 Impl <Trait Name> for <Struct Name>으로 구현할 수 있음.
- 이 Impl Block에서는 Visible Trait 관련 메서드만 구현할 수 있음.
- 다른 메서드를 선언하려며 Impl <Struct Name> 블록에서 구현하면 됨.
- Impl <Struct Name> 블록에서 구현한 메서드를 Impl <Trait Name> for <Struct Name>에서도 사용할 수 있음.
// Write Trait을 구현하는 타입들을 위해 Visible Trait을 구현할 수도 있음.
impl<W: Write> Visible for W {
fn draw(&self, canavas: &mut Canvas) {
todo!()
}
}
- 제네릭을 이용해 특정 Trait을 구현한 Struct를 위한 Trait을 구현할 수도 있음.
- 위 코드는 Write Trait을 구현한 타입들을 위해 Visible Trait을 구현하는 것임.
Trait Default Method
- 자바의 인터페이스가 Default Method를 지원하듯이, Rust의 Trait도 Default 메서드를 지원한다.
struct Struct1 {}
struct Struct2 {}
trait MyInterface {
fn hello() { println!("Parent Hello"); }
}
impl MyInterface for Struct1 {}
impl MyInterface for Struct2 {
fn hello() {
println!("It's implemented");
}
}
pub fn default_call() {
Struct1::hello();
Struct2::hello();
}
- Trait에 메서드에 구현되어있고, Impl에서 그 메서드를 구현하지 않은 경우 Default 메서드가 사용된다.
Trait 구현의 법칙
- 아래 두 가지 조건을 만족하는 경우, 그 타입을 위한 Trait을 구현할 수 없다.
- 내가 만든 타입이 아니다
- 내가 만든 Trait이 아니다.
- 이것은 Orphan Rule(고아 규칙)이라고 하며, 내가 만든 타입이 아니면서 Trait이 아닌 경우에는 이미 다른 라이브러리에서 실제 구현이 존재할 수 있다. 만약 내가 그것을 구현하면, 러스트는 어떤 것을 써야할지 알 수 없기 때문에 이 경우는 사용할 수 없게 차단한다.
Trait와 Self
- 러스트는 Self 키워드를 타입으로 사용할 수 있음. Self 키워드는 자기 자신과 동일한 타입을 의미함.
- Self 키워드를 쓴(함수 파라메터 or 반환 타입) Trait은 Trait Object로 사용할 수 없음. 자세한 이유는 아래에 기술
pub trait Concat {
fn concat(&self, other: &Self) -> &Self;
}
// 컴파일 에러 발생
impl Concat for S1 {
fn concat(&self, other: &Self) -> &Self { self }
}
// 컴파일 에러 발생
impl Concat for S2 {
fn concat(&self, other: &Self) -> &Self { self }
}
fn hello() {
let mut s1 = S1 {};
let mut s2 = S2 {};
concat_do_do(&s1, &s2);
}
fn concat_do_do(s1: &dyn Concat, s2: &dyn Concat) {
let k = s1.concat(s2);
}
- Self 키워드가 있는 Trait이 Trait Object로 사용될 수 없는 이유는 위 코드를 보며 설명한다.
- 컴파일러는 컴파일 시점에 각 Trait Object에 대한 VTable을 생성한다. 여기서는 VTable Concat for S1, VTable Concat for S2가 생성된다.
- Trait Object에서 사용하는 VTable은 반드시 공통된 함수 Signature로 묶일 수 있어야한다.
- 그러나 concat() 함수에 대해 S1은 concat(&self, other: &S1) -> &S1, S2는 concat(&self, other: &S2) -> &S2로 서로 다른 Signature를 가진다. 따라서 VTable을 만들 수 없기 때문에 컴파일 에러가 발생한다.
- VTable을 생성할 때는 다음과 같은 형태로 생성한다.
- Trait의 각 함수 포인터를 Table의 인덱스와 맵핑해서 저장한다. (첫번째 인덱스 -> 첫번째 함수 포인터 이런 방식)
- 함수 포인터도 엄연히 고유의 타입을 가진다. 예를 들면 아래와 같은 형식이다.
- fn(&str, usize) -> bool
- 그런데 Self를 이용하면, 각 인덱스마다 서로 다른 함수 포인터를 저장하려고 하기 때문에 '함수 타입'의 문제가 발생해서 저자할 수 없게 된다.
Sub Trait
- 러스트에서 Sub Trait은 자바에서 부모 인터페이스를 자식 인터페이스에서 상속하는 것과 같다.
- trait Child: Parent { ... } 같은 형식으로 구현한다. 이 때 Child를 'Sub Trait'이라고 한다.
- 만약 Child Trait을 구현하려는 구체가 있다면, 반드시 Parent Trait도 함께 구현해야한다.
- 마킹용 Trait으로 사용할 수도 있을 것 같음.
- 혹은 일반적인 인터페이스의 조합처럼 사용해 볼 수도 있을 듯 함.
trait Visible {
fn hello(&self);
fn hello_default() { println!("hello_default"); }
}
trait Creature: Visible {
fn hello_creature(&self);
}
struct Struct1 {}
impl Creature for Struct1 {
fn hello_creature(&self) { todo!() }
}
// Creature를 구현하려면 반드시 Visible도 함께 구현해야 컴파일 에러가 발생하지 않음.
impl Visible for Struct1 {
fn hello(&self) { todo!() }
}
- 위 코드에서 Creature는 Visible의 Sub Trait이다.
- Creature, Visible Trait을 각각 구현해야했다.
타입 연관 함수
- 러스트의 타입 연관 함수는 자바의 Static 메서드와 동일하다고 보면 된다.
- 아래에서 &self를 인자로 받지 않는 new() 같은 함수들을 타입 연관 함수라고 하고, 사용 시에는 StringSet::new() 같은 형태로 쓸 수 있음.
trait StringSet {
fn new() -> Self;
fn contains(&self, string: &str) -> bool;
}
fn unknown_words<S: StringSet>(document: &[String], wordlist: &S) -> S {
let mut unknowns = S::new(); // 타입 연관 함수도 이렇게 호출할 수 있음.
for word in document {
if !wordlist.contains(word) { // continue }
}
};
}
- StringSet을 Trait Object로 사용하고 싶다면, 현재 구현에서는 new()가 Self를 반환하기 때문에 객체 안전성이 깨져 컴파일 에러가 발생한다.
fn new() -> Self where Self: Sized;
- 컴파일 에러는 new() 함수의 메서드 시그니처를 위와 같이 변경하면 해결된다.
- &dyn MyTrait를 사용하면 컴파일 타임에 객체 안전한 메서드들을 위한 VTable을 생성함.
- MyTrait의 new() -> Self where Self: Sized같은 메서드는 기본적으로 객체 안전하지 않음. 또한, Trait Object(&dyn MyTrait) 입장에서는 Sized와 어떠한 관련이 있는지 알 수 없음.
- 따라서 이런 경우에는 new() 메서드 자체는 객체 안전하지 않지만, 그 메서드가 &dyn MyTrait과 관련없다는 것을 where 문법을 이용해 명시해주면서 컴파일 에러를 해결할 수 있음. 즉, MyTrait을 위한 VTable에서 new() 함수가 제외되고, Trait Objectd는 new() 함수를 호출할 수 없지만, 컴파일 에러는 해결되는 형식임.
타입 간의 관계를 정의하는 Trait
- Trait은 Struct들이 구현할 메서드들을 정의하는 인터페이스 역할을 했었음.
- 때로는 Trait은 각 타입들 사이의 관계를 정의하는 역할을 하기도 함.
- 여기서 이야기하는 타입은 Trait 타입이 아니라, Trait이 어떤 타입을 가지고 있고, 어떻게 연관되어 돌아가는지 정의되는 것을 의미함.
1. 연관 타입(Iterator의 동작 방식)
trait MyIterator {
// 연관타입 Item
type Item;
// 연관타입을 반환.
// Self::Item은 독립적인 타입이 아니라, 각 Iterator 구현에 종속적인 것을 의미함.
fn next(&mut self) -> Option<Self::Item>;
}
- 이터레이터는 내부에 연관타입 Item을 가지고 있음.
- 이터레이터의 next() 함수는 다음 element를 반환하는데, 이 타입을 Item 타입으로 표현해준다.
- 각 Iterator마다 독립된 연관 타입을 가지고 있음.
// 제네릭 코드에서도 연관 타입을 사용할 수도 있음.
// 이 함수는 문맥상 Iterator가 가진 요소들을 반환하는 것이기 때문에 iterator의 연관타입 item을 사용함.
fn collect_into_vector<I: Iterator>(iter: I) -> Vec<I::Item> {
let mut results = Vec::new();
for value in iter {
results.push(value);
}
results
}
- 제네릭 코드에서 연관 타입을 사용할 수 있음
- 위 코드는 Iterator가 가지는 요소들을 Vector로 반환함.
- 이 때 Iterator가 가지는 타입은 Iterator::Item에 정의되므로, 제네릭 코드에서는 이것을 이용해서 반환 타입을 명시할 수 있음.
fn dump<I>(iter: I)
where I: Iterator
{
// 아래 코드는 컴파일 되지 않는다.
// Iterator가 제공하는 value의 타입이 Debug Trait을 구현하는지 알 수 없음.
for (index, value) in iter.enumerate() {
println!("{}, {:?}", index, value);
}
}
- 다음 코드는 컴파일 되지 않는다.
- Iterator의 Element가 println!(...) 에서 필요한 Debug Trait을 구현하고 있는지 알 수 없기 때문이다.
fn dump1<I>(iter: I)
where I: Iterator, I::Item: Debug // I가 반환하는 Item 타입이 Display 가능한지를 표현해준다.
// where I: Iterator<Item=String> // 타입 바운드의 또 다른 방법
{
for (index, value) in iter.enumerate() {
println!("{}, {:?}", index, value);
}
}
- 코드를 컴파일 하기 위해서는 Iterator::Item 타입이 Debug Trait을 구현하는지 명시해주면 된다.
2. 제네릭 트레이트 (연산자 오버로딩의 동작 방식)
// RHS = Right hand side
pub trait MyMul<RHS=Self> {
// 연산 이후의 결과 타입.
type Output;
// * 연산자를 위한 메서드.
fn mul(self, rhs: RHS) -> Self::Output;
}
- 제네릭 트레이트에서도 연관 타입을 활용할 수 있다.
- Mul은 러스트에서 '*' 연산자를 위해 제공되는 Trait이다.
- 여기서 RHS는 제네릭을 의미한다.
struct MyComplex {}
// RHS=Self로 구현되어있기 때문에
// impl MyMul for MyComplex는 impl MyMul<Complex> for MyComplex와 동일하다.
// 여기서 RHS=Self는 fn mul()의 rhs의 타입에 사용된다.
impl MyMul for MyComplex {
type Output = MyComplex;
fn mul(self, rhs: MyComplex) -> Self::Output {
todo!()
}
}
- 실제 구현은 다음과 같이 해볼 수 있음.
- 위에서 RHS=Self라고 구현했다. 따라서 impl MyMul for MyComplex에서 RHS 제네릭은 SELF가 되어서 fn mul(...) 에서 사용되는 인자는 자연스레 MyComplex가 된다.
3. impl 트레이트
- 많은 제네릭 타입을 조합하다보면 가독성이 떨어지게 된다. 이를 개선하기 위해 impl Trait을 사용할 수 있음.
- Impl Trait은 아래 위치에서 사용할 수 있다.
- 함수의 반환 타입.
- 제네릭 함수 파라메터 대신에 함수 파라메터를 impl Trait으로 받을 수 있음.
use std::iter;
use std::vec::IntoIter;
// 이 코드는 반환 타입에 대한 가독성이 나쁘다.
fn cyclical_zip(v: Vec<u8>, u: Vec<u8>) -> iter::Cycle<iter::Chain<IntoIter<u8>, IntoIter<u8>>> {
v.into_iter().chain(u.into_iter()).cycle()
}
// 가독성을 높이기 위해 Trait Object로 바꿔서 반환할 수 있음.
// 그런데 고작 이거 개선하려고 동적 디스패치로 성능을 낮게 가져가는 것은 말도 안됨.
fn cyclical_zip2(v: Vec<u8>, u: Vec<u8>) -> Box<dyn Iterator<Item=u8>> {
Box::new(v.into_iter().chain(u.into_iter()).cycle())
}
// 가독성을 높이기 위해 impl Trait을 사용함. (좀 더 추상화 됨)
// 이 함수를 사용하는 쪽에서는 반환되는 값을 Iterator<u8>을 구현하는 타입이라고 가정하고 사용해도
// 문제없이 컴파일 된다.
fn cyclical_zip3(v: Vec<u8>, u: Vec<u8>) -> impl Iterator<Item=u8> {
v.into_iter().chain(u.into_iter()).cycle()
}
- 첫번째 코드는 반환 타입이 구체적으로 나오는데 복잡해서 읽기 어렵고, 실제 어떤 타입인지 알 수 없음.
- 두번째 코드는 첫번째 코드를 개선한 버전이지만, Trait Object를 반환하기 때문에 동적 디스패치가 발생해 성능 저하가 우려됨.
- 세번째 코드는 반환 타입을 impl Trait으로 설정하여 정적 디스패치 형태로 추상화했음.
- 세번째 코드를 사용하는 쪽에서는 Iterator<u8>을 구현한 값을 가정하고 코드를 작성해도, 컴파일러가 어떠한 불만도 표하지 않는다.
pub fn make_shape(shape: &str) -> impl Shape {
match shape {
"circle" => Box::new(Circle::new()),
"triangle" => Box::new(Triangle::new()),
_ => Box::new(Rectangular::new()),
}
}
- 다만 impl trait은 위 경우에는 사용할 수 없음.
- 위 코드는 서로 다른 타입을 반환한다. 서로 다른 타입이라는 것은 서로 다른 크기를 가질 수 있기 때문에 컴파일 시점에 메모리 사용을 결정할 수 없어서 컴파일러가 불평한다.
- 따라서 위 코드는 impl Shape로 추상화를 한다고 하더라도, 반환 타입이 여러가지이기 때문에 impl Shape로 해결할 수 없다.
- 엄밀히 말하면 impl Shape가 그 문제를 해결할 수 없다고 보는 것이 맞겠다.
4. 연관 상수
- Trait에서 연관상수를 선언할 수 있음.
- Trait 구현체에서 연관상수를 오버라이드 할 수도 있음.
- 연관 상수를 일반 제네릭 함수에서도 사용할 수 있다.
- 연관 상수를 사용하기 위해서는 컴파일 시점에 타입 정보가 필요하다. 그러나 Trait Object는 컴파일 시점에 구체적인 타입을 결정할 수 없기 때문에 어떤 연관 상수를 사용하는지 결정할 수 없다. 따라서 Trait Object에서는 연관상수를 쓸 수 없다.
use std::ops::Add;
// Trait 같은 인터페이스 내부에 연관 상수를 선언할 수 있음.
trait MyFloat {
// 연관 상수
const ZERO: Self;
const ONE: Self;
const TWO: &'static i32 = &1;
}
// 연관상수도 상속받아서 오버라이드 할 수 있음.
impl MyFloat for f32 {
const ZERO: f32 = 0.0;
const ONE: f32 = 1.0;
}
// 연관 상수를 일반적인 메서드에서 사용할 수도 있음.
// 연관 상수를 사용할 때는 타입 정보가 필요하기 때문에, 컴파일 시점에 타입 정보를 알 수 있어야 한다.
// 따라서 연관 상수는 Trait Object에서는 사용할 수 없다.
fn fib<T: MyFloat + Add<Output=T>>(n: usize) -> T {
match n {
0 => T::ZERO,
1 => T::ONE,
n => fib::<T>(n-1) + fib::<T>(n-2)
}
}
바운드 역설계
- 제네릭 코드를 작성하다보면 모든 요구 사항의 하나의 Trait으로 정의되지 않아서 표현하기 어렵다. 이 때는 바운드 역설계를 해볼 수 있다.
- 아래처럼 복잡한 바운드가 있다면, 오히려 필요한 값들을 하나씩 추가하면서 바운드 역설계를 해보는 것이 좋은 방법이 된다.
use std::ops::{Add, Mul};
// 이 함수를 제네릭으로 만들고 싶다.
fn dot1(v1: &[i64], v2: &[i64]) -> i64 {
let mut total = 0;
for i in 0 .. v1.len() {
total = total + v1[i] + v2[i];
}
total
}
// 제네릭 N을 넣었지만, 제네릭 N이 Copy 가능한지 알 수 없다.
// 따라서 v1[i]라고 하면 Move를 하려고 해서 컴파일러가 거절한다.
// Copy Trait을 구현하게 되면, Move 대신에 값을 복사해서 전달한다.
fn dot2<N>(v1: &[N], v2: &[N]) -> N
where N: Copy
{
let mut total: N = 0;
for i in 0 .. v1.len() {
total = total + v1[i] * v2[i];
}
total
}
// 제네릭 N이 곱하거나 더할 수 있는 값이라는 것을 알 수 없다.
fn dot3<N>(v1: &[N], v2: &[N]) -> N
where N: Mul<Output=N> + Add<Output=N> + Copy
{
let mut total: N = 0;
for i in 0 .. v1.len() {
total = total + v1[i] * v2[i];
}
total
}
// N 타입의 total이라는 변수가 0을 가질 수 있는지 알 수 없다.
// 따라서 적절한 Default Type을 넣어주는게 좋다.
fn dot4<N>(v1: &[N], v2: &[N]) -> N
where N: Mul<Output=N> + Add<Output=N> + Copy + Default
{
let mut total: N = N::default();
for i in 0 .. v1.len() {
total = total + v1[i] * v2[i];
}
total
}
'프로그래밍 언어 > Rust' 카테고리의 다른 글
Rust Chapter9 : Struct (0) | 2025.01.04 |
---|---|
Rust : 레퍼런스(Reference) (1) | 2024.12.21 |
Rust : 소유와 이동 (2) | 2024.12.14 |