지금까지 배운 두 가지 traits인 From과 Deref에 대해 다시 확인해보자.
pub trait From<T> {
fn from(value: T) -> Self;
}
pub trait Deref {
type Target;
fn deref(&self) -> &Self::Target;
}
둘 다 type parameters를 feature로 한다.
From의 경우, generic parameter인 T이고
Deref의 경우, associated type인 Target이다.
차이점이 무엇일까?
왜 서로 다른 것을 사용할까?
deref coercion이 동작하는 방식 때문에,
주어진 type에 오직 하나의 target type이 올 수 있다.
(String은 오직 str에만 deref 가능하다)
그것은 ambiguity를 피하기 위해서인데,
만약 당신이 하나의 type에 대해 Deref를 여러번 구현한다면,
당신이 &self method를 호출할 때, compiler는 어떤 Target type을 골라야할까?
이것이 Deref가 associated type인 Target을 사용하는 이유다.
하나의 associated type은 trait implementation에 의해 특별히 결정된다.
Deref를 두 번 이상 구현할 수 없기 때문에, 특정 type에 대해 하나의 Target만 지정할 수 있으며,
이러면 더이상 ambiguity가 없다.
반면, input type T가 달라지는 한, 하나의 type에 대해 From은 여러 번 구현할 수 있다.
예를 들어, 당신이 u32와 u16을 input types로 사용하는 Wrappingu32를 위한 From을 구현한다고 하자.
impl From<u32> for WrappingU32 {
fn from(value: u32) -> Self {
WrappingU32 { inner: value }
}
}
impl From<u16> for WrappingU32 {
fn from(value: u16) -> Self {
WrappingU32 { inner: value.into() }
}
}
이건 From<u16>과 From<u32>가 서로다른 traits로 간주되기 때문에 작동한다.
이곳에는 ambiguity가 존재하지 않는다.
compiler는 변환하려는 값의 type에 기반하여 어떤 구현을 사용할 지 결정할 수 있기 때문이다.
standard library의 Add trait을 보자.
pub trait Add<RHS = Self> {
type Output;
fn add(self, rhs: RHS) -> Self::Output;
}
위의 예시는 두가지의 mechanisms를 활용한다.
- Self를 default로 하는 generic parameter인 RHS가 있다.
- addition의 결과의 associated type인 Output이 있다.
RHS는 서로다른 types가 더하기 연산을 할 수 있도록 돕는 generic parameter다.
예를 들어, 당신이 standard library에서 다음 두 구현을 찾을 수 있을 것인데
impl Add<u32> for u32 {
type Output = u32;
fn add(self, rhs: u32) -> u32 {
// ^^^
// This could be written as `Self::Output` instead.
// The compiler doesn't care, as long as the type you
// specify here matches the type you assigned to `Output`
// right above.
// [...]
}
}
impl Add<&u32> for u32 {
type Output = u32;
fn add(self, rhs: &u32) -> u32 {
// [...]
}
}
위 코드는 다음 코드를 compile가능하게 한다.
let x = 5u32 + &5u32 + 6u32;
왜냐하면 u32는 Add<&u32>뿐만 아니라 Add<u32>도 구현했기 때문이다.
Output은 addition 결과의 type을 나타낸다.
왜 첫번째 줄에 Ouput이 필요할까?
그냥 Add를 구현한 type인 Self를 output으로 사용하면 안될까?
할 순 있지만, trait의 flexibility를 제한할 수 있다.
예를 들어, standard library에서 다음과 같은 구현을 찾을 수 있다.
impl Add<&u32> for &u32 {
type Output = u32;
fn add(self, rhs: &u32) -> u32 {
// [...]
}
}
trait을 구현할 type은 &u32다.
하지만 addition의 결과는 u32다.
add가 Self를 return하게 한다면, 위 구현은 불가능하다.
Output을 사용하면 std가 implementor를 return type에서 분리하여, 이것을 가능하게 한다.
반면, Output은 generic parameter가 될 수 없다.
피연산자의 type을 알고 나면 연산의 output type을 uniquely하게 결정해야한다.
이게 associated type인 이유다.
implementor와 generic parameters의 주어진 조합에 대해 오직 하나의 Output type만 있을 수 있다.
복습하자.
- 주어진 trait implementation에 type이 uniquely하게 결정돼야한다면, associated type을 사용해라.
- 같은 타입, 다른 input types에 대한 trait의 여러 구현을 하고 싶다면, generic parameter를 사용해라.
10_assoc_vs_generic exercise는 서로 다른 type에 대한 power 연산을 구현하는 것이었다.
macro를 사용하면 편할 수 있다고 했지만, 그냥 직접 구현했다.
반복문을 사용했으며, 아래와 같이 테스트를 잘 통과했다.

