#13 트레이트

Pt J·2020년 8월 23일
0

[完] Rust Programming

목록 보기
16/41
post-thumbnail

이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.

일련의 구조체들이 공통적인 특징을 가지고 있어 공통의 기능을 구현해야 한다면
이에 해당하는 메서드 시그니처를 모아놓고 그것을 구현하도록 하면 좋을 것 같다.
이렇게 공유 가능한 행위를 정의하는 데 사용되는 것이 트레이트다.
완전 일치하지는 않지만 여타 언어의 인터페이스와 비슷한 면이 있다고 볼 수 있다.

트레이트 Trait

트레이트는 trait 키워드를 통해 정의한다.
그리고 트레이트 코드블록에는 해당 트레이트가 갖는 기능에 해당하는 메서드 시그니처가 존재한다.

예를 들어, 몇 가지 구조체들에 그것이 가진 정보를 요약하여 문자열로 반환하는 메서드가 존재한다면
우리는 요약 가능한 것들의 트레이트를 만들어 요약 메서드를 그것에 포함시킬 수 있다.
다음과 같이 말이다.

peter@hp-laptop:~/rust-practice/chapter10/mixed_generic$ cd ..
peter@hp-laptop:~/rust-practice/chapter10$ cargo new summary_trait --lib
     Created library `summary_trait` package
peter@hp-laptop:~/rust-practice/chapter10$ cd summary_trait/
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/lib.rs

src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

어떤 트레이트를 구현하는 구조체를 생성하기 위해서는
구조체를 선언하고 메서드를 선언할 때 impl 트레이트이름 for 구조체이름의 코드블록을 사용한다.
해당 구조체를 위한 해당 트레이트의 메서드는 그 안에서 구현하면 된다.

peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/lib.rs

src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

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

