이전 chapter에서 Rust의 type과 ownership system에 대해 배웠다.
이제 좀 더 깊게 들어가서 traits에 대해 배울 것이다.
traits에 대해 배우면, 모든 곳에 그것을 적용시킬 수 있을 것이다.
사실 이전 Chapter에서 tratis에 대해 이미 봤다.
.into() 호출 뿐만 아니라 ==나 + operator가 그렇다.
Rust의 standard library에 정의된 핵심 traits에 대해서도 다룰 것이다.
- Operator traits(Add, Sub, PartialEq, etc.)
- From, Into(infallible conversions)
- Clone, Copy(copying values)
- Deref, deref coercion
- Sized(size를 표시하는 것)
- Drop(사용자 정의 cleanup logic)
변환에 대한 이야기를 했기 때문에,
우리는 이전 Chapter와의 지식 차이를 잡을 기회가 왔다.
"A title"이 정확히 무엇일까?
slices에 대해서도 배워보자!
00_intro exercise는 역시나 배울 준비가 됐는지 확인하는 코드다.

Ticket type에 대해 다시 한번 보자.
pub struct Ticket {
title: String,
description: String,
status: String,
}
지금까지 우리의 모든 tests에서 Ticket's fields를 이용해 assertions를 만들었다.
assert_eq!(ticket.title(), "A new title");
만약 두 Ticket의 instances를 직접 비교하려할 때는 어떨까?
let ticket1 = Ticket::new(/* ... */);
let ticket2 = Ticket::new(/* ... */);
ticket1 == ticket2
compiler는 다음과 같은 error를 띄운다.
error[E0369]: binary operation `==` cannot be applied to type `Ticket`
--> src/main.rs:18:13
|
18 | ticket1 == ticket2
| ------- ^^ ------- Ticket
| |
| Ticket
|
note: an implementation of `PartialEq` might be missing for `Ticket`
Ticket은 새로운 type이다.
기본적으로 ==에 연결된 동작이 없다.
Rust는 마법처럼 두 Ticket instances를 어떻게 비교하는지 추측할 수 없다.
왜냐하면, 두 Ticket instances는 string S를 포함하기 때문이다.
그래서 Rust compiler는 우리에게 넌지시 알려준다.
PartialEq의 구현을 잊은 것 같다고...
이러한 PartialEq이 trait이다!
traits는 무엇일까?
Traits는 interfaces를 정의하는 방법이다.
trait은 trait의 contract를 만족시키기 위해, type이 구현해야하는 set of methods를 정의한다.
trait definition의 문법은 다음과 같다.
trait <TraitName> {
fn <method_name>(<parameters>) -> <return_type>;
}
예를 들어, 다음과 같이 구현하는 사람이 is_zero method를 정의하도록 요구하는
MaybeZero라는 이름의 trait을 정의할 수 있다.
trait MaybeZero {
fn is_zero(self) -> bool;
}
type의 trait을 구현하기 위해,
regular methods와 같이 우리는 impl keyword를 사용한다.
하지만, 문법은 살짝 다르다.
impl <TraitName> for <TypeName> {
fn <method_name>(<parameters>) -> <return_type> {
// Method body
}
}
예를 들어, 사용자 정의 number type인 WrappingU32을 위한
MaybeZero trait을 구현하려한다면 다음과 같다.
pub struct WrappingU32 {
inner: u32,
}
impl MaybeZero for WrappingU32 {
fn is_zero(self) -> bool {
self.inner == 0
}
}
trait method를 부르기 위해, regular methods 때와 같이 .(dot) operator를 사용한다.
let x = WrappingU32 { inner: 5 };
assert!(!x.is_zero());
trait method를 부르려면, 다음과 같은 것들이 true여야 한다.
- type이 trait을 구현해야한다.
- trait이 scope내에 있어야한다.
두번째 조건을 만족하기 위해, 당신은 use statement를 추가해줘야한다.
use crate::MaybeZero;
하지만 이건 다음과 같을 때는 꼭 필요하지 않다.
- trait이 호출이 일어나는 같은 module에 정의되어 있을 때
- trait이 standard library의 prelude에 정의되어 있을 때
*prelude는 모든 Rust program에 자동으로 import되는 traits와 types의 집합이다.
*use std::prelude::*;을 사용하는 것과 같고 이는 모든 Rust module의 시작부분에 추가되어 있다.
*prelude에 대한 traits와 types의 list는 Rust documentation에서 확인 가능하다.
01_traits의 exercise는 같은 method에 대해 type에 따른
다른 동작을 구현하도록 했다.