여기서 reference의 값에 대한 접근인 *을 사용했다.
이전 chapter에서 ownership과 borrowing에 대해 배웠다.
특히 아래를 중요하게 언급했다.
- Rust의 모든 value는 어떤 시점에든 하나의 owner가 있다.
- function이 value의 ownership을 가져가면, caller는 더이상 해당 value를 사용하지 못한다.
이 제한들은 다소 불편할 수 있다.
가끔 우리는 value의 ownership을 가져가는 함수를 호출해야할 때가 있다.
하지만 우리는 이후에도 그 value를 사용하고 싶을 땐, 어떻게 해야할까?
fn consumer(s: String) { /* */ }
fn example() {
let mut s = String::from("hello");
consumer(s);
s.push_str(", world!"); // error: value borrowed here after move
}
Clone을 사용하면 된다.
Clone은 Rust의 standard library에 정의된 trait이다.
pub trait Clone {
fn clone(&self) -> Self;
}
clone method는 self의 reference를 취하고 같은 type의 새로운 owned instance를 return한다.
위의 예시로 다시 가보자.
우리는 consumer를 호출하기 전에 clone을 사용하여 새로운 String instance를 만들 수 있다.
fn consumer(s: String) { /* */ }
fn example() {
let mut s = String::from("hello");
let t = s.clone();
consumer(t);
s.push_str(", world!"); // no error
}
s의 ownership을 consumer에게 주지 않고, 우리는 새로운 String(s를 clone한)을 consumer에게 준다.
그러면 s는 여전히 valid하고 consumer 호출 후에도 사용가능하다.
위의 예시에서 memory에서는 어떤 일이 일어나는지 보자.
let mut s:String::from("hello");가 실행될 때, memory는 다음과 같다.
s
+---------+--------+----------+
Stack | pointer | length | capacity |
| | | 5 | 5 |
+--|------+--------+----------+
|
|
v
+---+---+---+---+---+
Heap: | H | e | l | l | o |
+---+---+---+---+---+
let t = s.clone()이 실행될 때, heap에 새로운 영역이 할당되고 data의 copy가 저장된다.
s t
+---------+--------+----------+ +---------+--------+----------+
Stack | pointer | length | capacity | | pointer | length | capacity |
| | | 5 | 5 | | | | 5 | 5 |
+--|------+--------+----------+ +--|------+--------+----------+
| |
| |
v v
+---+---+---+---+---+ +---+---+---+---+---+
Heap: | H | e | l | l | o | | H | e | l | l | o |
+---+---+---+---+---+ +---+---+---+---+---+
Java 같은 언어를 이전에 했다면, object의 deep copy라고 생각하면 편하다.
type이 Clone가능하게 하려면, type을 위한 Clone trait을 구현해야한다.
거의 deriving을 통해 Clone을 구현할 수 있다.
#[derive(Clone)]
struct MyType {
// fields
}
compiler는 당신이 예상한대로 MyType의 Clone을 구현한다.
MyType의 각 field를 clone하고 새로운 MyType instance를 해당 clone된 fields를 이용해 만든다.
derive macro에 의해 만들어진 코드는 cargo expand를 통해 확인할 수 있다는 것을 기억하자.
11_clone exercise는 compile되게 하는 것이다.
derive macro를 사용해 struct에 대한 Clone을 구현해주고
summary함수에서 ticket을 clone해 ticket과 clone한 것을 따로 return해줬다.
그랬더니 compile이 됐다.
이전과 비슷하지만 조금 다른 예시를 생각해보자.
String 대신 u32을 사용했을 때다.
fn consumer(s: u32) { /* */ }
fn example() {
let s: u32 = 5;
consumer(s);
let t = s + 1;
}
error없이 compile이 될 것이다.
어째서?
.clone()없이도 작동되게 하는 String과 u32의 차이점이 무엇일까?
Copy는 Rust의 standard library에 정의된 또 다른 trait이다.
pub trait Copy: Clone { }
그것은 Sized와 같은 marker trait이다.
만약 type이 Copy를 구현하면, type의 새로운 instance를 .clone()을 통해 만들 필요가 없다.
Rust가 implicitly하게 해준다.
u32는 Copy를 구현한 type 중 하나여서, 위 코드가 compile error나지 않는다.
comsumer(s)가 호출되면, Rust는 s의 bitwise copy를 통해 새로운 u32 instance를 만들고,
그것을 consumer에게 넘긴다.
이것이 당신이 아무것도 하지 않아도 뒤에서 알아서 일어나는 일이다.
Copy는 automatic cloning을 의미하지만, 동일시되진 않는다.
type은 Copy를 구현하기 위한 몇가지 요구사항들이 있다.
먼저, Clone을 구현해야한다.
왜냐하면 Copy는 Clone의 subtrait이기 때문이다.
만약 Rust가 type의 새로운 instance를 implicitly하게 만들 수 있다면,
type의 새로운 instance를 .clone()을 통해 explicitly하게 만들 수 있어야한다.
하지만 이게 다가 아니다.
몇가지 조건이 더 남았다.
- type은 memory에서 차지하는 std::mem::size_of bytes를 초과하는
추가 리소스(heap memory, file handles 등)를 관리하지 않는다. - type은 mutable reference(&mut T)가 아니다.
이 모든 조건들이 충족되면, Rust는 original instance에 대한 bitwise copy로
type의 새로운 instance를 안전하게 만들 수 있다.
bitwise copy는 C standard library function의 memcpy operation이랑 같다.
String은 Copy를 구현하지 않은 type이다.
왜일까?
왜냐하면, String은 string의 data를 저장하는
heap-allocated buffer가 additional resource로 존재하기 때문이다.
Rust가 String이 Copy를 구현할 수 있게 했다고 상상해보자.
original instance에 대해 bitwise copy가 진행돼 새로운 String instance가 만들어 질 때,
original과 new instance는 같은 memory buffer를 가리킬 것이다.
s copied_s
+---------+--------+----------+ +---------+--------+----------+
| pointer | length | capacity | | pointer | length | capacity |
| | | 5 | 5 | | | | 5 | 5 |
+--|------+--------+----------+ +--|------+--------+----------+
| |
| |
v |
+---+---+---+---+---+ |
| H | e | l | l | o | |
+---+---+---+---+---+ |
^ |
| |
+------------------------------------+
String instances는 scope 밖으로 나갈 때, memory buffer를 free하려 할 것이다.
그러면 double-free-error가 발생하는 것이다.
또한 같은 memory buffer를 가리키는
두 개의 구별된 &mut String references를 만들 수 있을 것이고,
이는 Rust의 borrowing rules에 어긋난다.
u32는 Copy를 구현한다.
사실 모든 integer types가 Copy를 구현한다.
하나의 integer는 그저 memory의 number를 나타내는 bytes다.
그게 끝이다.
만약 해당 bytes를 copy한다면,
또다른 valid한 integer instance를 완벽하게 얻는다.
아무 나쁜 일도 일어나지 않으므로, Rust는 인정해주는 것이다.
ownership과 mutable borrows에 대해 얘기했을 때,
다음과 같은 rule을 언급했다.
- 어느 시점이더라도 value의 mutable borrow는 오직 하나만 존재할 수 있다.
이것이 u32는 Copy를 구현했더라도, &mut u32가 Copy를 구현하지 않은 이유다.
만약 &mut u32가 Copy를 구현했다면,
당신은 같은 value에 대해 여러개의 mutable references를 만들 수 있으며,
동시에 여러 곳에서 그것을 수정할 수 있을 것이다.
이는 Rust의 borrowing rules에 어긋난다.
그래서 T가 무엇이든 &mut T는 절대 Copy를 구현할 수 없다.
대부분의 경우에, 직접 Copy를 구현할 필요가 없다.
그냥 이와 같이 derive하면 된다.
#[derive(Copy, Clone)]
struct MyStruct {
field: u32,
}
12_copy exercise는 compile error를 확인해가며 무엇이 필요한지 하나하나 고쳐가며 하는 과정이었다.
일단 std::ops::Add trait에 대한 add function을 custom해야했고
wrapping_u32에 대한 debug, clone, copy, partialeq trait을 derive해줘야했다.
역시나 중간에 막힌 채로 10분이상 지나 solution을 보고 역추적했는데
동작하는 mechanism이 어떻게 되는지는 이해했다.

내가 만든 type에 대한 traits를 골라서 쓰는 것이고
어떤 연산을 가능하게 할지 결정할 수 있었다.
value가 consume되는 문제 때문에 copy trait이 들어갔고 clone은 따라왔다.
또한 assert_eq! 때문에 Partialeq와 Debug가 필요했다.
반복하다보면 뭔가 익숙해질 것 같다.
역시나 재밌다.
'Rust Programming' 카테고리의 다른 글
| [Rust] 5. Ticket v2: Enums, Branching(match) (0) | 2024.07.31 |
|---|---|
| [Rust] 4. Traits: Drop trait (0) | 2024.07.31 |
| [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] 4. Traits: Trait, Orphan rule, Operator overloading (0) | 2024.07.25 |