트레이트 심화 Advanced Traits

Rust 타입 시스템의 핵심. Associated types, blanket impl, dyn vs generics의 실제 동작 원리를 살펴본다.

1. Associated Types

왜 제네릭 파라미터 대신 associated type?
제네릭은 "이 타입으로 한 트레이트를 여러 번 구현 가능" — Add<i32>, Add<f64>를 동시에.
Associated type은 "구현당 한 번만" — IteratorItem은 하나. 코드가 단순해지고 타입 추론이 쉬워진다.
// Associated type — Iterator::Item
trait Iterator {
    type Item; // 구현자가 타입 결정
    fn next(&mut self) -> Option;
}

// 제네릭이었다면 모든 사용처에서 타입 명시해야 함
// fn sum>(iter: I) -> i32 { ... }
// Associated type이면:
fn sum>(iter: I) -> i32 {
    iter.fold(0, |a, x| a + x)
}

// Add 트레이트: Output은 associated type, Rhs는 제네릭
// → 동일 타입끼리 더할 때 Output 한 번만 정해도 됨
use std::ops::Add;

#[derive(Clone, Copy)]
struct Vec2(f64, f64);

impl Add for Vec2 {
    type Output = Vec2; // associated type
    fn add(self, rhs: Vec2) -> Vec2 {
        Vec2(self.0 + rhs.0, self.1 + rhs.1)
    }
}
// 특정 Item 타입으로 구체화해서 바운드 걸기
fn print_all(iter: I)
where
    I: Iterator,
    I::Item: std::fmt::Display, // associated type에 바운드
{
    for item in iter { println!("{item}"); }
}

2. Blanket Implementation

조건을 만족하는 모든 타입에 한꺼번에 구현하는 강력한 패턴.

// 표준 라이브러리의 실제 구현
impl ToString for T {
    fn to_string(&self) -> String {
        format!("{}", self)
    }
}
// → Display만 구현하면 .to_string() 자동으로 사용 가능
// 42.to_string(), true.to_string(), 3.14.to_string() 모두 작동
// 직접 작성하는 blanket impl
trait Summary {
    fn summary(&self) -> String;
}

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

// Summary를 구현한 모든 타입에 Printable 자동 제공
impl Printable for T {}

// → 이제 Summary만 구현하면 .print() 가능
struct Article { title: String }
impl Summary for Article {
    fn summary(&self) -> String { self.title.clone() }
}
let a = Article { title: "Rust".into() };
a.print(); // blanket impl 덕분에 가능
Coherence (Orphan Rule): 트레이트와 타입 중 최소 하나는 현재 크레이트에 있어야 한다. 외부 트레이트를 외부 타입에 구현하는 건 불가 → 뉴타입 패턴으로 우회.

3. Trait Object vs Generic

정적 디스패치 (Generic)

fn notify(item: &T) {
    println!("{}", item.summary());
}
  • 컴파일 타임에 타입 확정
  • 단형화(monomorphization): 타입별 함수 복사본 생성
  • 인라이닝 가능 → 빠름
  • 바이너리 크기 증가 가능

동적 디스패치 (dyn Trait)

fn notify(item: &dyn Summary) {
    println!("{}", item.summary());
}
  • 런타임에 vtable로 함수 찾음
  • 단일 함수, 어떤 타입이든 받음
  • vtable 조회 비용 (나노초)
  • 이종 컬렉션 가능
// 이종 컬렉션 — dyn Trait만 가능
trait Shape { fn area(&self) -> f64; }
struct Circle { r: f64 }
struct Rect { w: f64, h: f64 }
impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.r * self.r } }
impl Shape for Rect   { fn area(&self) -> f64 { self.w * self.h } }

// Vec에 Circle과 Rect 동시에 담기
let shapes: Vec> = vec![
    Box::new(Circle { r: 3.0 }),
    Box::new(Rect { w: 4.0, h: 5.0 }),
];
let total: f64 = shapes.iter().map(|s| s.area()).sum();

Object Safety — dyn Trait으로 쓸 수 없는 경우

// Object unsafe: 제네릭 메서드가 있으면 dyn 불가
trait BadTrait {
    fn compare(&self, other: T) -> bool; // T를 모름 → vtable 만들 수 없음
}
// let x: &dyn BadTrait; // 컴파일 에러

// Object unsafe: Self를 반환 타입에 사용
trait Clone { // Clone이 dyn Clone 안 되는 이유
    fn clone(&self) -> Self; // Self의 크기를 모름
}

// 해결: where Self: Sized 조건 추가
trait MyTrait {
    fn to_owned(&self) -> Self where Self: Sized; // dyn에서 제외
    fn describe(&self) -> String; // 이건 dyn 가능
}

4. 고급 트레이트 바운드

