티스토리 뷰

Rust-Language

trait: 공유 동작 정의

kmj24 2021. 9. 8. 23:55

trait은 다른 종류의 추상화를 사용할 수 있도록 해준다.

이는 타입들이 공통적으로 갖는 동작에 대하여 추상화 하도록 해준다.

trait이란 러스트 컴파일러에게 특정한 타입이 갖고 다른 타입들과 함께 공유할 수도 있는 기능에 대하여 정의한다.

trait은 다른언어들의 interface와 기능이 유사하지만 몇가지 다른점이 있다.

트레잇 정의하기

어떤 타입의 동작은 해당 타입 상에서 호출할 수 있는 메서드의 집합이다. 만약 서로 다른 타입에 대해 모두 동일한 메서드를 호출할 수 있다면 이 타입들은 동일한 동작을 공유한다.

트레잇의 정의는 어떠한 목적을 달성하기위해 필요한 동작의 집합을 정의하기위해 메서드 시그니처들을 함께 묶는 방법이다.

예를 들어 신문기사 생각했을 때, 다양한 종류와 양을 갖는 여러가지 struct가 있다고 가정하자.

NewsArticle구조체는 세계의 특정한 곳에서 줄지어 들어오는 뉴스의 내용을 담고 있고, Tweet은 최대 140글자의 콘텐츠와 함께 해당 트윗이 리트윗인지 또는 다른 트윗에 대한 답변인지와 같은 메타데이터를 가지고 있다.

NewsArticle 혹은 Tweet 인스턴스에 저장되어 있을 데이터에 대한 종합 정리를 보여줄 수 있는 미디어 종합기 라이브러리를 만든다고 했을 때, 각각의 구조체들이 가질 필요가 있는 동작은 정리해주기가 되어야 하며, 각 인스턴스 상에서 summary 메서드를 호출함으로써 해당 정리된 내용을 얻어낼 수 있어야 한다.

trait키워드를 이용하여 trait을 선언한다.

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

 

특정 타입에 대한 trait 구현

NewsArticle구조체 상 Summariable trait을 구현한다. Tweet 구조체에 대하여, 트윗 내용이 이미 140자로 제한되어 있음을 가정하고, summary를 정의하는데 있어 사용자 이름과 해당 트윗의 전체 텍스트를 가지고 오도록 한다.

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

impl Summarizable 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 뒤에 사용하고자 하는 trait이름을 넣고 그 다음 for키워드와 trait을 구현하고자 하는 구조체를 작성한다.

impl scope 내에서는 trait의 메서드를 작성하고, 메서드에는 함수처럼 로직을 작성한다. 여기서 self는 for 키워드로 정의한 구조체를 가리킨다.

pub fn run() {
    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 new tweet: {}", tweet.summary());
}

trait의 구현에는 제한사항이 있다.

trait과 타입이 구현되는 크레이트 내부의 것일 경우에만 해당 trait을 정의할 수 있다. 즉 외부 타입에 대한 외부 trait을 구현하는 것은 허용하지 않는다.

예를 들어 Vec에 대한 Display trait은 구현이 불가능한데 Display와 Vec모두 표준 라이브러리 내에 정의되어 있기 때문이다.

현재 크레이트 기능의 일부로서 Tweet과 같은 커스텀 타입에 대한 Display와 같은 표준 라이브러리 trait을 구현하는 것은 허용된다. 또한 현재 크레이트 내에 Vec에 대한 Summarizable을 구현하는 것도 가능하다.

impl Summarizable for Vec<String> {
    fn summary(&self) -> String {
        format!("{}, {}", self[0], self[1])
    }
}


pub fn run() {
    let v = vec![String::from("string1"), String::from("string2")];
    println!("{}", v.summary());
}

 

(Vec에 대한 trait 사용)

이러한 제한은 고아 규칙(orphan rule)의 일부이다.

고아 규칙은 간단하게 표현하면 부모 타입이 존재하지 않기 때문에 고아 규칙이라고 부르며, 만약 이 규칙이 없다면 두 크레이트는 동일한 타입에 대해 동일한 트레잇을 구현할 수 있게 되고, 이 두개의 구현체가 충돌을 일으키게 될것이다.

