이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
일련의 구조체들이 공통적인 특징을 가지고 있어 공통의 기능을 구현해야 한다면
이에 해당하는 메서드 시그니처를 모아놓고 그것을 구현하도록 하면 좋을 것 같다.
이렇게 공유 가능한 행위를 정의하는 데 사용되는 것이 트레이트다.
완전 일치하지는 않지만 여타 언어의 인터페이스와 비슷한 면이 있다고 볼 수 있다.
트레이트는 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 컴파일러에게 혼란을 줄 수 있기 때문이다.
때로는 트레이트의 메서드를 구현하지 않을 경우
기본 동작을 수행하도록 하는 것이 유용한 경우도 있다.
이런 상황에서 사용할 수 있는 것이 디폴트 구현 메서드다.
기본적으로 트레이트는 메서드의 시그니처만 가지고 있지만
그것이 본문을 가질 경우 디폴트 구현 메서드가 된다.
해당 메서드를 구현하지 않을 경우 이것이 호출된다.
Summary
트레이트에 summarize
메서드에 대한 디폴트 구현 메서드를 생성하고
NewsArticle
의 summarize
부분을 주석 처리하여 이를 확인해보자.
이 때 주의할 점은, impl summarize for NewsArticle
부분까지 주석 처리할 경우
NewsArticle
이 Summary
를 구현한 녀석임을 알 수 없으므로
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
트레이트를 구현하지 않는 변수가 인자로 전달될 경우
largest
에 list[0]
를 가져올 수 없어 문제가 생길 수 있다는 것이다.
따라서 우리는 T
에 PartialOrd
트레이트뿐만 아니라
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
에 트레이트 경계를 적용함으로써
특정 트레이트를 구현하고 있는 경우에만 호출 가능한 메서드를 정의할 수 있다.
만약 어떤 트레이트가 가진 메서드를 사용하고자 하는데
그것과 연관된 그 어떤 자료도 가지고 있지 않는 경우라면
우리는 필드를 갖지 않는 구조체를 만들어 트레이트를 구현할 수 있다.
이러한 구조체를 유사 유닛 구조체라고 한다.
어떤 자료를 저장하기 위한 자료형이 아니라 단지 트레이트 메서드를 호출하기 위한 자료형인 것이다.
이것은 자료를 가지고 있지 않다는 점에서 ()
와 유사하다고 하여
유사 유닛 구조체라는 이름을 갖고 있다.
이 포스트의 내용은 공식문서의 10장 2절 Traits: Defining Shared Behavior에 해당합니다.