Поглиблено про Трейти

Ми вже розповідали про трейти у підрозділі “Трейти: визначення загальної поведінки” Розділу 10, але ми не говорили про глибші деталі. Тепер, коли ви більше знаєте про Rust, ми можемо перейти до дрібніших деталей.

Зазначення Заповнювача Типу у Визначенні Трейтів за Допомогою Асоційованих Типів

Асоційовані типи зв'язують заповнювач типу з трейтом таким чином, що методи трейту можуть використовувати ці заповнювачі типу у своїх сигнатурах. Той, хто реалізовуватиме трейт, зазначить конкретний тип, що буде використовуватися, замість заповнювача типу для конкретної реалізації. Таким чином ми можемо визначити трейт, що використовує деякі типи, без потреби точно знати ці типи до моменту реалізації трейту.

Ми описували більшість поглиблених особливостей у цьому розділі як такі, що рідко потрібні. Пов'язані типи десь посередині: вони використовуються рідше за функціонал, описаний в решті книги, але частіше ніж багато іншого функціоналу, обговорюваного в цьому розділі.

Одним з прикладів трейту з асоційованим типом є трейт Iterator, наданий стандартною бібліотекою. Асоційований тип називається Item і позначає тип значень, по яких ітерує тип, що реалізує трейт Iterator. Визначення трейту Iterator показано у Блоці коду 19-12.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;
}

Блок коду 19-12: визначення трейту Iterator, що має асоційований тип Item

Тип Item є заповнювачем, і визначення методу next показує, що він повертає значення типу Option<Self::Item>. Ті, хто реалізовуватимуть трейт Iterator, зазначать конкретний тип для Item, і метод next повертатиме Option, що міститиме значення цього конкретного типу.

Асоційовані типи можуть видатися концепцією, подібною до узагальнень, у тому, що останні дозволяють визначити функцію без зазначення, які типи вона може обробляти. Для вивчення відмінностей між двома концепціями, погляньмо на реалізацію трейту Iterator для типу, що зветься Counter із зазначеним типом Item u32:

Файл: src/lib.rs

struct Counter {
    count: u32,
}

impl Counter {
    fn new() -> Counter {
        Counter { count: 0 }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        // --snip--
        if self.count < 5 {
            self.count += 1;
            Some(self.count)
        } else {
            None
        }
    }
}

Цей синтаксис здається схожим на узагальнені параметри. То чому ж просто не визначити трейт Iterator з узагальненим параметром, як показано в Блоці коду 19-13?

pub trait Iterator<T> {
    fn next(&mut self) -> Option<T>;
}

Блок коду 19-13: гіпотетичне визначення трейту Iterator за допомогою узагальненого параметра

Різниця полягає в тому, що при використанні узагальнених параметрів, як у Блоці коду 19-13, ми маємо анотувати типи у кожній реалізації; а оскільки ми також можемо реалізувати Iterator<String> для Counter чи будь-якого іншого типу, ми можемо мати багато реалізацій Iterator для Counter. Іншими словами, коли трейт має узагальнений параметр, він може бути реалізованим для типу багато разів, кожного разу для іншого конкретного типу узагальненого параметра. Коли ми використовуємо метод next для Counter, нам доведеться надавати анотації типу, щоб позначити, яку реалізацію Iterator ми хочемо використати.

З асоційованими типами нам не треба анотувати типи, бо ми не можемо реалізувати трейт для типу кілька разів. У Блоці коду 19-12, з визначенням, яке використовує асоційовані типи, ми можемо обрати тип Item лише один раз, бо може бути лише один impl Iterator for Counter. Нам не треба зазначати, що ми хочемо ітератор по значеннях u32 всюди, де ми викликаємо next для Counter.

Асоційовані типи також стають частиною контракту трейту: ті, хто реалізують трейт, мають зазначити тип для заповнювача асоційованого типу. Асоційовані типи часто мають назву, що описує, як цей тип буде використано, і документування асоційованого типу в документації API є доброю практикою.

Узагальнені Параметри Типу за Замовчуванням і Перевантаження Операторів