고아 규칙을 강제함으로써, 다른 사람이 자신의 코드를 망가뜨리는 것에 대한 방어가 되며 반대의 경우도 가능하다.

 

기본 구현

모든 타입 상에서의 모든 구현체가 커스텀 동작을 정의하도록 하는 대신, trait의 몇몇 혹은 모든 메서드들에 대한 기본 동작을 갖추는 것이 가능하다.

특정 타입에 대한 trait을 구현할 때, 각 메서드의 기본 동작을 유지하거나 override하도록 선택할 수 있다.

메서드 시그니처를 정의만 하던 위의 경우와 달리 기본 동작을 정의해줄 수 있다.

pub trait Summarizable {
    fn summary(&self) -> String {
        String::from("기본 문자열")
    }
}

impl Summarizable for NewsArticle {}

NewsArticle에 대한 summary메서드를 직접 정의하지 않아도 summary메서드가 기본 구현을 가지고 있으므로 작동한다.

단 NewsArticle 구조체에 내용을 집어 넣었더라고 summary에서 기본으로 설정한 내용만 출력한다.

 

동일한 트레잇 내 다른 메서드들을 호출하는 것이 허용되어 있는데, 심지어 그 다른 메서드 들이 기본 구현을 갖고 있지 않아도 된다.

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

    fn summary(&self) -> String {
        String::from(self.author_summary())
    }
}

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

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

pub fn run() {
    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 new tweet: {}", tweet.summary());

}

위의 코드는 "1 new tweet: @horse_ebooks"를 출력한다.

위와 같이 유연하게 코드를 작성할 수 있다.

overriding된 구현으로부터 기본 구현을 호출하는것은 불가능하다.

 

trait 바운드

제네릭 타입 파라미터를 이용하는 트레잇을 사용할 수 있다.

제네릭 타입에 제약을 가하여 이 제네릭 타입이 어떤 타입이든  되기 보다, 이 제네릭 타입이 특정한 트레잇을 구현하여 이 타입들이 가지고 있을 필요가 있는 동작을 갖고 있도록 타입들로 제한함을 compiler에 알릴 수 있다.

 위에서 NewsArticle과 Tweet 타입에 대한 Summarizable trait을 구현했다.

예를 들어 parameter item 상 summary메서드를 호출하는 함수 notify를 정의할 수 있다. 이 item은 제네릭 타입 T의 값이다. 오류 없이 item상에서 summary를 호출하기위해 T에 대한 trait바운드를 사용하여 item에 Summarizable trait을 반드시 구현한 타입이어야 함을 특정할 수 있다.

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

trait바운드는 제네릭 타입선언부와 함께 : 뒤에 작성한다.

이제 notify를 호출하여 NewsArticle이나 Tweet의 어떠한 인스턴스라도 넘길 수 있다.

 

 

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

    fn summary(&self) -> String {
        String::from(self.author_summary())
    }
}

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

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

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

pub fn run() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from("of course, as you probably already know, people"),
        reply: false,
        retweet: false,
    };

    notify(tweet);

}

 

 

 

 

 

위의 코드 실행 결과는 "Breaking news! @of course, as you probably already know, people" 이다.

 

+ 를 이용하면 하나의 제네릭 타입에 대한 여러개의 trait 바운드를 특정할 수 있다.

만약 함수 내에서 타입 T에 대해 summary 메서드 뿐만 아니라 형식화된 출력을 사용하길 원한다면,

T: Summarizable + Display 를 이용할 수 있다. 이는 T가 Summarizable와 Display 둘다 구현한 어떤 타입이어야 함을 의미한다.

여러개의 제네릭 타입 parameter를 가진 함수들에 대하여, 각 제네릭은 고유의 trait 바운드를 가진다. 만약 많은 수의 trait바운드 정보를 작성하는 것은 코드의 가독성을 떨어뜨릴 수 있다.

where절 뒤로 trait바운드를 옮겨서 특정하도록 해주는 대안 문법이 있다.

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

위의 코드 대신

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

이렇게 작성할 수 있다.

함수명, parameter, 반환 타입을 가까이에 위치시키도록 하여 코드의 가독성을 높일 수 있다.

 

