Перейти к содержимому

Продвинутые трейты в 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 + Meters
impl Add for Meters {
type Output = Meters;
fn add(self, other: Meters) -> Meters {
Meters(self.0 + other.0)
}
}
// Переопределяем значение по умолчанию: Meters + Centimeters
impl 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, должно реализовать и Display
trait 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» (конфликтующие реализации).

Вот правило, которое хоть раз удивляет каждого. Попробуй дать 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());
}

Вывод:

Тебе нужно: [молоко, яйца, хлеб]
Всего покупок: 3
▶ Попробуй сам

self.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!
English
Spanish
▶ Попробуй сам

Три уровня, от короткого к полностью явному:

  • 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».
// Выбирает вызывающий: подойдёт любой итератор по u32
fn 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) -> String
where
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 this
4 | | } 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