모듈을 나눈 뒤에는 "서로 다른 타입이지만 같은 행동을 공유하는 경우"가 자주 생깁니다. Rust의 제네릭과 트레이트는 이 문제를 해결하는 기본 도구입니다. 이번 편에서는 제네릭 함수·구조체를 작성하고, trait을 통해 공통 행동을 정의한 뒤, [[impl|impl]] Trait와 trait bound를 읽는 연습을 합니다.
제네릭이 필요한 순간
다음 코드는 Vec<i32>만 정렬합니다.
fn pick_max(a: i32, b: i32) -> i32 {
if a > b { a } else { b }
}
문자열이나 사용자 정의 타입에도 같은 비교 로직을 적용하려면 제네릭 함수가 필요합니다.
fn pick_max<T: PartialOrd>(a: T, b: T) -> T {
if a > b { a } else { b }
}
T는 타입 매개변수입니다.T: PartialOrd는 "T가 비교 연산을 지원해야 한다"는 제약(trait bound)입니다.- 컴파일 시점에 각 타입별로 실제 함수가 생성되므로, 런타임 비용 없이 재사용을 늘릴 수 있습니다.
구조체와 열거형에 제네릭 적용하기
struct Pair<T> {
left: T,
right: T,
}
impl<T> Pair<T> {
fn new(left: T, right: T) -> Self {
Self { left, right }
}
}
impl<T: PartialOrd> Pair<T> {
fn max(&self) -> &T {
if self.left >= self.right { &self.left } else { &self.right }
}
}
impl<T> Pair<T>블록은 모든T에 대해 유효합니다.- [[impl|impl]]:
PartialOrd>블록처럼 추가 제약이 필요할 때는 별도의 impl 블록을 만듭니다. 즉,new는 모든 타입에서 쓸 수 있지만max는 비교 가능한 타입에서만 열립니다. enum Option<T>가 대표적인 제네릭 열거형의 예시입니다.
Trait으로 행동 정의하기
Trait은 "이 타입이 어떤 메서드를 제공하는가"를 설명합니다.
trait Printable {
fn format(&self) -> String;
}
struct User {
name: String,
}
impl Printable for User {
fn format(&self) -> String {
format!("사용자: {}", self.name)
}
}
fn print_item(item: &impl Printable) {
println!("{}", item.format());
}
impl Printable for User는User가Printable을 충족한다는 선언입니다.&impl Printable은 trait을 만족하는 모든 타입을 받아들입니다.fn print_item<T: Printable>(item: &T)형태로도 동일한 제약을 표현할 수 있습니다.
입문 단계에서는 이렇게 기억하면 충분합니다.
impl Trait: 짧고 읽기 쉬운 함수 시그니처를 원할 때T: Trait+where: 제약이 많아지거나 여러 타입을 함께 다룰 때
Trait bound 읽기 연습
fn log_all<T, I>(iter: I)
where
T: Printable,
I: IntoIterator<Item = T>,
{
for item in iter {
println!("{}", item.format());
}
}
where절은 제약이 길어질 때 가독성을 높여줍니다.IntoIterator<Item = T>는 "iter가 반복되면T값을 내놓는다"는 뜻입니다.- 이러한 제약을 명시하면, 나중에 모듈 경계에서 의도치 않은 타입이 들어오는 일을 막을 수 있습니다.
기본 메서드와 연관 타입
Trait은 기본 구현과 연관 타입도 제공합니다.
trait Repository {
type Item;
fn find_all(&self) -> Vec<Self::Item>;
fn count(&self) -> usize {
self.find_all().len()
}
}
type Item;은 trait을 구현하는 타입이 스스로 채워야 하는 자리입니다. 제네릭이 호출 시점에 타입을 바꾸는 느낌이라면, 연관 타입은 "이 구현에서는 이 타입을 쓴다"고 미리 정해 두는 방식에 가깝습니다.- 기본 메서드는
impl에서 재정의하지 않아도 자동으로 사용할 수 있습니다.
Blanket impl과 표준 trait
impl<T: Display> Printable for T { ... }처럼 조건에 맞는 모든 타입에 trait을 구현하는 패턴을 blanket impl이라고 합니다.- 표준 trait (
Display,Debug,Default,Iterator등)을 먼저 익히면, 직접 trait을 만들 때 어떤 API 표면을 제공해야 할지 감이 잡힙니다.
CodeSandbox로 이어서 실습하기
아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo check와 cargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요