Rust 타입 시스템의 핵심. Associated types, blanket impl, dyn vs generics의 실제 동작 원리를 살펴본다.
Add<i32>, Add<f64>를 동시에.Iterator의 Item은 하나. 코드가 단순해지고 타입 추론이 쉬워진다.
// 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}"); }
}
조건을 만족하는 모든 타입에 한꺼번에 구현하는 강력한 패턴.
// 표준 라이브러리의 실제 구현
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 덕분에 가능
fn notify(item: &T) {
println!("{}", item.summary());
}
fn notify(item: &dyn Summary) {
println!("{}", item.summary());
}
// 이종 컬렉션 — 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 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 가능
}
// 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 }
}
// 문제: 어떤 라이프타임의 참조도 받는 클로저
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 ≈ 제네릭 문법 설탕
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
#[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 수동 시 함께 |
Hash | HashMap 키로 사용 | k1==k2이면 hash도 같아야 |
Ord | 정렬, min/max | 필드 순서와 다른 정렬 기준 |
Hash와 PartialEq를 수동 구현할 때: a == b이면 반드시 hash(a) == hash(b)여야 한다. 이를 어기면 HashMap이 조용히 오작동한다.
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 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가 자동으로 따라옴
#[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)
}