Продвинутые трейты в Rust
На странице Трейты ты научился объявлять трейт и реализовывать его. На страницах Обобщения и Популярные трейты ты пользовался трейтами вроде Display, From и Add. Для большей части повседневного кода этого хватает. Но когда читаешь стандартную библиотеку или популярный крейт, встречаются возможности трейтов, которые поначалу выглядят странно: type Item, Add<Rhs = Self>, trait Pretty: Display, impl<T: Display> ... for T.
На этой странице мы разберём их по одной. Никакой магии здесь нет. Каждая решает небольшую реальную задачу, и когда понимаешь задачу, синтаксис становится понятным. Не торопись: вполне нормально прочитать один раздел, попробовать код и вернуться позже.
Ассоциированные типы: один ответ на тип
Заголовок раздела «Ассоциированные типы: один ответ на тип»С ассоциированным типом ты уже встречался: у Iterator есть type Item. Ассоциированный тип (associated type) — это тип-заполнитель внутри трейта. Каждая реализация задаёт его один раз.
Вот небольшой трейт Container. Плейлист хранит String, а список очков — u32, поэтому каждый выбирает свой Item:
use std::fmt::Debug;
trait Container { type Item;
fn get(&self, index: usize) -> Option<&Self::Item>; fn size(&self) -> usize;
fn first(&self) -> Option<&Self::Item> { self.get(0) }}
struct Playlist { songs: Vec<String>,}
struct Scores { values: Vec<u32>,}
impl Container for Playlist { type Item = String;
fn get(&self, index: usize) -> Option<&String> { self.songs.get(index) } fn size(&self) -> usize { self.songs.len() }}
impl Container for Scores { type Item = u32;
fn get(&self, index: usize) -> Option<&u32> { self.values.get(index) } fn size(&self) -> usize { self.values.len() }}
// Работает с любым Container, чьи элементы можно вывести через {:?}fn describe<C: Container>(c: &C)where C::Item: Debug,{ println!("элементов: {}, первый: {:?}", c.size(), c.first());}
fn main() { let playlist = Playlist { songs: vec![String::from("Вступление"), String::from("Финал")] }; let scores = Scores { values: vec![90, 72, 85] };
describe(&playlist); describe(&scores);}Вывод:
элементов: 2, первый: Some("Вступление")элементов: 3, первый: Some(90)Внутри трейта Self::Item означает «тот тип, который выбрала эта реализация». C::Item в describe означает то же самое для типа C. (Строка where — просто более аккуратное место для ограничения; подробнее об этом в конце страницы.)
Почему бы не взять обобщённый параметр? Можно было бы написать trait Container<T>. Но тогда один тип мог бы реализовать трейт много раз — и Container<String>, и Container<u32>, — и каждому вызывающему пришлось бы уточнять, какой из них он имеет в виду. Для плейлиста это бессмысленно: он хранит песни, и точка.
Именно поэтому Iterator использует type Item. Итератор по Vec<i32> выдаёт i32 и ничего больше, поэтому for x in v.iter() никогда не требует аннотации типа. Простое правило:
- Один естественный ответ на тип → ассоциированный тип (
Iterator::Item,Add::Output). - Много допустимых ответов на тип → обобщённый параметр (
From<T>:Stringможно создать из&str, изchar, изBox<str>…).
Параметры типа по умолчанию и операторы
Заголовок раздела «Параметры типа по умолчанию и операторы»На странице Популярные трейты ты заставил + работать для Point, реализовав Add. Вот как Add на самом деле объявлен в стандартной библиотеке:
trait Add<Rhs = Self> { type Output; fn add(self, rhs: Rhs) -> Self::Output;}Здесь встречаются две возможности. Output — ассоциированный тип (что получается в результате +). А Rhs = Self — параметр типа по умолчанию (default type parameter): «правая часть того же типа, что и левая, если ты не скажешь иначе». Когда ты пишешь impl Add for Point, используется значение по умолчанию. Если нужно складывать с другим типом, пишешь impl Add<OtherType> for Point.
use std::ops::Add;
#[derive(Debug, Clone, Copy)]struct Meters(f64);
#[derive(Debug, Clone, Copy)]struct Centimeters(f64);
// Используем значение по умолчанию Rhs = Self, то есть Meters + Metersimpl Add for Meters { type Output = Meters; fn add(self, other: Meters) -> Meters { Meters(self.0 + other.0) }}
// Переопределяем значение по умолчанию: Meters + Centimetersimpl Add<Centimeters> for Meters { type Output = Meters; fn add(self, other: Centimeters) -> Meters { Meters(self.0 + other.0 / 100.0) }}
fn main() { let a = Meters(2.0); let b = Meters(3.5); let c = Centimeters(50.0);
println!("{:?}", a + b); println!("{:?}", a + c);}Вывод:
Meters(5.5)Meters(2.5)Заметь, что Add использует оба инструмента: обобщённый параметр для правой части (много допустимых ответов — можно прибавлять Meters или Centimeters) и ассоциированный тип для результата (один ответ на каждую пару). Параметры типа по умолчанию полезны в основном тогда, когда ты проектируешь трейт: частый случай остаётся коротким, а редкий не блокируется.
Супертрейты: «реализуй ещё и вот это»
Заголовок раздела «Супертрейты: «реализуй ещё и вот это»»Иногда трейт имеет смысл, только если тип уже умеет что-то ещё. Супертрейт (supertrait) говорит: «чтобы реализовать мой трейт, реализуй ещё и тот». Пишется он через двоеточие, как ограничение:
use std::fmt;
// Всё, что реализует Pretty, должно реализовать и Displaytrait Pretty: fmt::Display { fn pretty(&self) -> String { format!("*** {} ***", self) }}
struct Temperature { celsius: f64,}
impl fmt::Display for Temperature { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{:.1}°C", self.celsius) }}
impl Pretty for Temperature {}
fn main() { let t = Temperature { celsius: 21.456 }; println!("{}", t); println!("{}", t.pretty());}Вывод:
21.5°C*** 21.5°C ***Метод pretty по умолчанию может использовать {} для self именно потому, что супертрейт обещает: Display есть. Без : fmt::Display Rust отказался бы компилировать этот format!, ведь он не мог бы знать, что тип умеет выводиться. impl Pretty for Temperature {} пустой — он просто говорит: «да, используй метод по умолчанию».
Настоящие примеры из стандартной библиотеки: Copy: Clone (каждый тип с Copy обязан быть и Clone) и Eq: PartialEq.
Обобщённые реализации: один impl для многих типов
Заголовок раздела «Обобщённые реализации: один impl для многих типов»Обобщённая реализация (blanket implementation) реализует трейт сразу для всех типов, которые подходят под условие. Синтаксис — impl<T: SomeBound> MyTrait for T.
Строки в этом примере английские: он переводит текст в верхний регистр, а такие примеры на этом сайте показываем на латинице.
use std::fmt::Display;
trait Shout { fn shout(&self) -> String;}
// Каждый тип, реализующий Display, получает Shout даромimpl<T: Display> Shout for T { fn shout(&self) -> String { format!("{}!", self.to_string().to_uppercase()) }}
fn main() { println!("{}", "hello".shout()); println!("{}", 42.shout()); println!("{}", 'a'.shout()); println!("{}", String::from("rust is fun").shout());}Вывод:
HELLO!42!A!RUST IS FUN!Мы ни разу не написали impl Shout for i32 или impl Shout for char. Обобщённая реализация покрыла их все — включая типы с Display, которых ещё даже не существует.
Ты много раз пользовался обобщённой реализацией, сам того не зная. В стандартной библиотеке есть (примерно) impl<T: Display> ToString for T. Поэтому у любого типа с Display есть метод .to_string(). Тот же трюк даёт тебе Into даром, когда ты реализуешь From.
Одно ограничение: если обобщённая реализация уже есть, нельзя добавить вторую, более конкретную реализацию для типа, который она уже покрывает (скажем, особую impl Shout for i32). У Rust было бы два ответа, и он отказывается с ошибкой «conflicting implementations» (конфликтующие реализации).
Правило сироты и обход через newtype
Заголовок раздела «Правило сироты и обход через newtype»Вот правило, которое хоть раз удивляет каждого. Попробуй дать Vec<String> красивый Display:
use std::fmt;
impl fmt::Display for Vec<String> { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.join(", ")) }}error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate --> src/main.rs:3:1 |3 | impl fmt::Display for Vec<String> { | ^^^^^^^^^^^^^^^^^^^^^^----------- | | | `Vec` is not defined in the current crate | = note: define and implement a trait or new type insteadКомпилятор говорит, что для типов, объявленных вне текущего крейта, можно реализовывать только трейты, объявленные в текущем крейте. Это правило сироты (orphan rule): реализовать трейт для типа можно, только если трейт или тип объявлен в твоём крейте. Display принадлежит стандартной библиотеке, и Vec тоже, так что ни то, ни другое не твоё.
Зачем это правило? Представь, что два крейта, от которых ты зависишь, написали каждый свою impl Display for Vec<String>. Какую из них должен использовать Rust? Хорошего ответа нет, поэтому Rust делает такую ситуацию невозможной.
Обход — паттерн newtype («новый тип»): оберни чужой тип в крошечную собственную структуру. Теперь тип твой, и для него можно реализовать что угодно:
use std::fmt;
// Локальная обёртка вокруг чужого типаstruct Shopping(Vec<String>);
impl fmt::Display for Shopping { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "[{}]", self.0.join(", ")) }}
fn main() { let list = Shopping(vec![String::from("молоко"), String::from("яйца"), String::from("хлеб")]); println!("Тебе нужно: {}", list); println!("Всего покупок: {}", list.0.len());}Вывод:
Тебе нужно: [молоко, яйца, хлеб]Всего покупок: 3self.0 даёт доступ к Vec внутри. Newtype ничего не стоит во время работы: в памяти Shopping — это ровно Vec<String>. Минус в том, что методов Vec у Shopping напрямую нет, поэтому пишешь list.0.len() (или добавляешь нужные методы). Newtype полезен далеко не только для правила сироты; см. Паттерны проектирования.
Полный синтаксис: когда имена совпадают
Заголовок раздела «Полный синтаксис: когда имена совпадают»У двух трейтов могут быть методы с одинаковым именем, а у самого типа — ещё один такой же. Какой из них вызовет robot.greet()?
trait English { fn greet(&self) -> String; fn language() -> String;}
trait Spanish { fn greet(&self) -> String; fn language() -> String;}
struct Robot;
impl Robot { fn greet(&self) -> String { String::from("БИП-БУП") }}
impl English for Robot { fn greet(&self) -> String { String::from("Hello!") } fn language() -> String { String::from("English") }}
impl Spanish for Robot { fn greet(&self) -> String { String::from("¡Hola!") } fn language() -> String { String::from("Spanish") }}
fn main() { let r = Robot;
println!("{}", r.greet()); // побеждает собственный метод типа println!("{}", English::greet(&r)); // выбираем трейт по имени println!("{}", Spanish::greet(&r));
// Здесь нет `self`, поэтому Rust нужен и тип, И трейт: println!("{}", <Robot as English>::language()); println!("{}", <Robot as Spanish>::language());}Вывод:
БИП-БУПHello!¡Hola!EnglishSpanishТри уровня, от короткого к полностью явному:
r.greet()— первым выбирается собственный (inherent) метод типа.English::greet(&r)— «версия изEnglish». Тип Rust выясняет по&r.<Robot as English>::language()— полный синтаксис (fully qualified syntax). Он нужен для функций безself, потому что Rust не на что посмотреть, чтобы узнать тип. ПростоEnglish::language()не скомпилируется:error[E0790]: cannot call associated function on trait without specifying the corresponding impl type— нельзя вызвать ассоциированную функцию трейта, не указав тип реализации.
Нужно это нечасто. А когда понадобится, ошибка компилятора обычно подсказывает точное исправление.
impl Trait в аргументах и возвращаемых значениях, плюс where
Заголовок раздела «impl Trait в аргументах и возвращаемых значениях, плюс where»impl Trait можно писать в двух местах, и в каждом он означает немного разное:
- В аргументе —
fn f(x: impl Trait): тип выбирает вызывающий. Это короткая запись обобщённойfn f<T: Trait>(x: T). - В возвращаемом значении —
fn f() -> impl Trait: функция выбирает один конкретный тип и прячет его. Вызывающий знает только, что «это что-то, реализующееTrait».
// Выбирает вызывающий: подойдёт любой итератор по u32fn print_all(items: impl Iterator<Item = u32>) { let all: Vec<u32> = items.collect(); println!("{:?}", all);}
// Выбирает функция: настоящий тип — длинный Filter<...>, который мы не пишемfn evens_up_to(limit: u32) -> impl Iterator<Item = u32> { (0..=limit).filter(|n| n % 2 == 0)}
fn main() { print_all(vec![3, 1, 4].into_iter()); print_all(evens_up_to(10));}Вывод:
[3, 1, 4][0, 2, 4, 6, 8, 10]impl Trait в возвращаемом значении особенно удобен с замыканиями и итераторами. Настоящий тип результата evens_up_to включает тип замыкания, а у замыканий нет имени, которое можно было бы написать. impl Iterator позволяет его не писать.
Когда у функции несколько обобщённых параметров, и у каждого несколько ограничений, в угловых скобках становится тесно. Блок where переносит ограничения за сигнатуру, и форму функции читать легче:
use std::fmt::{Debug, Display};
// Тесно: fn report<T: Display, U: Debug + PartialOrd>(label: T, a: U, b: U) -> String// Аккуратнее:fn report<T, U>(label: T, a: U, b: U) -> Stringwhere T: Display, U: Debug + PartialOrd,{ let bigger = if a > b { a } else { b }; format!("{}: больше — {:?}", label, bigger)}
fn main() { println!("{}", report("очки", 7, 12)); println!("{}", report(String::from("имена"), "ann", "bob"));}Вывод:
очки: больше — 12имена: больше — "bob"Для компилятора обе формы означают одно и то же. Кроме того, where — естественное место для ограничений на ассоциированные типы, как C::Item: Debug в первом примере на этой странице.
Частые ошибки
Заголовок раздела «Частые ошибки»Реализовал трейт, но забыл его супертрейт. Если убрать блок impl fmt::Display for Temperature из примера с супертрейтом, получится:
error[E0277]: `Temperature` doesn't implement `std::fmt::Display` --> src/main.rs:13:17 |13 | impl Pretty for Temperature {} | ^^^^^^^^^^^ unsatisfied trait bound |note: required by a bound in `Pretty` --> src/main.rs:3:15 | 3 | trait Pretty: fmt::Display { | ^^^^^^^^^^^^ required by this bound in `Pretty`Компилятор сообщает, что Temperature не реализует Display, хотя этого требует Pretty. Исправление — реализовать и супертрейт (здесь Display). Примечание note указывает прямо на : fmt::Display, которое его требует.
Два разных типа из -> impl Trait. impl Trait прячет один тип. Он не может быть «то одним, то другим»:
fn numbers(evens: bool) -> impl Iterator<Item = u32> { if evens { (0..10).filter(|n| n % 2 == 0) } else { vec![1, 3, 5].into_iter() }}error[E0308]: `if` and `else` have incompatible types --> src/main.rs:5:9 |2 | / if evens {3 | | (0..10).filter(|n| n % 2 == 0) | | ------------------------------ | | | | | | | the expected closure | | expected because of this4 | | } else {5 | | vec![1, 3, 5].into_iter() | | ^^^^^^^^^^^^^^^^^^^^^^^^^ expected `Filter<Range<{integer}>, {closure@...}>`, found `IntoIter<{integer}>`6 | | } | |_____- `if` and `else` have incompatible types | = note: expected struct `Filter<std::ops::Range<{integer}>, {closure@src/main.rs:3:24: 3:27}>` found struct `std::vec::IntoIter<{integer}>`help: you could change the return type to be a boxed trait object |1 - fn numbers(evens: bool) -> impl Iterator<Item = u32> {1 + fn numbers(evens: bool) -> Box<dyn Iterator<Item = u32>> { |help: if you change the return type to expect trait objects, box the returned expressions |3 ~ Box::new((0..10).filter(|n| n % 2 == 0))4 | } else {5 ~ Box::new(vec![1, 3, 5].into_iter()) |Компилятор говорит, что у веток if и else несовместимые типы, и предлагает вернуть трейт-объект в Box. Когда тип действительно меняется во время работы, так и сделай — возвращай Box<dyn Trait>. См. Трейт-объекты.
- Ассоциированный тип (
type Item) даёт один ответ на реализацию. Бери обобщённый параметр, когда тип может реализовать трейт несколькими способами. - Параметры типа по умолчанию вроде
Add<Rhs = Self>делают частый случай коротким;impl Add<Other> for Tих переопределяет. - Супертрейт (
trait Pretty: Display) требует другой трейт и позволяет методам по умолчанию им пользоваться. - Обобщённая реализация (
impl<T: Display> MyTrait for T) сразу покрывает все подходящие типы — так работает.to_string(). - Правило сироты не даёт реализовать чужой трейт для чужого типа. Чтобы его обойти, оберни тип в newtype.
- Используй полный синтаксис
<Type as Trait>::function(), когда имена совпадают;impl Traitв возвращаемом значении прячет один конкретный тип;whereделает длинные ограничения читаемыми.
Проверь себя
Заголовок раздела «Проверь себя»Проверь себя
1. Почему Iterator использует type Item, а не Iterator<T>?
2. Что означает Rhs = Self в trait Add<Rhs = Self>?
3. Тебе нужен impl Display for Vec<String>, но Rust отказывает. Как это обычно исправляют?
4. Какая строка вызывает версию из English для функции без параметра self?
5. Кто выбирает конкретный тип итератора в fn evens() -> impl Iterator<Item = u32>?
Результат: 0 / 5
Что почитать
Заголовок раздела «Что почитать»- Продвинутые типажи — книга «Язык программирования Rust» (перевод на русский): ассоциированные типы, параметры типа по умолчанию, супертрейты и паттерн newtype, с дополнительными примерами.
- Traits — The Rust Reference (на английском): точные правила для элементов трейтов, супертрейтов и ассоциированных элементов.
- Orphan rules — The Rust Reference (на английском): точное определение того, когда реализация разрешена.
impl Trait— The Rust Reference (на английском): чемimpl Traitв аргументе отличается отimpl Traitв возвращаемом значении.- Супертрейты — Rust на примерах (перевод на русский): короткий запускаемый пример трейтов, построенных на других трейтах.