RUST

[RUST] 트레이트 (Traits)

ssh9308 2024. 1. 23. 09:00
반응형

트레이트 (Traits) 란?

 

Rust에서 트레이트(trait)는 특정 타입이 구현해야 하는 동작을 정의하는 방법이다.

 

즉, 트레이트는 하나 이상의 메서드를 가지고 있으며,

 

이 메소드들을 구현하는 타입은 해당 트레이트의 기능을 제공하게 된다.

 

트레이트는 자바의 인터페이스나 C#의 인터페이스에 비슷하며,

 

다른 언어에서는 프로토콜이라고도 불린다.

 

 

트레이트의 목적

 

추상화

트레이트를 사용하여 다양한 타입에 공통적인 동작을 추상화할 수 있다.

 

이를 통해 타입에 관계없이 일관된 방식으로 메서드를 호출할 수 있다.

 


확장성

기존의 타입에 새로운 트레이트를 구현함으로써 해당 타입의 기능을 확장할 수 있다.

 

이는 기존 코드를 변경하지 않고도 타입에 새로운 기능을 추가할 수 있게 해 준다.

 

 

제네릭 프로그래밍

트레이트를 사용하면 제네릭 타입에 대한 제약 조건을 설정할 수 있다.

 

이를 통해 제네릭 타입이 특정 메서드를 반드시 구현하도록 요구할 수 있다.

 

 

 

 

트레이트의 구현 및 사용 예시

 

// 트레이트 정의
trait Greet {

    fn greeting(&self) -> String;
}

// Pet 열거체 정의
enum Pet {
    Cow,
    Cat,
    Lion
}

// Pet 열거체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Pet {

    fn greeting(&self) -> String {
        match self {
            Pet::Cow => String::from("음메"),
            Pet::Cat => String::from("야옹"),
            Pet::Lion => String::from("어흥"),
        }
    }
}

// Person 구조체
struct Person {
    name: String,
    buseo: String
}


// Person 구조체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Person {
    
    fn greeting(&self) -> String {
        String::from("안녕하십니까?")
    }

}


fn meet(one: &impl Greet, another: &impl Greet) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}



fn main() {

    let lion = Pet::Lion;

    let person1 = Person {
        name: String::from("임꺽정"),
        buseo: String::from("개발부")
    };

    meet(&lion, &person1);
    
}

 

 

 

위의 코드를 보면 trait 가 Java의 Interface와 비슷하단 걸 느낄 수 있다.

 

특정 trait의 구현책임이 생기면, trait 내에 존재하는 함수인 greeting() 함수를

 

무조건 구현할 책임이 주어진다.

 

 

아래와 같이 traits bound 기능을 통해서 파라미터의 타입을 제한할 수 도 있다.

 

// 트레이트 정의
trait Greet {

    fn greeting(&self) -> String;
}

// Pet 열거체 정의
enum Pet {
    Cow,
    Cat,
    Lion
}

// Pet 열거체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Pet {

    fn greeting(&self) -> String {
        match self {
            Pet::Cow => String::from("음메"),
            Pet::Cat => String::from("야옹"),
            Pet::Lion => String::from("어흥"),
        }
    }
}

// Person 구조체
struct Person {
    name: String,
    buseo: String
}


// Person 구조체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Person {
    
    fn greeting(&self) -> String {
        String::from("안녕하십니까?")
    }

}


fn meet(one: &impl Greet, another: &impl Greet) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}

fn meets<T: Greet>(one: &T, another: &T) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}


fn main() {

    let lion = Pet::Lion;
    let dog = Pet::Cow;

    let person1 = Person {
        name: String::from("임꺽정"),
        buseo: String::from("개발부")
    };

    let person2 = Person {
        name: String::from("강감찬"),
        buseo: String::from("기획부")
    };

    meets(&lion, &person1); // 불가능: line 은 Pet enum 이고, person1 은 Person 구조체
    meets(&lion, &dog); // 가능: line 은 Pet enum 이고, dog 는 Pet enum
    meets(&person1, &person2); // 불가능: person1 은 Pet enum 이고, person1 은 Person 구조체
}

 