Коли ми використовуємо узагальнені параметри типу, то можемо вказати конкретний тип за замовчуванням для узагальненого типу. Це усуває потребу для тих, хто реалізовуватиме трейт, вказувати конкретний тип, якщо тип за замовчанням працює. Ви можете вказати тип за замовчуванням при проголошенні узагальненого типу за допомогою синтаксису <PlaceholderType=ConcreteType>.

Чудовий приклад ситуації, коли ця техніка корисна, це перевантаження операторів, де ви налаштовуєте поведінку оператора (наприклад, +) в певних ситуаціях.

Rust не дозволяє вам створювати власні оператори або перевантажувати довільні оператори. Але ви можете перевантажити операції і відповідні трейти, перелічені в std::ops, реалізувавши трейти, пов'язані з оператором. Наприклад, у Блоці коду 19-14 ми перевантажуємо оператор +, щоб додавати два екземпляри Point. Ми робимо це, реалізуючи трейт Add для Point:

Файл: 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 }
    );
}

Блок коду 19-14: реалізація трейту Add для перевантаження оператора + для екземплярів Point

Метод add додає значення x двох екземплярів Point і значення y двох екземплярів Point, щоб створити нову Point. Трейт Add має асоційований тип, що називається Output, який визначає тип, який повертає метод add.

Узагальнений параметр типу за замовчанням у цьому коді належить трейту Add. Ось його визначення:

#![allow(unused)]
fn main() {
trait Add<Rhs=Self> {
    type Output;

    fn add(self, rhs: Rhs) -> Self::Output;
}
}

Цей код має виглядати в цілому знайомо: трейт з одним методом і асоційованим типом. Новим тут є Rhs=Self: цей синтаксис зветься параметром типу за замовчанням. Узагальнений параметр типу Rhs (скорочено для "right hand side" - "правий бік") визначає тип параметра rhs у методі add. Якщо ми не зазначимо конкретний тип для Rhs, коли ми реалізуємо трейт Add, тип Rhs буде взято за замовчанням як Self, тобто тип, для якого ми реалізуємо Add.

Коли ми реалізували Add для Point, ми використали значення за замовчанням для Rhs, бо ми хотіли додавати два екземпляри Point. Розгляньмо приклад реалізації трейта Add, де ми хочемо виставити свій тип Rhs, а не використовувати значення за замовчуванням.

Ми маємо дві структури, Millimeters і Meters, що містять значення в різних одиницях. Ця тонка обгортка типу, що існує, у іншу структуру відома як шаблон новий тип, який ми описували детальніше у підрозділі “Використання шаблону новий тип для реалізації зовнішніх трейтів на зовнішніх типах” . Ми хочемо додавати значення у міліметрах до значень у метрах, щоб реалізація

Add коректно виконувала перетворення. Ми можемо реалізувати Add для Millimeters з Meters як Rhs, як показано в Блоці коду 19-15.

Файл: 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))
    }
}

Блок коду 19-15: реалізація трейту Add для Millimeters, щоб додавати Millimeters до Meters

Щоб додати Millimeters і Meters, вказуємо impl Add<Meters>, щоб встановити значення параметра типу Rhs замість встановленого за замовчуванням Self.

Параметри типу за замовчанням використовуються у двох основних випадках:

  • Щоб розширити тип, не порушуючи коду, що існує
  • Щоб дозволити налаштування у певних випадках, не потрібних більшості користувачів

Трейт Add зі стандартної бібліотеки є прикладом другого призначення: зазвичай ви додаєте однакові типи, але трейт Add надає можливість глибшого налаштування. Використовуючи параметр типу за замовчуванням в трейті Add означає, що у більшості випадків вам не потрібно вказувати додатковий параметр. Іншими словами, не потрібно вказувати шматок шаблонного коду реалізації, що полегшує використання трейту.

Перше призначення схоже на друге, але навпаки: якщо ви хочете додати параметр типу до трейту, що вже існує, ви можете надати йому параметр типу за замовчуванням, щоб розширити функціональність трейту не порушивши наявного коду реалізації.

Повністю Кваліфікований Синтаксис для Уникнення Двозначностей: Виклик Методів з Однаковою Назвою

Ніщо у Rust не забороняє трейту мати метод з такою самою назвою, як і в іншому трейті, ані реалізувати обидва трейти для одного типу. Також можна реалізувати метод з такою ж назвою, як у трейтах, напряму для типу.

