- Объявлять трейт и реализовывать его для разных типов
- Писать обобщённые функции с ограничениями трейтов,
impl Traitиwhere - Получать
Debug,CloneиPartialEqчерез#[derive] - Объяснять ошибки E0106 и E0597 через времена жизни
Телефон, ноутбук и наушники — совсем разные устройства, но все заряжаются одним кабелем USB-C. Кабель ничего не знает об их устройстве — ему важно лишь умение «заряжаться». В Rust такая общая способность называется трейтом (trait). Вместе с обобщёнными типами трейты позволяют один раз написанному коду работать с множеством разных типов.
Трейты
Трейт — список методов, которые должны быть у типа. Блок impl ИмяТрейта for Тип пишет эти методы для конкретного типа. Трейт может задать и тело по умолчанию: тип может оставить его как есть или заменить своим вариантом. Display, Debug, Clone — всё это трейты стандартной библиотеки.
trait Describe {
fn name(&self) -> String;
fn describe(&self) -> String {
format!("This is {}", self.name())
}
}
struct Cat;
struct Car {
model: String,
}
impl Describe for Cat {
fn name(&self) -> String {
String::from("a cat")
}
}
impl Describe for Car {
fn name(&self) -> String {
format!("a car ({})", self.model)
}
fn describe(&self) -> String {
format!("Beep! {}", self.name())
}
}
fn main() {
let car = Car { model: String::from("sedan") };
println!("{}", Cat.describe());
println!("{}", car.describe());
}This is a cat Beep! a car (sedan)
Cat оставил метод describe по умолчанию, а Car заменил его своим вариантом. struct Cat; — структура без полей.Обобщения и ограничения трейтов
Обобщённая функция принимает вместо конкретного типа параметр типа: fn largest<T>(...). Но если о T ничего не известно, с ним ничего нельзя сделать — даже сравнить. Ограничение трейта (T: PartialOrd + Copy) говорит: «T — любой тип, который можно сравнивать и копировать». Компилятор создаёт отдельную копию функции для каждого конкретного типа, поэтому обобщённый код не медленнее во время работы.
fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut max = items[0];
for &item in items {
if item > max {
max = item;
}
}
max
}
fn main() {
println!("{}", largest(&[3, 9, 2]));
println!("{}", largest(&[1.5, 0.5]));
println!("{}", largest(&['a', 'z', 'm']));
}9 1.5 z
fn largest<T>(items: &[T]) -> T {
let mut max = items[0];
for &item in items {
if item > max {
max = item;
}
}
max
}fn largest<T: PartialOrd + Copy>(items: &[T]) -> T {
let mut max = items[0];
for &item in items {
if item > max {
max = item;
}
}
max
}binary operation > cannot be applied to type T и предлагает добавить T: PartialOrd. А Copy нужен, чтобы брать элемент из списка копированием.Для краткости в параметре можно писать impl Trait: item: &impl Display означает «любой тип, реализующий Display». Когда ограничений много, их удобнее вынести после сигнатуры в блок where. impl Trait можно указать и как возвращаемый тип — например, чтобы не выписывать длинный тип итератора.
use std::fmt::Display;
fn announce(item: &impl Display) {
println!("[{item}]");
}
fn show_pair<T, U>(a: T, b: U)
where
T: Display,
U: Display,
{
println!("{a} & {b}");
}
fn evens(limit: u32) -> impl Iterator<Item = u32> {
(1..=limit).filter(|n| n % 2 == 0)
}
fn main() {
announce(&42);
announce(&"hello");
show_pair("Rust", 2015);
let v: Vec<u32> = evens(10).collect();
println!("{:?}", v);
}[42] [hello] Rust & 2015 [2, 4, 6, 8, 10]
Готовые трейты через #[derive]
Многие стандартные трейты не нужно писать вручную: атрибут #[derive(...)] заставляет компилятор сгенерировать их автоматически. Для этого все поля структуры тоже должны реализовывать эти трейты.
#[derive(Debug, Clone, PartialEq)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let a = Point { x: 1, y: 2 };
let mut b = a.clone();
println!("{:?} == {:?}: {}", a, b, a == b);
b.x = 5;
println!("{:?} == {:?}: {}", a, b, a == b);
}Point { x: 1, y: 2 } == Point { x: 1, y: 2 }: true
Point { x: 1, y: 2 } == Point { x: 5, y: 2 }: false| Трейт | Что даёт |
|---|---|
Debug | печать через {:?} и {:#?} |
Clone | полная копия через .clone() |
Copy | автоматическое копирование вместо перемещения (только простые поля) |
PartialEq, Eq | сравнение через == и != |
PartialOrd, Ord | <, > и сортировка |
Hash | использование как ключа HashMap |
Default | значение по умолчанию через Type::default() |
Основы времён жизни
У каждой ссылки есть время жизни (lifetime) — часть кода, где ссылка действительна. Чаще всего компилятор выводит его сам. Но когда функция принимает две ссылки и возвращает одну из них, компилятор не знает, у какой из них заимствует результат. Аннотация 'a описывает связь: «результат живёт не дольше обоих входов». Аннотация не меняет срок жизни значений, а лишь объясняет его компилятору.
fn longest(a: &str, b: &str) -> &str {
if a.len() >= b.len() {
a
} else {
b
}
}fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() {
a
} else {
b
}
}missing lifetime specifier — this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from a or b. Сигнатуру справа компилятор предлагает сам.fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() {
a
} else {
b
}
}
fn main() {
let city = String::from("Lankaran");
let result;
{
let other = String::from("Baku");
result = longest(&city, &other);
println!("longest: {result}");
}
println!("{}", longest("Quba", "Shaki"));
}longest: Lankaran Shaki
Если перенести строку println!("longest: {result}") за пределы внутреннего блока, код перестанет компилироваться. Здесь результат на самом деле city, но по сигнатуре он мог заимствовать и у other, а other удаляется в конце блока:
error[E0597]: `other` does not live long enough
--> src/main.rs:14:33
|
13 | let other = String::from("Baku");
| ----- binding `other` declared here
14 | result = longest(&city, &other);
| ^^^^^^ borrowed value does not live long enough
15 | }
| - `other` dropped here while still borrowed
16 | println!("longest: {result}");
| -------- borrow later used hereГлавное
- Трейт описывает общее поведение;
impl Трейт for Типреализует его для конкретного типа, а методы по умолчанию можно переопределить. - Обобщённая функция пишется с
<T: Ограничение>; ограничение говорит, что можно делать сT. - Компилятор создаёт отдельный код для каждого типа, поэтому обобщённый код не теряет в скорости.
#[derive(Debug, Clone, PartialEq)]автоматически создаёт часто нужные трейты.- Аннотация времени жизни
'aописывает связь между ссылками, но не меняет срок жизни значений.
Проверь себя
Вопросов: 10. Каждый правильный ответ приносит XP.