meets() 함수를 살펴보면 제네릭 T: Greet를 사용하고 있다.

 

이 경우에 파라미터는 같은 타입이어야 하며, Greet 를 구현한 데이터여야 한다.

 

그래서 meets() 함수에 파라미터를 보면, 

 

첫 번째 파라미터가 Pet enum이고 두 번째 파라미터가 Person 구조체일 때는 

 

문제가 발생한다.

 

두 개의 데이터 객체가 Greet를 구현하고는 있지만,

 

서로의 데이터 타입이 불일치하기 때문이다.

 

만약 서로 다른 객체를 허용하려면 위의 아래와 같이 meets() 함수를 수정해 주면 된다.

 

// 트레이트 정의
trait Greet {

    fn greeting(&self) -> String;
}

// Pet 열거체 정의
enum Pet {
    Cow,
    Cat,
    Lion
}

// Pet 열거체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Pet {

    fn greeting(&self) -> String {
        match self {
            Pet::Cow => String::from("음메"),
            Pet::Cat => String::from("야옹"),
            Pet::Lion => String::from("어흥"),
        }
    }
}

// Person 구조체
struct Person {
    name: String,
    buseo: String
}


// Person 구조체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Person {
    
    fn greeting(&self) -> String {
        String::from("안녕하십니까?")
    }

}


fn meet(one: &impl Greet, another: &impl Greet) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}

// ================= 수정부분 ================= // 
fn meets<T: Greet, U: Greet>(one: &T, another: &U) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}


fn main() {

    let lion = Pet::Lion;
    let dog = Pet::Cow;

    let person1 = Person {
        name: String::from("임꺽정"),
        buseo: String::from("개발부")
    };

    let person2 = Person {
        name: String::from("강감찬"),
        buseo: String::from("기획부")
    };

    meets(&lion, &person1); // 가능: line 은 Pet enum 이고, person1 은 Person 구조체
    meets(&lion, &dog); // 가능: line 은 Pet enum 이고, dog 는 Pet enum
    meets(&person1, &person2); // 불가능: person1 은 Pet enum 이고, person1 은 Person 구조체
}

 

 

 

여러 개의 트레이트를 사용하기

 

물론 Rust에서는 여러 개의 트레이트를 사용할 수 있다.

 

여러개의 Trait를 사용할때에는 "+" 표시로 여러 Trait 를 이어 붙일 수 있다.

 

use std::fmt::Debug;

// 트레이트 정의
trait Greet {

    fn greeting(&self) -> String;
}

// Pet 열거체 정의
enum Pet {
    Cow,
    Cat,
    Lion
}

// Pet 열거체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Pet {

    fn greeting(&self) -> String {
        match self {
            Pet::Cow => String::from("음메"),
            Pet::Cat => String::from("야옹"),
            Pet::Lion => String::from("어흥"),
        }
    }
}

// Person 구조체
struct Person {
    name: String,
    buseo: String
}


// Person 구조체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Person {
    
    fn greeting(&self) -> String {
        String::from("안녕하십니까?")
    }

}


fn meet(one: &impl Greet, another: &impl Greet) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}

fn meets<T: Greet + Debug>(one: &T, another: &T) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}


fn main() {

    let lion = Pet::Lion;
    let dog = Pet::Cow;

    let person1 = Person {
        name: String::from("임꺽정"),
        buseo: String::from("개발부")
    };

    let person2 = Person {
        name: String::from("강감찬"),
        buseo: String::from("기획부")
    };

    // 아래의 두개의 함수 사용은 잘못된 것이다. 왜냐면 각각의 파라미터에 Debug Trait 가 구현되어 있어야 하기 때문이다.
    meets(&lion, &dog); 
    meets(&person1, &person2); 
}

 

 

하지만 위의 코드에서 meets(&lion, &dog) , meets(&person1, &person2) 코드 모두 문제가 생긴다.

 

이유는 각 파라미터의 데이터가 Greet trait는 구현했지만,

 