어떤 제네릭 상에서 어떤 trait으로 정의된 동작을 이용하기를 원하는 어떤 경우이든, 해당 제네릭 타입 parameter의 타입 내 trait바운드를 명시할 필요가 있다.

fn largest<T>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

pub fn run() {
    let numbers = vec![34, 50, 25, 100, 65];

    let result = largest(&numbers);
    println!("The largest number is {}", result);

    let chars = vec!['y', 'm', 'a', 'q'];

    let result = largest(&chars);
    println!("The largest char is {}", result);
}

배열을 받아서 가장 큰 값을 반환하는 largest함수이다.

위의 코드는 오류가 발생한다.

ownership 규칙에 의하여, T가 될 수 있는 모든 가능한 타입에 대하여 동작하지 않을것이라는 의미이다.

use std::cmp::PartialOrd;

fn largest<T: PartialOrd>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

제네릭 타입에 PartialOrd를 작성하였지만 여전히 오류가 발생한다.

만약 스택에 저장될 수 있는 i32나 char와 같은 타입들은 Copy trait을 자동으로 구현하고 있다. 이러한 타입이라면 문제가 없을 것이다. 하지만 Copy trait을 구현하지 않은 타입(힙 영역에 저장되는 타입 등)일 경우 ownership을 옮길 수 없다는 의미이다.

만약 Copy가 구현된 타입들을 가지고 호출하는 것만 원한다면 T trait 바운드에 Copy를 추가할 수 있다.

+ 키워드를 이용하여 Copy를 추가한다.

use std::cmp::PartialOrd;

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list.iter() {
        if item > largest {
            largest = item;
        }
    }

    largest
}

만약 Copy 트레잇을 구현한 타입에 대한 것으로만 제한하길 원하지 않는다면 T가 Copy 대신 Clone을 사용하도록 코드를 작성할 수 있다. 이로써 largest 함수가 소유권을 갖길 원하는 경우 슬라이스의 각 값이 복제되도록 할 수 있다.

하지만 Clone은 많은 힙 할당이 발생하므로, 프로그램이 무거워질 수 있는데, 참조타입을 반환하도록 할 수 있다.

use std::cmp::PartialOrd;

fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut largest = &list[0];

    for item in list.iter() {
        if item > largest {
            largest = &item;
        }
    }

    &largest
}

참조 타입을 사용하면 Clone을 사용하지 않아도 코드가 작동할 수 있다.

 

trait과 trait 바운드는 중복을 제거하기 위해 제네릭 타입 parameter를 사용하는 코드를 작성할 수 있도록 해주지만, 여전히 컴파일러에게 해당 제네릭 타입이 어떤 동작을 할 필요가 있는지를 정확히 명시하도록 한다. 컴파일러에게 trait바운드를 제공하기 때문에 코드와 함께 이용되는 모든 구체적인 타입들이 정확한 동작을 제공하는지를 확인할 수 있다. 동적 타입 언어(ex, javascript)에서는, 어떤 타입에 대하여 어떤 메서드를 호출하는 시도를 헀는데 해당 타입이 그 메서드를 구현하지 않았다면, 런타임에 에러를 얻게 된다. rust는 이러한 런타임 에러를 컴파일 타임으로 옮겨 프로그램이 실행되기 전에 그 문제를 해결하도록 강제한다.이에 더하여 런타임에 해당 동작에 대한 검사를 하는 코드를 작성할 필요가 없게 되고 이는 제네릭의 유연성을 포기하지 않고도 높은 성능향상을 기대할 수 있다.

 

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

 

트레잇: 공유 동작을 정의하기 - The Rust Programming Language

트레잇은 다른 종류의 추상화를 사용할 수 있도록 해줍니다: 이는 타입들이 공통적으로 갖는 동작에 대하여 추상화하도록 해줍니다. 트레잇(trait) 이란 러스트 컴파일러에게 특정한 타입이 갖고

rinthel.github.io

 

'Rust-Language' 카테고리의 다른 글

trait 다른 타입 간 허용  (0) 2021.09.15
life time, 라이프 타임  (0) 2021.09.12
반복자, iterator  (0) 2021.09.03
Closure, 클로저 함수  (0) 2021.08.05
스마트 포인터  (0) 2021.05.16
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함