Skip to content

Latest commit

 

History

History
244 lines (184 loc) · 7.42 KB

Trait.md

File metadata and controls

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

  • Trait는 trait 라는 키워드를 사용하여 선언하며, trait 블럭 안에 공통 메서드의 원형(method signature)을 갖는다

    trait Draw {
        fn draw(&self, x: i32, y: i32);
    }
  • Rectangle 이라는 구조체에 Draw 라는 trait를 구현하기 위해서는 impl 트레이트명 for 타입명과 같이 정의하고 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);
        }
    }
  • 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);
    }

Trait Bound

  • 제네릭에 Trait Bound를 추가해서 사용할 수도 있다.
    • 복수 개의 Trait을 갖는다면 +를 사용하여 여러 Trait을 지정할 수 있다.
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);
}

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

// 이렇게 쓰는 것도 가능
fn draw_it<T>(item: T, x: i32, y: i32) 
   where T: Draw + Print 
{
    item.draw(x, y);
}

dyn

  • Trait Bound, impl Trait을 사용하는 것은 결과적으로 정적 디스패치를 구현하는 것이다. 즉, 컴파일러가 컴파일 타임에 타입들을 검사하고 내부적으로 명시된 타입들에 대한 코드를 구현하는 것이다.
  • Rust에서 동적 디스패치를 구현하기 위해서는 dyn 키워드를 사용해야한다.
  • 이는 컴파일 비용을 줄이는 대신 런타임 비용을 증가시킨다.
  • dyn Trait의 참조자는 인스턴스 객체를 위한 포인터와 vtable을 가리키는 포인터 총 두 개의 포인터를 갖는다. 그리고 런타임에 이 함수가 필요해지면 vtable을 참조해 포인터를 얻게 된다.
fn get_car(is_sedan: bool) -> Box<dyn Car>{
    if is_sedan {
        Box::new(Sedan)
    } else {
        Box::new(Suv)
    }
}

디폴트 구현

  • Trait은 일반적으로 공통 행위(메서드)에 대해 어떠한 구현도 하지 않는다.
  • 하지만, 필요한 경우 Trait의 메서드에 디폴트로 실행되는 구현을 추가할 수 있다.

뉴타입 패턴 (newtype pattern)

  • 튜플 구조체 내에 새로운 타입을 만드는 뉴타입 패턴을 사용하면 외부 타입에 대해 외부 트레잇을 구현할 수 있다.

  • Vec에 대하여 Display을 구현하고 싶다고 가정해보자.

    • Display 트레잇과 Vec 타입은 라이브러리에 정의되어 있기 때문에 바로 구현하는 것은 불가능하다.
    • 이때 뉴타입 패턴을 적용하여 Vec의 인스턴스를 가지고 있는 Wrapper 구조체를 만들 수 있다. 그리고 Wrapper 상에 Display를 구현하고 Vec 값을 이용할 수 있여 구현할 수 있다.
use std::fmt;

struct Wrapper(Vec<String>);

impl fmt::Display for Wrapper {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "[{}]", self.0.join(", "))
    }
}

fn main() {
    let w = Wrapper(vec![String::from("hello"), String::from("world")]);
    println!("w = {}", w);
}

Derive

  • Rust는 derive 키워드를 사용해서 구조체가 특정 기본 trait 구현 기능을 사용하도록 할 것인지를 정의할 수 있도록 한다.
  • 구조체 위에 #[derive()]와 같이 적고, 괄호 안에 구현할 항목을 ,로 구분하여 적으면 된다.
    • 가능한 항목은 아래와 같은 것들이 있다.

      • Eq, PartialEq, Ord, PartialOrd: 동등, 순서 비교
      • Clone: &T로부터 T를 복사하는 메서드를 사용하는지 여부
      • Copy: 소유권 이전(move) 대신 copy가 동작하도록 할 것인지 여부
      • Hash: &T로부터 hash를 계산할 것인지 여부
      • Default: data type 대신 빈 instance를 사용할 수 있게 할 것인지 여부
      • Debug: {:?} 포매터에 대한 출력을 사용하는지 여부
      // `Centimeters`, a tuple struct that can be compared
      #[derive(PartialEq, PartialOrd)]
      struct Centimeters(f64);
      
      // `Inches`, a tuple struct that can be printed
      #[derive(Debug)]
      struct Inches(i32);
      
      impl Inches {
          fn to_centimeters(&self) -> Centimeters {
              let &Inches(inches) = self;
              Centimeters(inches as f64 * 2.54)
          }
      }
      
      // `Seconds`, a tuple struct with no additional attributes
      struct Seconds(i32);
      
      fn main() {
          let _one_second = Seconds(1);
      
          // Error: `Seconds` can't be printed; it doesn't implement the `Debug` trait
          //println!("One second looks like: {:?}", _one_second);
          // TODO ^ Try uncommenting this line
      
          // Error: `Seconds` can't be compared; it doesn't implement the `PartialEq` trait
          //let _this_is_true = (_one_second == _one_second);
          // TODO ^ Try uncommenting this line
      
          let foot = Inches(12);
      
          println!("One foot equals {:?}", foot);
      
          let meter = Centimeters(100.0);
      
          let cmp =
              if foot.to_centimeters() < meter {
                  "smaller"
              } else {
                  "bigger"
              };
      
          println!("One foot is {} than one meter.", cmp);
      }

연산자 오버로딩

  • Rust는 Trait으로 연산자 오버로딩을 지원한다.
use std::ops;

struct Foo;
struct Bar;

#[derive(Debug)]
struct FooBar;

#[derive(Debug)]
struct BarFoo;

// The `std::ops::Add` trait is used to specify the functionality of `+`.
// Here, we make `Add<Bar>` - the trait for addition with a RHS of type `Bar`.
// The following block implements the operation: Foo + Bar = FooBar
impl ops::Add<Bar> for Foo {
    type Output = FooBar;

    fn add(self, _rhs: Bar) -> FooBar {
        println!("> Foo.add(Bar) was called");

        FooBar
    }
}

// By reversing the types, we end up implementing non-commutative addition.
// Here, we make `Add<Foo>` - the trait for addition with a RHS of type `Foo`.
// This block implements the operation: Bar + Foo = BarFoo
impl ops::Add<Foo> for Bar {
    type Output = BarFoo;

    fn add(self, _rhs: Foo) -> BarFoo {
        println!("> Bar.add(Foo) was called");

        BarFoo
    }
}

fn main() {
    println!("Foo + Bar = {:?}", Foo + Bar);
    println!("Bar + Foo = {:?}", Bar + Foo);
}

참고