Перейти к содержанию
Educora
Средний20 мин8 / 10

Трейты, обобщения и времена жизни

Описывай общее поведение трейтами, пиши обобщённые функции с ограничениями трейтов, используй `impl Trait` и `#[derive]` и разберись в основах аннотаций времени жизни.

Проверь себя
В этом уроке ты узнаешь
  • Объявлять трейт и реализовывать его для разных типов
  • Писать обобщённые функции с ограничениями трейтов, impl Trait и where
  • Получать Debug, Clone и PartialEq через #[derive]
  • Объяснять ошибки E0106 и E0597 через времена жизни

Телефон, ноутбук и наушники — совсем разные устройства, но все заряжаются одним кабелем USB-C. Кабель ничего не знает об их устройстве — ему важно лишь умение «заряжаться». В Rust такая общая способность называется трейтом (trait). Вместе с обобщёнными типами трейты позволяют один раз написанному коду работать с множеством разных типов.

Трейты

Трейт — список методов, которые должны быть у типа. Блок impl ИмяТрейта for Тип пишет эти методы для конкретного типа. Трейт может задать и тело по умолчанию: тип может оставить его как есть или заменить своим вариантом. Display, Debug, Clone — всё это трейты стандартной библиотеки.

Rust
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 — любой тип, который можно сравнивать и копировать». Компилятор создаёт отдельную копию функции для каждого конкретного типа, поэтому обобщённый код не медленнее во время работы.

Rust
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
Ошибка E0369
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 можно указать и как возвращаемый тип — например, чтобы не выписывать длинный тип итератора.

Rust
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(...)] заставляет компилятор сгенерировать их автоматически. Для этого все поля структуры тоже должны реализовывать эти трейты.

Rust
#[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 описывает связь: «результат живёт не дольше обоих входов». Аннотация не меняет срок жизни значений, а лишь объясняет его компилятору.

Ошибка E0106
fn longest(a: &str, b: &str) -> &str {
    if a.len() >= b.len() {
        a
    } else {
        b
    }
}
Правильно с `'a`
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. Сигнатуру справа компилятор предлагает сам.
Rust
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 удаляется в конце блока:

Text
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.

1 / 10
Что такое трейт в Rust?