[Rust] Trait

silver·2023년 4월 15일
0

트레잇: 공유 동작을 정의하기

트레잇은 다른 종류의 추상화를 사용할 수 있도록 해준다: 이는 타입들이 공통적으로 갖는 동작에 대하여 추상화하도록 해준다.
트레잇(trait)이란 러스트 컴파일러에게 특정한 타입이 갖고 다른 타입들과 함께 공유할 수도 있는 기능에 대해 말해준다. 우리가 제네릭 타입 파라미터를 사용하는 상황에서는, 커파일 타임에 해당 제네릭 타입이 어떤 트레잇을 구현한 타입이어야 함을 명시하여, 그러한 상황에서 우리가 사용하길 원하는 동작을 갖도록 하기 위해 트레잇 바운드(trait bounds)를 사용할 수 있다.

트레잇은 다른 언어들에서 '인터페이스(interface)'라고 부르는 기능과 유사하지만, 몇 가지 다른 점이 있다.

트레잇 정의하기

어떤 타입의 동작은 우리가 해당 타입 상에서 호출할 수 있는 메소드들로 구성되어 있다. 만일 우리가 서로 다른 타입에 대해 모두 동일한 메소드를 호출할 수 있다면 이 타입들은 동일한 동작을 공유하는 것이다. 트레잇의 정의는 어떠한 목적을 달성하기 위해 필요한 동작의 집합을 정의하기 위해 메소드 시그니처들을 함께 묶는 방법이다.

pub trait Summarizable {
	fn summary(&self) -> String;
}

trait 키워드 다음 트레잇의 이름, 위의 경우 Summarizable 을 써서 트레잇을 선언했다. 메소드 시그니처 뒤에, 중괄호 내의 정의부를 제공하는 대신, 세미콜론을 집어넣었다. 그러면 이 트레잇을 구현하는 각 타입은 이 메소드의 본체에 대한 해당 타입 고유의 커스텀 동작을 제공해야 하는데, 컴파일러는 Summarizable 트레잇을 갖는 어떠한 타입이든 그에 대한 메소드 summary 를 정확히 동일한 시그니처로 정의되도록 강제할 것이다.

트레잇은 한 줄 당 하나의 메소드 시그니처와 각 줄의 끝에 세미콜론을 갖도록 함으로써, 본체 내에 여러개의 메소드를 가질 수 있다.

특정 타입에 대한 트레잇 구현하기

pub struct NewsArticle {
	pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summariazble for NewsArticle {
	fn summary(&self) -> String {
    	format!("{}, by{} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
	pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summarizable for Tweet {
	fn summary(&self) -> String {
    	format!("{}: {}", self.username, self.content)
    }
}

어떤 타입 상에서의 트레잇 구현은 트레잇과 관련이 없는 메소드를 구현하는 것과 유사하다. 다른 점은 impl 뒤에 우리가 구현하고자 하는 트레잇 이름을 넣고, 그다음 for 와 우리가 트레잇을 구현하고자 하는 타입의 이름을 쓴다는 것이다.

트레잇을 한 번 구현했다면, 트레잇의 일부가 아닌 메소드들을 호출했던 것과 동일한 방식으로 NewsArticleTweet 의 인스턴스 상에서 해당 메소드를 호출할 수 있다.

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 enw tweet: {}", tweet.summary());

트레잇 구현과 함께 기억할 한 가지 제한 사항이 있다: 트레잇 혹은 타입이 우리의 크레이트 내의 것일 경우에만 해당 타입에서의 트레잇을 정의할 수 있다. 다릏게 말하면, 외부의 타입에 대한 외부 트레잇을 구현하는 것은 혀용되지 않는다.

예를 들어, Vec에 대한 Display 트레잇을 구현이 불가능한데, Display와 Vec 모두 표준 라이브러리 내에 정의되어 있기 때문이다. 이러한 제한은 orphan rule 이라고 불리는 것의 일부이다.

기본 구현

종종 모든 타입 상에서의 모든 구현체가 커스텀 동작을 정의하도록 하는 대신, 트레잇의 몇몇 혹은 모든 메소드들에 대한 기본 동작을 정의하는 것이 유용할 수 있다. 특정한 타입에 대한 트레잇을 구현할 때, 각 메소드의 기본 동작을 유지하거나 오버라이드(override)하도록 선택할 수 있다.

pub trait Summarizable {
	fun smmuary(&self) -> String {
    	String::from("(Read more...)")
    }
}

커스텀 구현을 정의하는 대신 NewsArticle 의 인스턴스를 정리하기 위해 이 기본 구현을 사용하고자 한다면, 빈 impl 블록을 명시하면 된다.

impl Summarization for NewsArticle {}

기본 구현을 오버라이딩 하기 위한 문법은 기본 구현이 없는 트레잇 메소드를 구현하기 위한 문법과 정확히 동일하다.

기본 구현은 동일한 트레잇 내의 다른 메소드들을 호출하는 것이 허용되어 있는데, 심지어 그 다른 메소드들이 기본 구현을 갖고 있지 않아도 된다. 이러한 방식으로, 트레잇은 수많은 유용한 기능을 제공하면서도 다른 구현자들이 해당 트레잇의 작은 일부분만 구현하도록 요구할 수 있다.

pub trait Summarizable {
	fn author_summary(&self) -> String;
    
    fn summary(&self) -> String {
    	format!("(Read more from {}...)", self.author_summary())
    }
}

위의 Summarizable 을 사용하기 위해서는, 어떤 타입에 대한 이 트레잇을 구현할 때 author_summary 만 정의하면 된다.

impl Summarizable for Tweet {
	fn autor_summary(&self) -> String {
    	format!("@{}", self.username)
    }
}

트레잇 바운드

우리는 제네릭 타입에 제약을 가하여 이 제네릭 타입이 어떠한 타입이든 되기 보다는, 이 제네릭 타입이 특정한 트레잇을 구현하여 이 타입들이 가지고 있을 필요가 있는 동작을 갖고 있도록 타입들로 제한함을 컴파일러가 확신하도록 할 수 있다.

pub fn notify<T: Summarizable>(item: T) {
	println!("Breaking news! {}", item.summary());
}

+ 를 이용하면 하나의 제네릭 타입에 대해 여러 개의 트레잇 바운드를 특정할 수 있다. 만일 함수 내에서 타입 T 에 대해 summary 메소드 뿐만 아니라 형식화된 출력을 사용하길 원한다면, 트레잇 바운드 T: Summarizable + Display 를 이용할 수 있다. 이는 TSummarizableDisplay 둘 다 구현한 어떤 타입이어야 함을 의미한다.

함수 이름과 파라미터 리스트 사이의 꺾쇠 괄호 내에 많은 수의 트레잇 바운드 정보를 특정하는 것은 코드를 읽기 힘들게 만들 수 있으므로, 함수 시그니처 뒤에 where 절 뒤로 트레잇 바운드를 옮겨서 특정하도록 해주는 대안 문법이 있다.

fn some_function<T: Display + Clone, U: Clone + Debug>(t: T, u: U) -> i32 {

where 절을 이용하여 아래와 같이 수정할 수 있다.

fn some_function<T, U>(t: T, u: U) -> i32 
	where T: Display + Clone,
    	  U: Clone + Debug
{

참고: https://rinthel.github.io/rust-lang-book-ko/ch10-02-traits.html

0개의 댓글