JS, TS랑 비교하며 Rust 공부하기 (8)

트레이트

트레이트는 어떤 타입이 공통적으로 가질 수 있는 기능(메서드)을 정의한 것이다.
타입이 어떤 트레이트를 사용하려면, 그 트레이트에 정의된 메서드를 직접 구현해야 한다.
약간의 차이는 있지만 TS의 인터페이스와 유사하다.

pub trait Summary {
    fn summarize(&self) -> String;
}

특정 타입에 트레이트를 구현하는 방법은 impl뒤에 구현하고자 하는 트레이트 이름을 적고 그다음 for키워드와 트레이트를 구현할 타입을 명시하면 된다.

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

트레이트가 구현된 타입을 사용할 때는 타입과 트레이트를 스코프로 가져와야 한다.

use aggregator::{Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

트레이트 구현에는 충돌 방지를 위해서 한 가지 제약사항이 있는데, 이는 트레이트나 트레이트를 구현할 타입 둘 중 하나는 반드시 현재 크레이트에 포함되어 있어야 해당 타입에 대한 트레이트를 구현할 수 있다는 것이다.

impl Display for Vec<i32> { // 불가능
    // ...
}
// 현재 크레이트에 포함된 타입
struct MyType;

impl Display for MyType { // 가능
    // ...
}
// 현재 크레이트에 포함된 트레이트
trait MyTrait {
    fn hello(&self);
}

impl MyTrait for Vec<i32> { // 가능
    fn hello(&self) {
        println!("hello from vec!");
    }
}

기본 구현

트레이트의 메서드에 기본 동작을 정의할 수 있다.
기본 구현 내부에 트레이트의 다른 메서드를 호출할 수도 있다.

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

트레이트 바운드

트레이트 바운드로 함수의 제네릭 타입 매개변수에 특정 트레이트를 구현해야 한다는 제약을 줄 수 있다.
트레이트 바운드를 여러 개 지정하고 싶다면 + 구문을 사용하면 된다.

fn foo<T: Trait>(x: T)
fn bar<T: TraitA + TraitB>(x: T)

impl Trait 문법으로 간단하게 작성할 수 있다.

fn foo(x: impl Trait)
fn bar(x: impl TraitA + TraitB)

where 절을 사용하면 길어진 트레이트 바운드를 정리할 수 있다.

fn foo<T, U>(x: T, y: U)
where
    T: TraitA + TraitB,
    U: TraitC,

impl 블록에 트레이트 바운드를 지정하면 해당 조건을 만족하는 타입만 특정 메서드를 사용할 수 있다.

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

특정 트레이트를 구현한 타입에 한해서, 다른 트레이트를 구현할 수 있도록 조건을 걸 수 있다.

use std::fmt::Display;

struct MyType;

impl Display for MyType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "MyType입니다!")
    }
}

// 내가 정의한 트레이트
trait MyTrait {
    fn hello(&self);
}

// 조건부 구현: Display를 구현한 타입만 MyTrait도 구현 가능
impl<T: Display> MyTrait for T {
    fn hello(&self) {
        println!("Hello from a Display type: {}", self);
    }
}

fn main() {
    let value = MyType;
    value.hello();
}

트레이트 객체

트레이트 객체는 특정 트레이트를 구현한 타입을 런타임에 다형적으로 다루기 위한 방법이다. 구체 타입을 알지 못하더라도, 해당 타입이 어떤 트레이트를 구현하고 있다는 사실만으로 값을 사용할 수 있게 해준다.

트레이트 객체는 실제 값을 가리키는 포인터와, 그 값이 구현한 메서드들을 모아둔 가상 메서드 테이블(vtable)에 대한 포인터로 이루어져 있다. 이 vtable을 통해 컴파일 시점이 아니라 런타임에 어떤 메서드를 호출할지가 결정된다.

트레이트 객체는 dyn 키워드를 붙여 트레이트를 명시하면 되며, 크기가 고정되어 있지 않으므로(?Sized) 반드시 포인터를 통해서만 다룰 수 있다. &dyn Trait, Box<dyn Trait>, Rc<dyn Trait> 같은 식으로 작성하면 된다.

제네릭 타입이나 트레이트 바운드와 달리, 트레이트 객체는 컴파일 타임에 모든 구체 타입을 알 필요가 없다. 따라서 런타임에 다양한 타입을 하나의 인터페이스로 다룰 수 있는 장점이 있다. 다만 동적 디스패치가 발생하므로, 제네릭을 사용할 때보다 성능상 오버헤드가 생긴다.

pub trait Draw {
    fn draw(&self);
}

pub struct Screen {
    pub components: Vec<Box<dyn Draw>>,
}

pub struct Button {
    pub width: u32,
    pub height: u32,
    pub label: String,
}

impl Draw for Button {
    fn draw(&self) {
        // 실제로 버튼을 그리는 코드
    }
}

struct SelectBox {
    width: u32,
    height: u32,
    options: Vec<String>,
}

impl Draw for SelectBox {
    fn draw(&self) {
        // 실제로 선택 상자를 그리는 코드
    }
}

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"),
        }),
    ],
};

for component in screen.components.iter() {
    component.draw();
}

트레이트 상속

트레이트 상속으로 한 트레이트가 다른 트레이트를 필요로 한다는 제약 조건을 추가할 수 있다.
trait A: B 형태로 작성하며, 이는 “A를 구현하려면 B도 함께 구현해야 한다”는 의미다.
상속을 통해 상위 트레이트의 메서드를 하위 트레이트 안에서 바로 사용할 수 있다.
트레이트 상속은 표준 라이브러리의 Ord: Eq + PartialOrd처럼, 기능 간의 계층 구조를 명확하게 표현할 때 자주 쓰인다.

use std::fmt::Display;

trait Printable: Display {
    fn print(&self) {
        println!("{}", self);
    }
}

impl Printable for i32 {}

42.print();