type이 다른 crate에 정의되어 있을 때,
그것을 위한 새로운 methods를 직접 정의할 수 없다.
아래 코드를 실행시켜보면
impl u32 {
fn is_even(&self) -> bool {
self % 2 == 0
}
}
다음과 같은 compile error가 뜬다.
error[E0390]: cannot define inherent `impl` for primitive types
|
1 | impl u32 {
| ^^^^^^^^
|
= help: consider using an extension trait instead
extension trait은 u32와 같은 외래 types에 새로운 methods를 붙이기 위한 것이 주요 목적이다.
이전 exercise에서 IsEven trait을 정의하여 i32 type과 u32 type을 위한 method를 구현했던 것과 같은 원리다.
당신은 IsEven이 scope안에 있는한 해당 types에 대해 is_even을 호출하는 것은 언제든 가능하다.
// Bring the trait in scope
use my_library::IsEven;
fn main() {
// Invoke its method on a type that implements it
if 4.is_even() {
// [...]
}
}
하지만 당신이 쓸 수 있는 trait implementations에는 한계가 있다.
가장 간단하고 단순한 것은 한 crate 내에 당신은 같은 type에 대해 같은 trait을 두번 구현할 수 없다는 것이다.
예를 들어,
trait IsEven {
fn is_even(&self) -> bool;
}
impl IsEven for u32 {
fn is_even(&self) -> bool {
true
}
}
impl IsEven for u32 {
fn is_even(&self) -> bool {
false
}
}
위 코드를 실행시키면, 다음과 같은 error가 뜬다.
error[E0119]: conflicting implementations of trait `IsEven` for type `u32`
|
5 | impl IsEven for u32 {
| ------------------- first implementation here
...
11 | impl IsEven for u32 {
| ^^^^^^^^^^^^^^^^^^^ conflicting implementation for `u32`
u32 값에 대해 불려지는 IsEven::is_even을 사용할 때, 모호함이 없어야한다.
그래서 하나만 있어야한다.
여러 crates가 포함되면, 상황이 더욱 미묘해진다.
특히, 다음 중 하나 이상이 충족돼야한다.
- trait이 현재 crate에 정의됐다.
- implementor type이 현재 crate에 정의됐다.
이것을 Rust에선 orphan rule이라 부른다.
이것의 목적은 method resolution process를 모호하지 않게 만들기 위함이다.
다음과 같은 상황을 상상해보자.
- Crate A가 IsEven trait을 정의한다.
- Crate B가 u32 type을 위한 IsEven을 구현한다.
- Crate C가 u32 type을 위한 IsEven을 B와 다르게 구현한다.
- Crate D가 B와 C를 use하고 1.is_even()을 호출한다.
그럼 B와 C중 어떤 것이 사용돼야하나?
답을 찾을 수 없다.
이러한 시나리오를 예방하기 위해 orphan rule을 정의한 것이다.
orphan rule덕분에, B나 C 중 어느것도 compile되지 않는다.
02_orphan_rule exercise는 현재 crate에 trait이 정의되지 않았고
implementer type도 정의되지 않은 상황에 대한 compile error를 확인하는 것이었다.
이제 traits가 무엇인지는 알았다.
operator overloading으로 돌아가보자.
Operator overloading은 +, -, *, /, ==, != 등과 같은 operators의 행동을 custom하는 것이다.
Rust에서, operators는 traits다.
각 operator에 대해, 상응하는 trait이 있고 operator의 행동을 정의한다.
당신의 type에 대한 trait을 구현함으로써, operators를 사용해 원하는 행동을 할 수 있다.
예를 들어, 다음을 보면 PartialEq trait이 ==과 != operators를 정의한다.
// The `PartialEq` trait definition, from Rust's standard library
// (It is *slightly* simplified, for now)
pub trait PartialEq {
// Required method
//
// `Self` is a Rust keyword that stands for
// "the type that is implementing the trait"
fn eq(&self, other: &Self) -> bool;
// Provided method
fn ne(&self, other: &Self) -> bool { ... }
}
x == y라는 코드를 쓰고 compile할 때, compiler는 x와 y type에 대한 PartialEq trait을 찾는다.
그리고 x == y를 x.eq(y)로 대체한다.
주요 operators에 대한 서신은 다음과 같다.
| Operator | Trait |
| + | Add |
| - | Sub |
| * | Mul |
| / | Div |
| % | Rem |
| == and != | PartialEq |
| <, >, <=, and >= | PartialOrd |
산술 연산자들은 std::ops module에 있고, 비교 연산자들은 std::cmp module에 있다.
PartialEq::ne의 comment는 다음과 같이 말한다.
"ne is a provided method."
이것은 PartialEq이 ne의 trait definition에 대한 기본 구현을 제공한다는 것을 의미한다.
pub trait PartialEq {
fn eq(&self, other: &Self) -> bool;
fn ne(&self, other: &Self) -> bool {
!self.eq(other)
}
}
예상했던 것과 같이, ne는 eq의 negation이다.
기본적인 구현이 제공되기 때문에, 당신의 type에 대한 PartialEq ne를 구현할 필요가 없다.
eq를 구현하는 것으로 충분하다.
struct WrappingU8 {
inner: u8,
}
impl PartialEq for WrappingU8 {
fn eq(&self, other: &WrappingU8) -> bool {
self.inner == other.inner
}
// No `ne` implementation here
}
하지만 기본적인 구현을 꼭 사용할 필요는 없다.
당신은 trait을 구현할 때, override할 것인지 선택할 수 있다.
struct MyType;
impl PartialEq for MyType {
fn eq(&self, other: &MyType) -> bool {
// Custom implementation
}
fn ne(&self, other: &MyType) -> bool {
// Custom implementation
}
}
03_operator_overloading exercise는 Ticket struct에 대한 PartialEq eq를 구현하는 것이었다.
Ticket에 대한 PartialEq를 override하면 되기에, eq만 구현했다.
이 과정에서 String type에 대한 비교도 똑같이 비교연산자로 해줬는데
검색해보니 이 또한 PartialEq에 구현되어 있어서 사용했다.

점점 어려워지는 것 같으면서도 아닌것 같고... 할만한 것 같기도 하다.
사실 개념이 많아서 그렇지 어디서 다 한번씩 해본 것들이다.
인자 수에 따른 다른 method 실행이나, 연산자 오버로딩 등은 다 해봤다.
지금까지 복습이 많이 돼서 이후 배울 내용도 기대가 되는 것은 사실이다.
'Rust Programming' 카테고리의 다른 글
| [Rust] 4. Traits: Deref trait, Sized trait, From trait (0) | 2024.07.29 |
|---|---|
| [Rust] 4. Traits: Derive macros, Trait bounds, String slices (0) | 2024.07.26 |
| [Rust] 3. Ticket v1: Heap, References, Destructors (0) | 2024.07.24 |
| [Rust] 3. Ticket v1: Encapsulation, Ownership, Setters, Stack (0) | 2024.07.23 |
| [Rust] 3. Ticket v1: Structs, Validation, Modules, Visibility (0) | 2024.07.22 |