Rust 기초부터 마스터까지 — 5화
제네릭, trait, 수명으로 재사용 가능한 API 만들기
본 블로그 작성 글들은, 강의를 목표로 하고 제작되었습니다. 또한 작성된 글의 정보도 틀렸을 수 있으니, 주의해주세요.
제네릭은 타입만 다른 같은 규칙을 재사용하고 trait은 타입이 제공해야 하는 동작을 정의한다. 수명 표기는 참조 사이의 관계를 컴파일러에 설명한다.
trait으로 동작 정의하기
trait Damageable {
fn is_alive(&self) -> bool;
fn take_damage(&mut self, amount: i32);
}
fn attack(target: &mut impl Damageable, damage: i32) {
if target.is_alive() {
target.take_damage(damage);
}
}
attack은 구체 타입을 몰라도 Damageable을 구현한 값을 사용할 수 있다. trait은 상속 계층보다 필요한 동작의 계약에 가깝다.
제네릭과 trait bound
fn strongest<T>(values: &[T]) -> Option<&T>
where
T: Ord,
{
values.iter().max()
}
T가 Ord를 구현한다는 조건이 있으므로 컴파일러는 값을 비교할 수 있음을 안다. 잘못된 타입 조합은 실행 전에 거부된다.
trait 구현
struct Monster {
health: i32,
}
impl Damageable for Monster {
fn is_alive(&self) -> bool {
self.health > 0
}
fn take_damage(&mut self, amount: i32) {
self.health = (self.health - amount).max(0);
}
}
데이터와 공통 계약을 분리하면 외부 타입에도 허용되는 범위에서 trait을 구현할 수 있다.
수명 관계
fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
'a는 반환 참조가 두 입력 참조보다 오래 살 수 없다는 관계를 나타낸다. 참조의 실제 수명을 늘리거나 런타임 자원을 만드는 기능은 아니다.
대부분의 단순한 수명은 컴파일러가 추론한다. 오류가 날 때 표기부터 늘리기보다 반환값이 어떤 입력을 빌리는지 API의 소유 관계를 먼저 확인해야 한다.