트레이트(trait)

트레이트로 메소드 공유하기

파이썬은 클래스를 상속해 공통된 메소드를 사용할 수 있지만, 러스트는 구조체의 상속이 되지 않습니다.

먼저 파이썬에서 다음과 같이 Person 을 상속하는 새로운 클래스 Student 를 선언합니다.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.alive = True

    def say_hello(self):
        print("Hello, Rustacean!")

    def get_older(self, year):
        self.age += year


class Student(Person):
    def __init__(self, name, age, major):
        super().__init__(name, age)
        self.major = major

    def say_hello(self):
        print(f"Hello, I am {self.name} and I am studying {self.major}")

Student 클래스는 새로운 프로퍼티 major 를 가지고 있고, Personsay_hello 메소드를 오버라이드하고 있습니다.

Rust는 하나의 struct를 상속하는 방법이 존재하지 않습니다. 즉 필드와 메소드를 다른 struct에 전달할 수 없습니다. 하지만 서로 다른 struct 타입들이 메소드를 공유할 수 있는 하나의 속성을 정의할 수 있는데, 바로 trait입니다. 러스트에서는 구조체에서 공유하는 메소드를 구현하기 위해 트레이트를 먼저 선언해야 합니다. 트레이트에서는 공유할 메소드의 원형을 선언합니다.

trait Greet {
    fn say_hello(&self) {}
}

이렇게 선언하면, say_hello는 아무것도 실행하지 않는 빈 함수이기 때문에, 실제 내용을 각 구조체의 메소드에서 구현해야 합니다. 혹은 say_hello의 기본 구현형을 트레이트를 선언할 때 정의할 수도 있습니다.

이제 파이썬과 동일하게 러스트 코드를 작성해 보겠습니다.

struct Person {
    name: String,
    age: i32,
    alive: bool,
}

impl Person {
    fn new(name: &str, age: i32) -> Person {
        Person {
            name: String::from(name),
            age: age,
            alive: true
        }
    }

    fn get_older(&mut self, year: i32) {
        self.age += year;
    }
}

impl Greet for Person {}


struct Student {
    name: String,
    age: i32,
    alive: bool,
    major: String,
}

impl Student {
    fn new(name: &str, age: i32, major: &str) -> Student {
        Student {
            name: String::from(name),
            age: age,
            alive: true,
            major: String::from(major),
        }
    }
}

impl Greet for Student {
    fn say_hello(&self) {
        println!("Hello, I am {} and I am studying {}", self.name, self.major)
    }
}

이제 메인 함수에서 PersonStudent 구조체의 인스턴스를 만들고 say_hello 메소드를 각각 호출해 보겠습니다.

fn main() {
    let mut person = Person::new("John", 20);
    person.say_hello(); // 🫢
    person.get_older(1);
    println!("{} is now {} years old", person.name, person.age);

    let student = Student::new("Jane", 20, "Computer Science");
    student.say_hello();
}

실행 결과

John is now 21 years old
Hello, I am Jane and I am studying Computer Science

person.say_hello()trait Greet의 메소드를 그대로 사용하기 때문에 아무것도 출력되지 않는 것을 알 수 있습니다.

다시 트레이트 선언으로 돌아가보면 say_hello 함수는 파라미터로 &self 를 받고 있지만, 트레이트에 정의되는 함수는 인스턴스 프로퍼티에 접근할 수 없습니다. 만일 여기서 다음과 같이 함수의 원형을 수정하고 컴파일해보면 에러가 발생합니다.

trait Greet {
    fn say_hello(&self) {
        println!("Hello, Rustacean!");
    }
}

파생(Derive)

컴파일러는 #[derive] 트레이트을 통해 일부 특성에 대한 기본 구현을 제공할 수 있습니다. 보다 복잡한 동작이 필요한 경우 이러한 특성은 직접 구현할 수 있습니다.

다음은 파생 가능한 트레이트 목록입니다:

  • 비교: Eq, PartialEq, Ord, PartialOrd.
  • Clone, 복사본을 통해 &T에서 T를 생성합니다.
  • Copy, '이동 시맨틱' 대신 '복사 시맨틱' 타입을 제공합니다.
  • Hash, &T에서 해시를 계산합니다.
  • Default, 데이터 타입의 빈 인스턴스를 생성합니다.
  • {:?} 포매터를 사용하여 값의 형식을 지정하려면 Debug.
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1); // 🤯
}
error[E0277]: `Rectangle` doesn't implement `Debug`
  --> src/main.rs:12:31
   |
12 |     println!("rect1 is {:?}", rect1); // 🤯
   |                               ^^^^^ `Rectangle` cannot be formatted using `{:?}`
   |
   = help: the trait `Debug` is not implemented for `Rectangle`
   = note: add `#[derive(Debug)]` to `Rectangle` or manually `impl Debug for Rectangle`
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!("rect1 is {:?}", rect1);
}