impl Summary for NewsArticle {
    fn summarize(&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 Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

impl 트레이트이름 for 구조체이름 코드블록으로 정의한 메서드는
impl 구조체이름 코드블록으로 정의한 메서드와 동일하게 사용할 수 있다.

peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/main.rs

src/main.rs

use summary_trait::Summary;
use summary_trait::Tweet;

fn main() {
    let tweet = Tweet {
        username: String::from("Peter J"),
        content: String::from("여러분 Rust하자 Rust!!"),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ cargo run
   Compiling summary_trait v0.1.0 (/home/peter/rust-practice/chapter10/summary_trait)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/summary_trait`
1 new tweet: Peter J: 여러분 Rust하자 Rust!!
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ 

main.rs에서 lib.rs에 있는 트레이트나 구조체를 사용하고자 한다면
use를 통해 현재 범위로 가져와야 한다는 것을 유의하자.

그리고 트레이트를 구현할 때 한 가지 제약 사항이 있는데
트레이트 또는 그것을 구현할 자료형 둘 중 하나는 현재 크레이트에 있어야 한다.
외부 트레이트를 로컬 자료형에 구현하는 것도 가능하고
로컬 트레이트를 외부 자료형에 구현하는 것도 가능하지만
외부 트레이트를 외부 자료형에 구현하는 것은 허용하지 않는다.
이는 외부 트레이트를 외부 자료형에 구현하는 것을 허용할 경우
서로 다른 사람이 구현해놓은 내용이 충돌을 일으켜 Rust 컴파일러에게 혼란을 줄 수 있기 때문이다.

default

때로는 트레이트의 메서드를 구현하지 않을 경우
기본 동작을 수행하도록 하는 것이 유용한 경우도 있다.
이런 상황에서 사용할 수 있는 것이 디폴트 구현 메서드다.
기본적으로 트레이트는 메서드의 시그니처만 가지고 있지만
그것이 본문을 가질 경우 디폴트 구현 메서드가 된다.
해당 메서드를 구현하지 않을 경우 이것이 호출된다.

Summary 트레이트에 summarize 메서드에 대한 디폴트 구현 메서드를 생성하고
NewsArticlesummarize 부분을 주석 처리하여 이를 확인해보자.
이 때 주의할 점은, impl summarize for NewsArticle 부분까지 주석 처리할 경우
NewsArticleSummary를 구현한 녀석임을 알 수 없으므로
summarize 부분만 주석 처리 하도록 하자.

peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/lib.rs

src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

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

impl Summary for NewsArticle {
    /*
    fn summarize(&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 Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/main.rs

src/main.rs

use summary_trait::{Summary, Tweet, NewsArticle};

fn main() {
    // snip

    let article = NewsArticle {
        headline: String::from("Peter broken Hazzi's Windows"),
        location: String::from("Gwangjin-goo, Seoul, Korea"),
        author: String::from("Ferris"),
        content: String::from("Finally, Peter broken Hazzi's Windows, \
            and install Linux on Hazzi's laptop."),
    };

    println!("New article available! {}", article.summarize());
}
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ cargo run
   Compiling summary_trait v0.1.0 (/home/peter/rust-practice/chapter10/summary_trait)
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/summary_trait`
1 new tweet: Peter J: 여러분 Rust하자 Rust!!
New article available! (Read more...)
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ 

Tweet의 경우 summarize를 구현하여 그것이 호출되었으며
NewsArticle의 경우 구현하지 않아 디폴트 구현 메서드가 호출된 것을 알 수 있다.

디폴트 구현 메서드는 그 본문에서 트레이트의 다른 메서드를 호출할 수 있는데
심지어 상대가 디폴트 구현 메서드가 아니어도 호출할 수 있다.
이 트레이트를 구현하는 순간 해당 메서드는 구현될 것이 확실하니 가능한 일이다.

그런 경우의 예제를 한 번 작성해보자.

peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/lib.rs

src/lib.rs

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

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

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        self.author.clone()
    }
  
    /*
    fn summarize(&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 Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }

    /*
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
    */
}
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ cargo run
   Compiling summary_trait v0.1.0 (/home/peter/rust-practice/chapter10/summary_trait)
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s
     Running `target/debug/summary_trait`
1 new tweet: (Read more from @Peter J...)
New article available! (Read more from Ferris...)
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$

매개변수

트레이트는 매개변수로도 사용할 수 있다.
트레이트가 매개변수로 사용될 경우 그 트레이트를 구현한 어떤 값이든 인자로 전달할 수 있다.
인자의 자료형 대신 impl 트레이트이름을 사용함으로서 가능하다.

앞서 작성한 예제를 통해 Summary 변수를 인자로 받는 함수를 작성해보자.

peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/lib.rs

src/lib.rs

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

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

impl Summary for NewsArticle {
    fn summarize_author(&self) -> String {
        self.author.clone()
    }
  
    fn summarize(&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 Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }

    /*
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
    */
}

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ vi src/main.rs

src/main.rs

use summary_trait::{Tweet, NewsArticle};

fn main() {
    let _tweet = Tweet {
        username: String::from("Peter J"),
        content: String::from("여러분 Rust하자 Rust!!"),
        reply: false,
        retweet: false,
    };

    //println!("1 new tweet: {}", tweet.summarize());

    let article = NewsArticle {
        headline: String::from("Peter broken Hazzi's Windows"),
        location: String::from("Gwangjin-goo, Seoul, Korea"),
        author: String::from("Ferris"),
        content: String::from("Finally, Peter broken Hazzi's Windows, \
            and install Linux on Hazzi's laptop."),
    };

    //println!("New article available! {}", article.summarize());

    summary_trait::notify(&article);
}
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ cargo run
   Compiling summary_trait v0.1.0 (/home/peter/rust-practice/chapter10/summary_trait)
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/summary_trait`
Breaking news! Peter broken Hazzi's Windows, by Ferris, (Gwangjin-goo, Seoul, Korea)
peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ 

트레이트를 매개변수로 받을 때 impl 트레이트이름을 사용할 경우
그러한 매개변수가 둘 이상일 경우 그들끼리 자료형이 같음은 보장받지 못한다.
그들끼리 자료형이 일치할 필요는 없는 경우라면 상관 없지만
일치해야 하는 경우라면 트레이트 경계 문법을 사용해야 한다.
이것은 제네릭과 함께 사용된다.

트레이트 경계 문법

우리는 앞서 트레이트를 매개변수로 받는 함수를 다음과 같이 작성했다.

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

트레이트 경계 문법을 사용하면 같은 함수를 다음과 같이 작성할 수 있다.

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

같은 제네릭을 사용한 변수는 같은 자료형이어야 한다는 것을 이용하여
필요에 따라 트레이트 변수끼리의 자료형 일치를 설정할 수 있다.

여러 개의 트레이트를 구현한 자료형

때로는 둘 이상의 트레이트를 구현한 자료형만 인자로 받고 싶을 때가 있다.
그 경우 + 문법을 사용하면 되는데 위 두 가지 방식 모두에서 사용 가능하다.

pub fn notify(item: &impl Summary + Display) {
    println!("Breaking news! {}", item.summarize());
}
pub fn notify<T: Summary + Display>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

그런데 이런 식으로 작성하다보면 함수 시그니처가 너무 길어질 수 있다.
따라서 Rust는 트레이트 경계 문법에 수정을 가하여 이 문제를 해결하는 방법을 제시했다.

where

다음과 같은 함수가 존재한다고 하자.

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

우리는 이것을 where 절을 이용하여 다음과 같이 수정할 수 있다.

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

반환값

트레이트를 구현한 자료형을 반환하고 싶을 경우에도 impl 트레이트이름을 사용할 수 있다.
그런데 여기서 주의해야 할 점은,
조건문을 이용하여 서로 다른 값을 반환할 수 있는 상황에서
모든 반환값의 자료형은 동일해야 한다는 것이다.
즉, fn some_function() -> impl Summary와 같은 함수의 반환값이
어떤 경우에는 NewsArticle이고 어떤 경우에는 Tweet일 수는 없다는 것이다.

largest

우리는 지난 시간에 다음과 같이 이야기한 바가 있다.

우리는 아직 이것을 배우지 않았으므로 일단 이대로 두고
트레이트에 대한 내용을 다룰 때 작동하는 코드로 수정해보도록 하자.

이제 트레이트를 배웠으므로 그것을 수정할 수 있다.
그것의 원본은 다음과 같았다.

peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ view ../largest_generic/src/main.rs

../largest_generic/src/main.rs

fn main() {
    let number_list = vec![34, 50, 35, 100, 65];
    println!("The largest number is {}", largest(&number_list));

    let character_list = vec!['y', 'm', 'a', 'q'];
    println!("The largest character is {}", largest(&character_list));
}

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

    for item in list {
        if *item > largest {
            largest = *item;
        }
    }
    largest
}

그리고 비교 연산자는 std::cmp::PartialOrd 트레이트에 정의되어 있어
그것을 구현한 자료형에서만 사용할 수 있다.
따라서 우리는 largest의 매개변수가 이것을 구현했음을 보장받아야 한다.
std::cmp::PartialOrd 트레이트는 Prelude에 포함되어 있어 현재 범위로 가져올 필요 없다.

peter@hp-laptop:~/rust-practice/chapter10/summary_trait$ cd ..
peter@hp-laptop:~/rust-practice/chapter10$ cargo new modified_largest_generic
     Created binary (application) `modified_largest_generic` package
peter@hp-laptop:~/rust-practice/chapter10$ cd modified_largest_generic/
peter@hp-laptop:~/rust-practice/chapter10/modified_largest_generic$ cp ../largest_generic/src/main.rs src/main.rs 
peter@hp-laptop:~/rust-practice/chapter10/modified_largest_generic$ vi src/main.rs

src/main.rs

fn main() {
    let number_list = vec![34, 50, 35, 100, 65];
    println!("The largest number is {}", largest(&number_list));

    let character_list = vec!['y', 'm', 'a', 'q'];
    println!("The largest character is {}", largest(&character_list));
}

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

    for item in list {
        if *item > largest {
            largest = *item;
        }
    }
    largest
}

그런데 이렇게 수정하고 나면 또 다른 오류로 인해 컴파일이 되지 않는다.

peter@hp-laptop:~/rust-practice/chapter10/modified_largest_generic$ cargo run
   Compiling modified_largest_generic v0.1.0 (/home/peter/rust-practice/chapter10/modified_largest_generic)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
  --> src/main.rs:12:23
   |
12 |     let mut largest = list[0];
   |                       ^^^^^^^
   |                       |
   |                       cannot move out of here
   |                       move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
   |                       help: consider borrowing here: `&list[0]`

error[E0507]: cannot move out of `*item` which is behind a shared reference
  --> src/main.rs:16:23
   |
16 |             largest = *item;
   |                       ^^^^^ move occurs because `*item` has type `T`, which does not implement the `Copy` trait

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `modified_largest_generic`.

To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter10/modified_largest_generic$

Copy 트레이트를 구현하지 않는 변수가 인자로 전달될 경우
largestlist[0]를 가져올 수 없어 문제가 생길 수 있다는 것이다.
따라서 우리는 TPartialOrd 트레이트뿐만 아니라
Copy 트레이트도 구현하고 있어야 한다는 것을 명시해야 한다.

peter@hp-laptop:~/rust-practice/chapter10/modified_largest_generic$ vi src/main.rs

src/main.rs

fn main() {
    let number_list = vec![34, 50, 35, 100, 65];
    println!("The largest number is {}", largest(&number_list));

    let character_list = vec!['y', 'm', 'a', 'q'];
    println!("The largest character is {}", largest(&character_list));
}

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

    for item in list {
        if *item > largest {
            largest = *item;
        }
    }
    largest
}
peter@hp-laptop:~/rust-practice/chapter10/modified_largest_generic$ cargo run
   Compiling modified_largest_generic v0.1.0 (/home/peter/rust-practice/chapter10/modified_largest_generic)
    Finished dev [unoptimized + debuginfo] target(s) in 0.20s
     Running `target/debug/modified_largest_generic`
The largest number is 100
The largest character is y
peter@hp-laptop:~/rust-practice/chapter10/modified_largest_generic$ 

트레이트 경계의 활용

제네릭을 포함한 자료형을 구현할 때 우리는 impl에 트레이트 경계를 적용함으로써
특정 트레이트를 구현하고 있는 경우에만 호출 가능한 메서드를 정의할 수 있다.

유사 유닛 구조체 Unit-like Struct

만약 어떤 트레이트가 가진 메서드를 사용하고자 하는데
그것과 연관된 그 어떤 자료도 가지고 있지 않는 경우라면
우리는 필드를 갖지 않는 구조체를 만들어 트레이트를 구현할 수 있다.
이러한 구조체를 유사 유닛 구조체라고 한다.
어떤 자료를 저장하기 위한 자료형이 아니라 단지 트레이트 메서드를 호출하기 위한 자료형인 것이다.
이것은 자료를 가지고 있지 않다는 점에서 ()와 유사하다고 하여
유사 유닛 구조체라는 이름을 갖고 있다.

이 포스트의 내용은 공식문서의 10장 2절 Traits: Defining Shared Behavior에 해당합니다.

profile
Peter J Online Space - since July 2020

0개의 댓글