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();