При виклику методів з однаковою назвою вам треба вказати Rust, котрий саме метод ви хочете використати. Розгляньмо код у Блоці коду 19-16, де ми визначили два трейти, Pilot і Wizard, що обидва мають метод fly. Тоді ми реалізуємо обидва трейти для типу Human, що також має реалізований на ньому метод з назвою fly. Кожен метод fly робить щось інше.

Файл: 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*");
    }
}

fn main() {}

Блок коду 19-16: два трейти маю визначення, що містить метод "fly" і реалізовані для типу Human, а також метод fly, реалізований безпосередньо для Human

Коли ми викликаємо fly на екземплярі Human, компілятор за замовчуванням викликає метод, реалізований безпосередньо на типі, як показано у Блоці коду 19-17.

Файл: 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*");
    }
}

fn main() {
    let person = Human;
    person.fly();
}

Блок коду 19-17: виклик fly для екземпляра Human

Запуск цього коду виведе *waving arms furiously*, показуючи, що Rust викликав метод fly, реалізований безпосередньо для Human.

Щоб викликати методи fly з трейту Pilot або трейту Wizard, нам треба використати більш явний синтаксис, щоб зазначити, який саме метод fly ми маємо на увазі. Блок коду 19-18 демонструє такий синтаксис.

Файл: 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*");
    }
}

fn main() {
    let person = Human;
    Pilot::fly(&person);
    Wizard::fly(&person);
    person.fly();
}

Блок коду 19-18: уточнення, метод fly якого саме трейту ми хочемо викликати

Зазначення назви трейту перед назвою методу прояснює для Rust, котру реалізацію fly ми хочемо викликати. Ми також могли б написати Human::fly(&person), що є еквівалентом person.fly(), який ми використали в Блоці коду 19-18, але так трохи довше писати, якщо нам не треба уникнути двозначності.

