Trait

Trait

Trait의 기초

여러 타입(type)의 공통된 행위/특성을 표시한 것을 Trait 라고 한다. Rust에서의 Trait 는 (약간의 차이는 있지만) 다른 프로그래밍 언어에서의 인터페이스(interface)와 비슷한 개념이다.

Trait는 trait 라는 키워드를 사용하여 선언하며, trait 블럭 안에 공통 메서드의 원형(method signature)을 갖는다. 예를 들어, 아래는 Draw 라는 Trait를 정의한 것으로 이 Trait는 draw() 라는 공통 메서드를 갖고 있다. Trait 안의 draw() 메서드는 실제로 구현되지 않았고, 다만 메서드 이름과 파라미터 및 리턴타입 만을 표시하고 있다.

trait Draw {
    fn draw(&self, x: i32, y: i32);
}

Trait는 공통된 특성을 갖는 타입(들)에 붙여 사용될 수 있다. 예를 들어, 위의 Draw 트레이트는 Rectangle이나 Circle 같은 타입에 의해 구현될 수 있을 것이다. 즉, 아래와 같이 Rectangle 이라는 구조체에 Draw 라는 trait를 구현하기 위해서는 "impl 트레이트명 for 타입명" 과 같이 정의하고 trait 안의 메서드(들)을 구현하면 된다. 만약 trait 안에 여러 개의 메서드가 있으면 (그리고 trait의 디폴트 구현이 없으면) 모든 trait 메서드를 해당 타입에서 구현하여야 한다.

struct Rectangle {
    width: i32,
    height: i32
}

impl Draw for Rectangle {
    fn draw(&self, x: i32, y: i32) { 
        let x2 = x + self.width;
        let y2 = y + self.height;
        println!("Rect({},{}~{},{})", x, y, x2, y2);
    }
}

struct Circle {
    radius: i32
}

impl Draw for Circle {
    fn draw(&self, x: i32, y: i32) { 
        println!("Circle({},{},{})", x, y, self.radius);
    }
}

위와 같이 Rectangle과 Circle 타입에 Draw 트레이트를 구현했을 때, 아래 예제와 같이 draw_it() 라는 함수는 Draw 트레이트를 갖는 서로 다른 타입들을 받아들여 공통 메서드(즉, Draw 트레이트의 메서드)를 사용할 수 있다.

아래 draw_it() 함수는 첫번째 파라미터로 Draw 트레이트를 받아들이고 있는데, 이때 트레이트를 "impl Draw"와 같이 표현한다.

fn draw_it(item: impl Draw, x: i32, y: i32) {
    item.draw(x, y);
}

fn main() {
    let rect = Rectangle { width: 20, height: 20 };
    let circle = Circle { radius: 5 };

    draw_it(rect, 1, 1);
    draw_it(circle, 2, 2);
}

만약 어떤 함수에 Draw 트레이트를 리턴한다면, 마찬가지로 "impl Draw"와 같이 표현한다.

fn draw_basic_circle() -> impl Draw {
    Circle { radius: 1 }
} 

Trait Bound

Trait Bound는 함수의 파라미터나 리턴에 트레이트를 전달하기 위한 표현 방식으로 제네릭을 사용하여 표현한다. 예를 들어, 아래 첫번째 draw_it() 함수는 위에서 설명한 "impl Draw" 문법을 사용하였고, 두번째 draw_it() 함수는 Trait Bound 문법을 사용한 것이다. 두번째의 경우 제네릭으로 Draw 트레이트의 타입 T를 정의하고, 이를 파라미터에서 사용하였다. "impl Draw" 문법은 Trait Bound 문법을 보다 간결하게 표현한 문법으로 볼 수 있다.

fn draw_it(item: impl Draw, x: i32, y: i32) {
    item.draw(x, y);
}

fn draw_it<T: Draw>(item: T, x: i32, y: i32) {
    item.draw(x, y);
}

만약 어떤 파라미터가 복수 개의 트레이트를 갖는다면 + 를 사용하여 여러 트레이트들을 지정할 수 있다. 예를 들어, 아래와 같이 Draw 트레이트와 Print 트레이트를 동시에 갖는 타입들을 받아들이고 싶다면, Draw + Print와 같이 표현할 수 있다.

trait Print {}

fn draw_it(item: (impl Draw + Print), x: i32, y: i32) {
    item.draw(x, y);
}

fn draw_it<T: Draw + Print>(item: T, x: i32, y: i32) {
    item.draw(x, y);
}

Trait Bound에서 바운드 되는 트레이트(들)을 where 뒤에 쓰는 표현도 있다. where는 타입 파라미터들이 복잡하게 앞에 정의될 때, 이를 where 뒤에 정리해서 표현할 수 있게 해준다. 위의 draw_it() 함수는 아래와 같이 where를 사용하여 표현할 수 있다.

fn draw_it<T>(item: T, x: i32, y: i32) 
   where T: Draw + Print 
{
    item.draw(x, y);
}

Trait: 디폴트 구현

Trait는 일반적으로 공통 행위(메서드)에 대해 어떠한 구현도 하지 않는다. 하지만, 필요한 경우 Trait의 메서드에 디폴트로 실행되는 구현을 추가할 수 있는데, 이 경우 Trait를 구현하는 타입에 해당 메서드를 구현하지 않는 경우, Trait에 정의된 디폴트 구현을 사용하게 된다.

예를 들어, Draw 트레이트의 draw() 메서드에 대해 아래와 같이 디폴트 구현을 추가하고, Shape와 같은 타입에 대해 별도로 Draw 트레이트의 draw() 메서드를 구현하지 않았다면, Shape 타입은 트레이트에 구현된 디폴트 구현을 사용하게 된다.

trait Draw {
    fn draw(&self, x: i32, y: i32) {
        println!("draw at {},{}", x, y);
    }
}

struct Shape {
    name: String
}

impl Draw for Shape {}

This site is not affiliated with or endorsed by the Rust Foundation or Rust Project.