Поглиблено про Трейти
Ми вже розповідали про трейти у підрозділі “Трейти: визначення загальної поведінки” Розділу 10, але ми не говорили про глибші деталі. Тепер, коли ви більше знаєте про Rust, ми можемо перейти до дрібніших деталей.
Зазначення Заповнювача Типу у Визначенні Трейтів за Допомогою Асоційованих Типів
Асоційовані типи зв'язують заповнювач типу з трейтом таким чином, що методи трейту можуть використовувати ці заповнювачі типу у своїх сигнатурах. Той, хто реалізовуватиме трейт, зазначить конкретний тип, що буде використовуватися, замість заповнювача типу для конкретної реалізації. Таким чином ми можемо визначити трейт, що використовує деякі типи, без потреби точно знати ці типи до моменту реалізації трейту.
Ми описували більшість поглиблених особливостей у цьому розділі як такі, що рідко потрібні. Пов'язані типи десь посередині: вони використовуються рідше за функціонал, описаний в решті книги, але частіше ніж багато іншого функціоналу, обговорюваного в цьому розділі.
Одним з прикладів трейту з асоційованим типом є трейт Iterator
, наданий стандартною бібліотекою. Асоційований тип називається Item
і позначає тип значень, по яких ітерує тип, що реалізує трейт Iterator
. Визначення трейту Iterator
показано у Блоці коду 19-12.
pub trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::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<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 } ); }
Метод 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))
}
}
Щоб додати 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() {}
Коли ми викликаємо 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(); }
Запуск цього коду виведе *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(); }
Зазначення назви трейту перед назвою методу прояснює для 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()); }
Ми реалізували код для називання всіх цуценят 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());
}
Оскільки 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()); }
Ми надаємо 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() {}
Оскільки ми вказали, що 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); }
Реалізація 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