// where 절 — 복잡한 바운드 정리
fn complex(t: &T, u: &U) -> V
where
    T: Clone + std::fmt::Debug + Send + 'static,
    U: Iterator,
    V: FromIterator + Default,
{
    // ...
    V::default()
}
// 라이프타임 바운드
fn longest_with_announcement<'a, T>(
    x: &'a str,
    y: &'a str,
    ann: T,
) -> &'a str
where
    T: std::fmt::Display,
{
    println!("{ann}");
    if x.len() > y.len() { x } else { y }
}

HRTB — Higher-Ranked Trait Bounds

// 문제: 어떤 라이프타임의 참조도 받는 클로저
fn apply_to_str(f: F) -> String
where
    F: for<'a> Fn(&'a str) -> &'a str, // HRTB
{
    f("hello world").to_string()
}

// 일반적으로는 명시할 필요 없음 — 컴파일러가 추론
// Fn(&str) -> &str 만 써도 보통 됨
// HRTB는 trait object를 반환하거나 저장할 때 자주 필요

impl Trait — 인수 vs 반환

// 인수 위치: &impl Trait ≈ 제네릭 문법 설탕
fn print_it(item: &impl std::fmt::Display) { println!("{item}"); }
// 위는 아래와 동일:
fn print_it(item: &T) { println!("{item}"); }

// 반환 위치: "이 트레이트를 구현하는 어떤 타입" 반환 (불투명 타입)
fn make_adder(x: i32) -> impl Fn(i32) -> i32 {
    move |y| x + y
}
// 클로저의 실제 타입을 몰라도 됨, 컴파일러가 단일 타입으로 고정

// 주의: impl Trait 반환은 단일 타입만 가능
fn bad(flag: bool) -> impl Display {
    if flag { 1i32 } else { "nope" } // 에러: 두 가지 다른 타입
}
// 여러 타입 반환해야 할 때는 Box

5. Derive 매크로와 트레이트

#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
struct Version {
    major: u32,
    minor: u32,
    patch: u32,
}
Derive제공하는 것수동 impl 해야 할 때
Debug{:?} 포맷필드 일부 숨기고 싶을 때
Clone.clone()포인터/리소스 deep copy 등
PartialEq== !=동등 비교 정의가 복잡할 때
Eq완전 동등성 마커PartialEq 수동 시 함께
HashHashMap 키로 사용k1==k2이면 hash도 같아야
Ord정렬, min/max필드 순서와 다른 정렬 기준
HashPartialEq를 수동 구현할 때: a == b이면 반드시 hash(a) == hash(b)여야 한다. 이를 어기면 HashMap이 조용히 오작동한다.

6. Operator Overloading

use std::ops::{Add, Mul, Neg, Index};

#[derive(Clone, Copy, PartialEq, Debug)]
struct Matrix2x2([[f64; 2]; 2]);

impl Add for Matrix2x2 {
    type Output = Matrix2x2;
    fn add(self, rhs: Matrix2x2) -> Matrix2x2 {
        Matrix2x2([
            [self.0[0][0] + rhs.0[0][0], self.0[0][1] + rhs.0[0][1]],
            [self.0[1][0] + rhs.0[1][0], self.0[1][1] + rhs.0[1][1]],
        ])
    }
}

impl Index<(usize, usize)> for Matrix2x2 {
    type Output = f64;
    fn index(&self, (r, c): (usize, usize)) -> &f64 {
        &self.0[r][c]
    }
}

let m = Matrix2x2([[1.0, 2.0], [3.0, 4.0]]);
println!("{}", m[(0, 1)]); // 2.0

Deref 연쇄

// Deref coercion: 컴파일러가 자동으로 deref 체인을 따라감
// Box → String → str

fn takes_str(s: &str) { println!("{s}"); }

let boxed = Box::new(String::from("hello"));
takes_str(&boxed); // &Box → &String → &str 자동 변환

// 직접 Deref 구현
struct MyBox(T);

impl std::ops::Deref for MyBox {
    type Target = T;
    fn deref(&self) -> &T {
        &self.0
    }
}

let b = MyBox(String::from("world"));
takes_str(&b); // &MyBox → &String → &str

From / Into — 타입 변환의 관용 패턴

// From을 구현하면 Into가 자동으로 따라옴
#[derive(Debug)]
struct Email(String);

impl From<&str> for Email {
    fn from(s: &str) -> Self {
        assert!(s.contains('@'), "유효하지 않은 이메일");
        Email(s.to_string())
    }
}

let e: Email = "user@example.com".into(); // Into 자동 사용
let e = Email::from("user@example.com"); // From 직접 사용

// 에러 타입 변환에도 활용 (? 연산자가 내부적으로 From 사용)
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
}

impl From for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

fn read_port() -> Result {
    let s = std::fs::read_to_string("port.txt")?; // io::Error → AppError 자동
    let port: u16 = s.trim().parse()?; // ParseIntError → AppError 자동
    Ok(port)
}