Виконання цього коду виведе наступне:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target/debug/traits-example`
This is your captain speaking.
Up!
*waving arms furiously*

Оскільки метод fly приймає параметр self, то якщо ми маємо два типи, що обидва реалізують один трейт, Rust може зрозуміти, яку реалізацію трейту використати, виходячи з типу self.

Однак асоційовані функції, що не є методами, не мають параметру self. Коли є багато типів чи трейтів, що визначають функції, що не є методами, з однаковими назвами, Rust не завжди знає, який тип ви мали на увазі, якщо ви не використаєте повний кваліфікований синтаксис. Наприклад, у Блоці коду 19-19 ми створюємо трейт для притулку тварин, що хоче називати всіх маленьких собак Spot. Ми створюємо трейт Animal з асоційованою функцією - не методом baby_name. Трейт Animal реалізований для структури Dog, для якої ми також визначаємо напряму асоційовану функцію - не метод baby_name.

Файл: 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());
}

Блок коду 19-19: трейт з асоційованою функцією і тип з асоційованою функцією з такою ж назвою, що реалізує цей трейт

Ми реалізували код для називання всіх цуценят Spot у асоційованій функції baby_name, визначеній для Dog. Тип Dog також реалізує трейт Animal, що описує характеристики, спільні для всіх тварин. Дитинчата собак звуться цуценятами, і це виражено в реалізації трейту Animal доя Dog у функції baby_name, асоційованій з трейтом Animal.

У main ми викликаємо функцію Dog::baby_name, яка викликає асоційовану функцію, визначену безпосередньо для Dog. Цей код виводить таке:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.54s
     Running `target/debug/traits-example`
A baby dog is called a Spot

Ми хотіли не такого виведення. Ми хотіли викликати функцію baby_name, що є частиною трейту Animal, який ми реалізували для Dog, щоб код вивів A baby dog is called a puppy. Техніка зазначення назви трейту, яку ми використали у Блоці коду 19-18 тут не допомагає; якщо ми змінимо main на код, наведений у Блоці коду 19-20, ми отримаємо помилку компіляції.

Файл: 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 {}", Animal::baby_name());
}

Блок коду 19-20: спроба викликати функцію baby_name з трейту Animal, але Rust не знає, яку реалізацію використати

Оскільки Animal::baby_name не має параметру self, і можуть бути інші типи, що реалізують трейт Animal, Rust не може з'ясувати, яку реалізацію Animal::baby_name ми хочемо. Ми отримуємо цю помилку компілятора:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
error[E0790]: cannot call associated function on trait without specifying the corresponding `impl` type
  --> src/main.rs:20:43
   |
2  |     fn baby_name() -> String;
   |     ------------------------- `Animal::baby_name` defined here
...
20 |     println!("A baby dog is called a {}", Animal::baby_name());
   |                                           ^^^^^^^^^^^^^^^^^ cannot call associated function of trait
   |
help: use the fully-qualified path to the only available implementation
   |
20 |     println!("A baby dog is called a {}", <::Dog as Animal>::baby_name());
   |                                           +++++++++       +

For more information about this error, try `rustc --explain E0790`.
error: could not compile `traits-example` due to previous error

Щоб розрізнити реалізації і сказати Rust, що ми хочемо використати реалізацію Animal для Dog, а не реалізацію Animal для якогось іншого типу, ми маємо використати повний кваліфікований синтаксис. Блок коду 19-21 демонструє, як використовувати повний кваліфікований синтаксис.

Файл: 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 as Animal>::baby_name());
}

Блок коду 19-21: використання повністю кваліфікованого синтаксису, щоб вказати, що ми хочемо викликати функцію baby_name з трейту Animal, реалізованого для Dog

Ми надаємо Rust анотацію типу в кутових дужках, що показує, що ми хочемо викликати метод baby_name з трейту Animal, як він реалізований для Dog, кажучи, що ми хочемо розглядати тип e Dog як Animal для цього виклику функції. Цей код тепер виведе те, що ми хотіли:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
    Finished dev [unoptimized + debuginfo] target(s) in 0.48s
     Running `target/debug/traits-example`
A baby dog is called a puppy

В цілому, повний кваліфікований синтаксис визначений таким чином:

<Type as Trait>::function(receiver_if_method, next_arg, ...);

Для асоційованих функцій, які не є методами, не використовується receiver: там буде лише список решти аргументів. Ви можете використовувати повний кваліфікований синтаксис всюди, де викликаєте функції або методи. Однак, ви можете пропустити будь-яку частину синтаксису, яку Rust може визначити за допомогою іншої інформації у програмі. Вам потрібно використовувати цей більш багатослівний синтаксис у випадках, коли є декілька реалізацій, які використовують одну назву і Rust потребує допомоги, щоб визначити, яку реалізацію ви хочете викликати.

Використання Супертрейтів для Вимоги Функціонала Одного Трейта в Іншому Трейті

Іноді ви можете написати визначення трейта, що залежить від іншого трейта: для типу, що реалізує перший трейт, ви хочете вимагати, щоб цей тип також реалізовував другий трейт. Вам може бути таке потрібно, якщо визначення вашого трейта використовує асоційовані елементи другого трейта. Трейт, на який покладається визначення вашого трейта, зветься супертрейтом вашого трейта.

Наприклад, припустимо, що ми хочемо зробити трейт OutlinePrint з методом outline_print, що виводить задане значення, форматоване рамкою з зірочок. Тобто, якщо структура Point реалізує трейт зі стандартної бібліотеки Display і виводить (x, y), то коли ми викликаємо outline_print на екземплярі Point, що має значення 1 для x і 3 для y, він має вивести таке:

**********
*        *
* (1, 3) *
*        *
**********

У реалізації методу outline_print ми хочемо використати функціональність трейту Display. Відповідно, нам потрібно вказати що трейт OutlinePrint буде працювати тільки для типів, які також реалізують Display і надають функціональність, потрібну OutlinePrint. Ми можемо зробити це у визначені трейта, вказавши OutlinePrint: Display. Ця техніка схожа на додавання до трейта трейтового обмеження. Блок коду 19-22 показує реалізацію трейту OutlinePrint.

Файл: 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));
    }
}

fn main() {}

Блок коду 19-22: реалізація трейту OutlinePrint, який вимагає функціональності Display

Оскільки ми вказали, що OutlinePrint потребує трейту Display, ми можемо використовувати функцію to_string, автоматично реалізовану для будь-якого типу, що реалізовує Display. Якби ми спробували використати to_string, не додавши двокрапки і трейту Display після назви трейту, ми б отримали помилку про те, що метод to_string не був знайдений для типу &Self у поточній області видимості.

Подивімося, що станеться, коли ми спробуємо реалізувати OutlinePrint для типу, що не реалізує Display, такому як структура Point:

Файл: 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));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

Ми отримуємо помилку, яка повідомляє, що Display є потрібним, але не реалізованим:

$ cargo run
   Compiling traits-example v0.1.0 (file:///projects/traits-example)
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`