Debug Trait는 구현하지 않았기 때문이다.

 

아래와 같이 Person 구조체와 

 

Pet 열거체에 Debug trait를 구현해 주면 된다.

 

use std::fmt::Debug;


// 트레이트 정의
trait Greet {

    fn greeting(&self) -> String;
}

// Pet 열거체 정의
// ############# Debug Trait 구현 #############
#[derive(Debug)]
enum Pet {
    Cow,
    Cat,
    Lion
}

// Pet 열거체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Pet {

    fn greeting(&self) -> String {
        match self {
            Pet::Cow => String::from("음메"),
            Pet::Cat => String::from("야옹"),
            Pet::Lion => String::from("어흥"),
        }
    }
}

// Person 구조체
// ############# Debug Trait 구현 #############
#[derive(Debug)]
struct Person {
    name: String,
    buseo: String
}


// Person 구조체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Person {
    
    fn greeting(&self) -> String {
        String::from("안녕하십니까?")
    }

}


fn meet(one: &impl Greet, another: &impl Greet) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}

fn meets<T: Greet + Debug>(one: &T, another: &T) {
    println!("첫번째가 인사합니다. => {}", one.greeting());
    println!("두번째가 인사합니다. => {}", another.greeting());
}


fn main() {

    let lion = Pet::Lion;
    let dog = Pet::Cow;

    let person1 = Person {
        name: String::from("임꺽정"),
        buseo: String::from("개발부")
    };

    let person2 = Person {
        name: String::from("강감찬"),
        buseo: String::from("기획부")
    };

    // 아래의 두개의 함수 사용은 잘못된 것이다. 왜냐면 각각의 파라미터에 Debug Trait 가 구현되어 있어야 하기 때문이다.
    meets(&lion, &dog); 
    meets(&person1, &person2); 
}

 

 

 

 

여러 개의 트레이트를 사용하기 - where 문법

 

위와 같이 여러 트레이트를 사용하는 경우 "+" 표현으로 계속 진행하게 되면,

 

가독성에서 문제가 발생할 수 있다.

 

이를 대비해서 RUST에서는 where 문법을 제공한다.

use std::fmt::{Debug, Display};

// 트레이트 정의
trait Greet {

    fn greeting(&self) -> String;
}

// Pet 열거체 정의
#[derive(Debug)]
enum Pet {
    Cow,
    Cat,
    Lion
}

// Pet 열거체에게 Greet 트레이트를 구현하도록 정의
impl Greet for Pet {

    fn greeting(&self) -> String {
        match self {
            Pet::Cow => String::from("음메"),
            Pet::Cat => String::from("야옹"),
            Pet::Lion => String::from("어흥"),
        }
    }
}

// Person 구조체
#[derive(Debug)]
struct Person {
    name: String,
    buseo: String
}



// Person 구조체가 Greet 트레이트를 구현하도록 정의.
impl Greet for Person {
    
    fn greeting(&self) -> String {
        String::from("안녕하십니까?")
    }
}

// Person 구조체가 Display 트레이트를 구현하도록 정의. 
impl Display for Person {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.name.as_str())
    }
}
'//

fn meets<T: Greet + Debug, U: Greet + Display>(one: &T, another: &U) {
    println!("첫번째가 인사합니다. => {:?}", one.greeting());
    println!("두번째가 인사합니다. => {:?}", another.greeting());
}

// 위의 함수 표현을 아래와 같이 where 을 써서 간단하게 표현할 수 있다.
fn meets_where<T, U>(one: &T, another: &U) 
where
    T: Greet + Debug,
    U: Greet + Display
{
    println!("첫번째가 인사합니다. => {:?}", one.greeting());
    println!("두번째가 인사합니다. => {:?}", another.greeting());
}


fn main() {

    let lion = Pet::Lion;

    let person1 = Person {
        name: String::from("임꺽정"),
        buseo: String::from("개발부")
    };

    meets(&lion, &person1); 
    meets_where(&lion, &person1); 
}

 

반응형