소개
Advanced Traits에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.
이 랩에서는 "Traits: Defining Shared Behavior"에서 이전에 다루었던 트레이트의 더 심층적인 세부 사항을 살펴볼 것입니다. 이제 Rust 에 대한 이해도가 높아졌으므로 더욱 심도 있는 학습이 가능합니다.
Advanced Traits
"Traits: Defining Shared Behavior"에서 처음 트레이트를 다루었지만, 더 심층적인 세부 사항은 논의하지 않았습니다. 이제 Rust 에 대해 더 많이 알게 되었으니, 자세한 내용으로 들어가 보겠습니다.
연관 타입 (Associated Types)
연관 타입은 타입 플레이스홀더 (type placeholder) 를 트레이트와 연결하여 트레이트 메서드 정의가 이러한 플레이스홀더 타입을 시그니처에서 사용할 수 있도록 합니다. 트레이트를 구현하는 사람은 특정 구현에 대해 플레이스홀더 타입 대신 사용할 구체적인 타입을 지정합니다. 이러한 방식으로, 트레이트가 구현될 때까지 정확히 어떤 타입인지 알 필요 없이 일부 타입을 사용하는 트레이트를 정의할 수 있습니다.
이 장에서 설명하는 대부분의 고급 기능은 거의 필요하지 않다고 설명했습니다. 연관 타입은 중간 정도에 위치합니다. 책의 나머지 부분에서 설명하는 기능보다 드물게 사용되지만, 이 장에서 논의하는 다른 많은 기능보다는 더 일반적으로 사용됩니다.
연관 타입을 가진 트레이트의 한 예는 표준 라이브러리가 제공하는 Iterator 트레이트입니다. 연관 타입은 Item이라는 이름으로 지정되며, Iterator 트레이트를 구현하는 타입이 반복하는 값의 타입을 나타냅니다. Iterator 트레이트의 정의는 Listing 19-12 에 나와 있습니다.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
Listing 19-12: 연관 타입 Item을 가진 Iterator 트레이트의 정의
Item 타입은 플레이스홀더이며, next 메서드의 정의는 Option<Self::Item> 타입의 값을 반환함을 보여줍니다. Iterator 트레이트를 구현하는 사람은 Item에 대한 구체적인 타입을 지정하며, next 메서드는 해당 구체적인 타입의 값을 포함하는 Option을 반환합니다.
연관 타입은 제네릭 (generics) 과 유사한 개념처럼 보일 수 있습니다. 제네릭은 처리할 수 있는 타입을 지정하지 않고 함수를 정의할 수 있도록 해주기 때문입니다. 두 개념의 차이점을 살펴보기 위해, Item 타입을 u32로 지정하는 Counter 타입에 대한 Iterator 트레이트의 구현을 살펴보겠습니다.
Filename: src/lib.rs
impl Iterator for Counter {
type Item = u32;
fn next(&mut self) -> Option<Self::Item> {
--snip--
이 구문은 제네릭의 구문과 유사해 보입니다. 그렇다면 Listing 19-13 과 같이 제네릭을 사용하여 Iterator 트레이트를 정의하지 않는 이유는 무엇일까요?
pub trait Iterator<T> {
fn next(&mut self) -> Option<T>;
}
Listing 19-13: 제네릭을 사용하는 Iterator 트레이트의 가상 정의
차이점은 Listing 19-13 과 같이 제네릭을 사용할 때 각 구현에서 타입을 주석 처리해야 한다는 것입니다. Iterator<``String``> for Counter 또는 다른 모든 타입을 구현할 수 있으므로, Counter에 대한 Iterator의 여러 구현을 가질 수 있습니다. 즉, 트레이트에 제네릭 매개변수가 있는 경우, 제네릭 타입 매개변수의 구체적인 타입을 매번 변경하면서 여러 번 타입에 대해 구현할 수 있습니다. Counter에서 next 메서드를 사용할 때, 어떤 Iterator 구현을 사용하고 싶은지 나타내기 위해 타입 주석을 제공해야 합니다.
연관 타입을 사용하면 여러 번 타입에 대해 트레이트를 구현할 수 없으므로 타입을 주석 처리할 필요가 없습니다. 연관 타입을 사용하는 정의인 Listing 19-12 에서, Item의 타입을 한 번만 선택할 수 있습니다. impl Iterator for Counter는 하나만 있을 수 있기 때문입니다. Counter에서 next를 호출할 때마다 u32 값의 이터레이터를 원한다고 지정할 필요가 없습니다.
연관 타입은 또한 트레이트의 계약의 일부가 됩니다. 트레이트를 구현하는 사람은 연관 타입 플레이스홀더를 대신할 타입을 제공해야 합니다. 연관 타입은 종종 타입이 어떻게 사용될지 설명하는 이름을 가지며, API 문서에서 연관 타입을 문서화하는 것은 좋은 관행입니다.
기본 제네릭 타입 매개변수 및 연산자 오버로딩
제네릭 타입 매개변수를 사용할 때, 제네릭 타입에 대한 기본 구체적인 타입을 지정할 수 있습니다. 이렇게 하면 기본 타입이 작동하는 경우 트레이트를 구현하는 사람이 구체적인 타입을 지정할 필요가 없습니다. <PlaceholderType=ConcreteType> 구문을 사용하여 제네릭 타입을 선언할 때 기본 타입을 지정합니다.
이 기술이 유용한 상황의 훌륭한 예는 연산자 오버로딩입니다. 특정 상황에서 연산자 (예: +) 의 동작을 사용자 정의할 수 있습니다.
Rust 는 사용자 정의 연산자를 만들거나 임의의 연산자를 오버로딩하는 것을 허용하지 않습니다. 그러나 연산자와 관련된 트레이트를 구현하여 std::ops에 나열된 연산과 해당 트레이트를 오버로딩할 수 있습니다. 예를 들어, Listing 19-14 에서 + 연산자를 오버로딩하여 두 개의 Point 인스턴스를 더합니다. Point 구조체에 Add 트레이트를 구현하여 이를 수행합니다.
Filename: src/main.rs
use std::ops::Add;
#[derive(Debug, Copy, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
impl Add for Point {
type Output = Point;
fn add(self, other: Point) -> Point {
Point {
x: self.x + other.x,
y: self.y + other.y,
}
}
}
fn main() {
assert_eq!(
Point { x: 1, y: 0 } + Point { x: 2, y: 3 },
Point { x: 3, y: 3 }
);
}
Listing 19-14: Point 인스턴스에 대한 + 연산자를 오버로딩하기 위해 Add 트레이트 구현
add 메서드는 두 Point 인스턴스의 x 값과 두 Point 인스턴스의 y 값을 더하여 새로운 Point를 생성합니다. Add 트레이트에는 add 메서드에서 반환되는 타입을 결정하는 Output이라는 연관 타입이 있습니다.
이 코드의 기본 제네릭 타입은 Add 트레이트 내에 있습니다. 다음은 해당 정의입니다.
trait Add<Rhs=Self> {
type Output;
fn add(self, rhs: Rhs) -> Self::Output;
}
이 코드는 일반적으로 익숙해야 합니다. 하나의 메서드와 연관 타입을 가진 트레이트입니다. 새로운 부분은 Rhs=Self입니다. 이 구문은 기본 타입 매개변수라고 합니다. Rhs 제네릭 타입 매개변수 ("right-hand side"의 약자) 는 add 메서드의 rhs 매개변수의 타입을 정의합니다. Add 트레이트를 구현할 때 Rhs에 대한 구체적인 타입을 지정하지 않으면, Rhs의 타입은 기본적으로 Self가 되며, 이는 Add를 구현하는 타입이 됩니다.
Point에 대해 Add를 구현했을 때, 두 개의 Point 인스턴스를 더하고 싶었기 때문에 Rhs에 대한 기본값을 사용했습니다. 기본값을 사용하는 대신 Rhs 타입을 사용자 정의하려는 Add 트레이트 구현의 예를 살펴보겠습니다.
Millimeters와 Meters라는 두 개의 구조체가 있으며, 서로 다른 단위로 값을 저장합니다. 다른 구조체에서 기존 타입을 얇게 감싸는 것을 newtype 패턴이라고 하며, "Using the Newtype Pattern to Implement External Traits on External Types"에서 자세히 설명합니다. 밀리미터 단위의 값을 미터 단위의 값에 더하고 Add의 구현이 변환을 올바르게 수행하도록 하려고 합니다. Listing 19-15 와 같이 Meters를 Rhs로 사용하여 Millimeters에 대해 Add를 구현할 수 있습니다.
Filename: src/lib.rs
use std::ops::Add;
struct Millimeters(u32);
struct Meters(u32);
impl Add<Meters> for Millimeters {
type Output = Millimeters;
fn add(self, other: Meters) -> Millimeters {
Millimeters(self.0 + (other.0 * 1000))
}
}
Listing 19-15: Millimeters와 Meters를 더하기 위해 Millimeters에 Add 트레이트 구현
Millimeters와 Meters를 더하려면, Self의 기본값을 사용하는 대신 impl Add<Meters>를 지정하여 Rhs 타입 매개변수의 값을 설정합니다.
기본 타입 매개변수는 주로 두 가지 방식으로 사용합니다.
- 기존 코드를 손상시키지 않고 타입을 확장하기 위해
- 대부분의 사용자가 필요하지 않은 특정 경우에 사용자 정의를 허용하기 위해
표준 라이브러리의 Add 트레이트는 두 번째 목적의 예입니다. 일반적으로 두 개의 동일한 타입을 더하지만, Add 트레이트는 그 이상으로 사용자 정의할 수 있는 기능을 제공합니다. Add 트레이트 정의에서 기본 타입 매개변수를 사용하면 대부분의 경우 추가 매개변수를 지정할 필요가 없습니다. 즉, 약간의 구현 보일러플레이트가 필요하지 않아 트레이트를 사용하기가 더 쉬워집니다.
첫 번째 목적은 두 번째 목적과 유사하지만 반대입니다. 기존 트레이트에 타입 매개변수를 추가하려는 경우, 기존 구현 코드를 손상시키지 않고 트레이트의 기능을 확장할 수 있도록 기본값을 제공할 수 있습니다.
동일한 이름을 가진 메서드 간의 모호성 제거
Rust 에서는 트레이트가 다른 트레이트의 메서드와 동일한 이름을 가진 메서드를 갖는 것을 방지하지 않으며, 한 타입에 두 트레이트를 모두 구현하는 것도 방지하지 않습니다. 또한 트레이트의 메서드와 동일한 이름을 가진 메서드를 타입에 직접 구현하는 것도 가능합니다.
동일한 이름을 가진 메서드를 호출할 때, 어떤 메서드를 사용하고 싶은지 Rust 에 알려줘야 합니다. Listing 19-16 의 코드를 살펴보겠습니다. 여기서는 fly라는 메서드를 모두 가진 Pilot와 Wizard라는 두 개의 트레이트를 정의했습니다. 그런 다음, 이미 fly라는 메서드가 구현된 Human 타입에 두 트레이트를 모두 구현합니다. 각 fly 메서드는 서로 다른 작업을 수행합니다.
Filename: src/main.rs
trait Pilot {
fn fly(&self);
}
trait Wizard {
fn fly(&self);
}
struct Human;
impl Pilot for Human {
fn fly(&self) {
println!("This is your captain speaking.");
}
}
impl Wizard for Human {
fn fly(&self) {
println!("Up!");
}
}
impl Human {
fn fly(&self) {
println!("*waving arms furiously*");
}
}
Listing 19-16: fly 메서드를 갖도록 정의된 두 개의 트레이트가 Human 타입에 구현되었으며, fly 메서드가 Human에 직접 구현되었습니다.
Human의 인스턴스에서 fly를 호출하면, 컴파일러는 Listing 19-17 과 같이 타입에 직접 구현된 메서드를 호출하는 것을 기본으로 합니다.
Filename: src/main.rs
fn main() {
let person = Human;
person.fly();
}
Listing 19-17: Human의 인스턴스에서 fly 호출
이 코드를 실행하면 *waving arms furiously*가 출력되어 Rust 가 Human에 직접 구현된 fly 메서드를 호출했음을 보여줍니다.
Pilot 트레이트 또는 Wizard 트레이트에서 fly 메서드를 호출하려면, 어떤 fly 메서드를 의미하는지 지정하기 위해 더 명시적인 구문을 사용해야 합니다. Listing 19-18 은 이 구문을 보여줍니다.
Filename: src/main.rs
fn main() {
let person = Human;
Pilot::fly(&person);
Wizard::fly(&person);
person.fly();
}
Listing 19-18: 호출하려는 트레이트의 fly 메서드 지정
메서드 이름 앞에 트레이트 이름을 지정하면 Rust 가 호출하려는 fly의 구현을 명확하게 알 수 있습니다. Human::fly(&person)을 작성할 수도 있습니다. 이는 Listing 19-18 에서 사용한 person.fly()와 동일하지만, 모호성을 제거할 필요가 없다면 작성하는 데 약간 더 깁니다.
이 코드를 실행하면 다음과 같이 출력됩니다.
This is your captain speaking.
Up!
*waving arms furiously*
fly 메서드가 self 매개변수를 사용하기 때문에, 두 개의 타입이 모두 하나의 트레이트를 구현하는 경우, Rust 는 self의 타입을 기반으로 사용할 트레이트의 구현을 파악할 수 있습니다.
그러나 메서드가 아닌 연관 함수에는 self 매개변수가 없습니다. 동일한 함수 이름을 가진 비 메서드 함수를 정의하는 여러 타입 또는 트레이트가 있는 경우, 완전한 정규화된 구문을 사용하지 않으면 Rust 는 어떤 타입을 의미하는지 항상 알지 못합니다. 예를 들어, Listing 19-19 에서 모든 강아지 이름을 Spot 으로 지정하려는 동물 보호소에 대한 트레이트를 만듭니다. baby_name이라는 연관 비 메서드 함수가 있는 Animal 트레이트를 만듭니다. Animal 트레이트는 Dog 구조체에 대해 구현되며, 여기에도 baby_name 연관 비 메서드 함수를 직접 제공합니다.
Filename: src/main.rs
trait Animal {
fn baby_name() -> String;
}
struct Dog;
impl Dog {
fn baby_name() -> String {
String::from("Spot")
}
}
impl Animal for Dog {
fn baby_name() -> String {
String::from("puppy")
}
}
fn main() {
println!("A baby dog is called a {}", Dog::baby_name());
}
Listing 19-19: 동일한 이름을 가진 연관 함수와 트레이트를 구현하는 연관 함수를 가진 타입이 있는 트레이트
모든 강아지 이름을 Spot 으로 지정하는 코드를 Dog에 정의된 baby_name 연관 함수에 구현합니다. Dog 타입은 또한 모든 동물이 갖는 특성을 설명하는 Animal 트레이트를 구현합니다. 강아지는 puppy 라고 불리며, 이는 Animal 트레이트의 Dog에 대한 구현에서 Animal 트레이트와 관련된 baby_name 함수로 표현됩니다.
main에서 Dog::baby_name 함수를 호출하면, Dog에 직접 정의된 연관 함수가 호출됩니다. 이 코드는 다음을 출력합니다.
A baby dog is called a Spot
이 출력은 우리가 원하는 것이 아닙니다. Dog에 구현된 Animal 트레이트의 일부인 baby_name 함수를 호출하여 코드가 A baby dog is called a puppy를 출력하도록 하려고 합니다. Listing 19-18 에서 사용한 트레이트 이름을 지정하는 기술은 여기서는 도움이 되지 않습니다. Listing 19-20 의 코드로 main을 변경하면 컴파일 오류가 발생합니다.
Filename: src/main.rs
fn main() {
println!("A baby dog is called a {}", Animal::baby_name());
}
Listing 19-20: Animal 트레이트에서 baby_name 함수를 호출하려고 시도하지만, Rust 는 어떤 구현을 사용할지 알 수 없습니다.
Animal::baby_name에는 self 매개변수가 없고, Animal 트레이트를 구현하는 다른 타입이 있을 수 있으므로, Rust 는 어떤 Animal::baby_name 구현을 원하는지 파악할 수 없습니다. 다음과 같은 컴파일러 오류가 발생합니다.
error[E0283]: type annotations needed
--> src/main.rs:20:43
|
20 | println!("A baby dog is called a {}", Animal::baby_name());
| ^^^^^^^^^^^^^^^^^ cannot infer
type
|
= note: cannot satisfy `_: Animal`
모호성을 제거하고 다른 타입에 대한 Animal 구현이 아닌 Dog에 대한 Animal 구현을 사용하려는 것을 Rust 에 알리려면, 완전한 정규화된 구문을 사용해야 합니다. Listing 19-21 은 완전한 정규화된 구문을 사용하는 방법을 보여줍니다.
Filename: src/main.rs
fn main() {
println!(
"A baby dog is called a {}",
<Dog as Animal>::baby_name()
);
}
Listing 19-21: Dog에 구현된 Animal 트레이트에서 baby_name 함수를 호출하려는 것을 지정하기 위해 완전한 정규화된 구문 사용
꺽쇠 괄호 안에 타입 주석을 제공하여, 이 함수 호출에 대해 Dog 타입을 Animal로 취급하겠다고 말함으로써 Dog에 구현된 Animal 트레이트에서 baby_name 메서드를 호출하려는 것을 Rust 에 나타냅니다. 이 코드는 이제 우리가 원하는 것을 출력합니다.
A baby dog is called a puppy
일반적으로 완전한 정규화된 구문은 다음과 같이 정의됩니다.
<Type as Trait>::function(receiver_if_method, next_arg, ...);
메서드가 아닌 연관 함수의 경우, receiver가 없습니다. 다른 인수의 목록만 있을 것입니다. 함수 또는 메서드를 호출하는 모든 곳에서 완전한 정규화된 구문을 사용할 수 있습니다. 그러나 Rust 가 프로그램의 다른 정보에서 파악할 수 있는 이 구문의 모든 부분을 생략할 수 있습니다. 동일한 이름을 사용하는 여러 구현이 있고 Rust 가 호출하려는 구현을 식별하는 데 도움이 필요한 경우에만 이 더 자세한 구문을 사용해야 합니다.
Supertrait 사용하기
때로는 다른 트레이트에 의존하는 트레이트 정의를 작성할 수 있습니다. 첫 번째 트레이트를 구현하려면 해당 타입이 두 번째 트레이트도 구현하도록 요구하고 싶을 것입니다. 이렇게 하면 트레이트 정의에서 두 번째 트레이트의 연관 항목을 사용할 수 있습니다. 트레이트 정의가 의존하는 트레이트를 해당 트레이트의 supertrait라고 합니다.
예를 들어, 별표로 프레임이 지정되도록 주어진 값을 출력하는 outline_print 메서드가 있는 OutlinePrint 트레이트를 만들고 싶다고 가정해 보겠습니다. 즉, 표준 라이브러리 트레이트 Display를 구현하여 (x, y)가 되도록 하는 Point 구조체가 주어지면, x가 1이고 y가 3인 Point 인스턴스에서 outline_print를 호출하면 다음과 같이 출력되어야 합니다.
**********
* *
* (1, 3) *
* *
**********
outline_print 메서드의 구현에서 Display 트레이트의 기능을 사용하고 싶습니다. 따라서 OutlinePrint 트레이트가 Display도 구현하고 OutlinePrint에 필요한 기능을 제공하는 타입에 대해서만 작동하도록 지정해야 합니다. OutlinePrint: Display를 지정하여 트레이트 정의에서 이를 수행할 수 있습니다. 이 기술은 트레이트 바운드를 트레이트에 추가하는 것과 유사합니다. Listing 19-22 는 OutlinePrint 트레이트의 구현을 보여줍니다.
Filename: src/main.rs
use std::fmt;
trait OutlinePrint: fmt::Display {
fn outline_print(&self) {
let output = self.to_string();
let len = output.len();
println!("{}", "*".repeat(len + 4));
println!("*{}*", " ".repeat(len + 2));
println!("* {} *", output);
println!("*{}*", " ".repeat(len + 2));
println!("{}", "*".repeat(len + 4));
}
}
Listing 19-22: Display의 기능을 요구하는 OutlinePrint 트레이트 구현
OutlinePrint가 Display 트레이트를 요구하도록 지정했으므로, Display를 구현하는 모든 타입에 대해 자동으로 구현되는 to_string 함수를 사용할 수 있습니다. 트레이트 이름 뒤에 콜론을 추가하고 Display 트레이트를 지정하지 않고 to_string을 사용하려고 하면, 현재 범위에서 &Self 타입에 대해 to_string이라는 메서드가 발견되지 않았다는 오류가 발생합니다.
Point 구조체와 같이 Display를 구현하지 않는 타입에 대해 OutlinePrint를 구현하려고 할 때 어떤 일이 발생하는지 살펴보겠습니다.
Filename: src/main.rs
struct Point {
x: i32,
y: i32,
}
impl OutlinePrint for Point {}
Display가 필요하지만 구현되지 않았다는 오류가 발생합니다.
error[E0277]: `Point` doesn't implement `std::fmt::Display`
--> src/main.rs:20:6
|
20 | impl OutlinePrint for Point {}
| ^^^^^^^^^^^^ `Point` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Point`
= note: in format strings you may be able to use `{:?}` (or {:#?} for
pretty-print) instead
note: required by a bound in `OutlinePrint`
--> src/main.rs:3:21
|
3 | trait OutlinePrint: fmt::Display {
| ^^^^^^^^^^^^ required by this bound in `OutlinePrint`
이를 해결하려면, Point에 Display를 구현하고 OutlinePrint가 요구하는 제약 조건을 충족합니다.
Filename: src/main.rs
use std::fmt;
impl fmt::Display for Point {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
그런 다음, Point에 OutlinePrint 트레이트를 구현하면 성공적으로 컴파일되고, Point 인스턴스에서 outline_print를 호출하여 별표 윤곽선 내에 표시할 수 있습니다.
Newtype 패턴을 사용하여 외부 트레이트 구현하기
"타입에 트레이트 구현하기"에서, 트레이트 또는 타입, 또는 둘 다가 우리 크레이트에 로컬인 경우에만 타입에 트레이트를 구현할 수 있다는 고아 규칙 (orphan rule) 을 언급했습니다. newtype 패턴을 사용하여 이 제한을 우회할 수 있습니다. 이 패턴은 튜플 구조체에서 새로운 타입을 생성하는 것을 포함합니다. (튜플 구조체는 "이름 없는 필드를 사용하여 다른 타입 생성하기"에서 다루었습니다.) 튜플 구조체는 하나의 필드를 가지며, 트레이트를 구현하려는 타입 주위에 얇은 래퍼 (wrapper) 가 됩니다. 그러면 래퍼 타입은 우리 크레이트에 로컬이 되며, 래퍼에 트레이트를 구현할 수 있습니다. Newtype은 Haskell 프로그래밍 언어에서 유래된 용어입니다. 이 패턴을 사용하는 데는 런타임 성능 저하가 없으며, 래퍼 타입은 컴파일 시간에 제거됩니다.
예를 들어, 고아 규칙으로 인해 직접 수행할 수 없는 Vec<T>에 Display를 구현하고 싶다고 가정해 보겠습니다. Display 트레이트와 Vec<T> 타입은 우리 크레이트 외부에서 정의되기 때문입니다. Vec<T>의 인스턴스를 보유하는 Wrapper 구조체를 만들 수 있습니다. 그런 다음 Listing 19-23 과 같이 Wrapper에 Display를 구현하고 Vec<T> 값을 사용할 수 있습니다.
Filename: src/main.rs
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(vec![
String::from("hello"),
String::from("world"),
]);
println!("w = {w}");
}
Listing 19-23: Display를 구현하기 위해 Vec<String> 주위에 Wrapper 타입을 생성
Display의 구현은 내부 Vec<T>에 접근하기 위해 self.0을 사용합니다. Wrapper가 튜플 구조체이고 Vec<T>가 튜플의 인덱스 0 에 있는 항목이기 때문입니다. 그런 다음 Wrapper에서 Display 타입의 기능을 사용할 수 있습니다.
이 기술을 사용하는 단점은 Wrapper가 새로운 타입이므로, 보유하고 있는 값의 메서드가 없다는 것입니다. Vec<T>의 모든 메서드를 Wrapper에 직접 구현하여 메서드가 self.0에 위임하도록 해야 합니다. 이렇게 하면 Wrapper를 정확히 Vec<T>처럼 취급할 수 있습니다. 새로운 타입이 내부 타입이 가진 모든 메서드를 갖기를 원한다면, 내부 타입을 반환하도록 Wrapper에 Deref 트레이트를 구현하는 것이 해결책이 될 것입니다 ( "Deref 를 사용하여 스마트 포인터를 일반 참조처럼 취급하기"에서 Deref 트레이트 구현에 대해 논의했습니다). Wrapper 타입이 내부 타입의 모든 메서드를 갖기를 원하지 않는 경우 (예: Wrapper 타입의 동작을 제한하려는 경우), 원하는 메서드만 수동으로 구현해야 합니다.
이 newtype 패턴은 트레이트가 관련되지 않은 경우에도 유용합니다. 초점을 전환하여 Rust 의 타입 시스템과 상호 작용하는 몇 가지 고급 방법을 살펴보겠습니다.
요약
축하합니다! 고급 트레이트 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.