For more information about this error, try `rustc --explain E0277`.
error: could not compile `traits-example` due to previous error

Щоб виправити це, ми реалізуємо Display для Point і задовольняємо обмеження для OutlinePrint ось таким чином:

Файл: src/main.rs

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));
    }
}

struct Point {
    x: i32,
    y: i32,
}

impl OutlinePrint for Point {}

use std::fmt;

impl fmt::Display for Point {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "({}, {})", self.x, self.y)
    }
}

fn main() {
    let p = Point { x: 1, y: 3 };
    p.outline_print();
}

To fix this, we implement Display on Point and satisfy the constraint that OutlinePrint requires, like so:

Використання Шаблону "Новий Тип" для Реалізації Зовнішніх Трейтів на Зовнішніх Типах

У Розділі 10, у підрозділі “Реалізація трейта для типу” , ми згадали правило сироти, яке каже, що ми можемо реалізовувати трейт для типу, якщо трейт або тип є локальним для нашого крейта. Це обмеження можна обійти за допомогою паттерна "новий тип", що передбачає створення нового типу у структурі-кортежі. (Про структури-кортежі ми говорили у підрозділі "Використання структур-кортежів без названих полів для створення нових типів" Розділу 5.) Структури-кортежі мають одне поле і є тонкою обгорткою для типу, для якого ми хочемо реалізувати трейт. Тоді тип-обгортка є локальним для нашого крейта, і ми можемо реалізувати трейт для обгортки.

Новий тип - це термін, який походить з мови програмування Haskell. Використання цього шаблону не призводить до втрат швидкодії, а тип обгортки приховується під час компіляції.

Наприклад, скажімо, ми хочемо реалізувати Display для Vec<T>, що безпосередньо заборонено правилом сироти, тому що трейт Display і тип Vec<T> визначається поза нашим крейтом. Ми можемо зробити структуру Wrapper, що містить екземпляр Vec<T>; тоді ми можемо реалізувати Display для Wrapper використати значення Vec<T>, як показано в Блоці коду 19-23.

Файл: 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);
}

Блок коду 19-23: створення типу Wrapper навколо Vec<String>для реалізації Display`

Реалізація Display використовує self.0 для доступу до внутрішнього Vec<T>, оскільки Wrapper - це структура-кортеж, а Vec<T> - це елемент з індексом 0 в кортежі. Тоді ми можемо використати функціонал типу Display на Wrapper.

Недоліком використання цієї техніки є те, що Wrapper є новим типом, тож він не має методів значення, яке він містить. Ми мали б реалізувати всі методи Vec<T> безпосередньо на Wrapper, делегуючи всі методи self.0, що дозволить нам використовувати Wrapper точно як і Vec<T>. Якби ми хотіли, щоб новий тип мав кожен метод, який має внутрішній тип, то реалізація трейту Deref (про який йдеться у Розділі 15 у підрозділі “Використання розумних вказівників як звичайних посилань за допомогою трейта Deref ) для Wrapper, щоб повертав внутрішній тип, могла б бути розв'язанням проблеми. Якщо ж ми не хочемо, щоб тип Wrapper мав усі методи внутрішнього типу - наприклад, для обмеження поведінки типу Wrapper - то нам треба реалізувати потрібні нам методи вручну.

Цей паттерн "новий тип" також корисний навіть без залучення трейтів. Змінімо фокус і погляньмо на деякі поглиблені способи взаємодії з системою типів Rust. ch10-02-traits.html#implementing-a-trait-on-a-type ch10-02-traits.html#traits-defining-shared-behavior