1. 1. Introduction
  2. 2. Введение
  3. 3. C чего начать
  4. 4. Изучение Rust
    1. 4.1. Угадайка
    2. 4.2. Обедающие философы
    3. 4.3. Вызов кода на Rust из других языков
  5. 5. Синтаксис и семантика
    1. 5.1. Связывание имён
    2. 5.2. Функции
    3. 5.3. Простые типы
    4. 5.4. Комментарии
    5. 5.5. Конструкция `if`
    6. 5.6. Циклы
    7. 5.7. Владение
    8. 5.8. Ссылки и заимствование
    9. 5.9. Время жизни
    10. 5.10. Изменяемость
    11. 5.11. Структуры
    12. 5.12. Перечисления
    13. 5.13. Конструкция `match`
    14. 5.14. Шаблоны сопоставления `match`
    15. 5.15. Синтаксис методов
    16. 5.16. Вектора
    17. 5.17. Строки
    18. 5.18. Обобщённое программирование
    19. 5.19. Типажи
    20. 5.20. Типаж `Drop`
    21. 5.21. Конструкция `if let`
    22. 5.22. Типажи-объекты
    23. 5.23. Замыкания
    24. 5.24. Универсальный синтаксис вызова функций
    25. 5.25. Контейнеры и модули
    26. 5.26. `const` и `static`
    27. 5.27. Атрибуты
    28. 5.28. Псевдонимы типов
    29. 5.29. Приведение типов
    30. 5.30. Ассоциированные типы
    31. 5.31. Безразмерные типы
    32. 5.32. Перегрузка операций
    33. 5.33. Преобразования при разыменовании
    34. 5.34. Макросы
    35. 5.35. Сырые указатели
    36. 5.36. Небезопасный код
  6. 6. Эффективное использование Rust
    1. 6.1. Стек и куча
    2. 6.2. Тестирование
    3. 6.3. Условная компиляция
    4. 6.4. Документация
    5. 6.5. Итераторы
    6. 6.6. Многозадачность
    7. 6.7. Обработка ошибок
    8. 6.8. Выбор гарантий
    9. 6.9. Интерфейс внешних функций
    10. 6.10. Типажи `Borrow` и `AsRef`
    11. 6.11. Каналы сборок
    12. 6.12. Using Rust without the standard library
  7. 7. Нестабильные возможности Rust
    1. 7.1. Плагины к компилятору
    2. 7.2. Встроенный ассемблерный код
    3. 7.3. Без stdlib
    4. 7.4. Внутренние средства
    5. 7.5. Элементы языка
    6. 7.6. Продвинутое руководство по компоновке
    7. 7.7. Тесты производительности
    8. 7.8. Синтаксис упаковки и шаблоны `match`
    9. 7.9. Шаблоны `match` для срезов
    10. 7.10. Ассоциированные константы
    11. 7.11. Пользовательские менеджеры памяти
  8. 8. Глоссарий
  9. 9. Syntax Index
  10. 10. Библиография

Типажи

Типаж --- это возможность объяснить компилятору, что данный тип должен предоставлять определённую функциональность.

Вы помните ключевое слово impl, используемое для вызова функции через синтаксис метода?

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}Run

Типажи схожи, за исключением того, что мы определяем типаж, содержащий лишь сигнатуру метода, а затем реализуем этот типаж для нужной структуры. Например, как показано ниже:

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}Run

Как вы можете видеть, блок trait очень похож на блок impl. Различие состоит лишь в том, что тело метода не определяется, а определяется только его сигнатура. Когда мы реализуем типаж, мы используем impl Trait for Item, а не просто impl Item.

Мы можем использовать типажи для ограничения обобщённых типов. Рассмотрим похожую функцию, которая также не компилируется, и выводит ошибку:

fn print_area<T>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}Run

Rust выводит:

error: type `T` does not implement any method in scope named `area`

Поскольку T может быть любого типа, мы не можем быть уверены, что он реализует метод area. Но мы можем добавить «ограничение по типажу» к нашему обобщённому типу T, гарантируя, что он будет соответствовать требованиям:

fn print_area<T: HasArea>(shape: T) {
    println!("This shape has an area of {}", shape.area());
}Run

Синтаксис <T: HasArea> означает «любой тип, реализующий типаж HasArea». Так как типажи определяют сигнатуры типов функций, мы можем быть уверены, что любой тип, который реализует HasArea, будет иметь метод .area().

Вот расширенный пример того, как это работает:

trait HasArea {
    fn area(&self) -> f64;
}

struct Circle {
    x: f64,
    y: f64,
    radius: f64,
}

impl HasArea for Circle {
    fn area(&self) -> f64 {
        std::f64::consts::PI * (self.radius * self.radius)
    }
}

struct Square {
    x: f64,
    y: f64,
    side: f64,
}

impl HasArea for Square {
    fn area(&self) -> f64 {
        self.side * self.side
    }
}

fn print_area<T: HasArea>(shape: T) {
    println!("Площадь этой фигуры равна {}", shape.area());
}

fn main() {
    let c = Circle {
        x: 0.0f64,
        y: 0.0f64,
        radius: 1.0f64,
    };

    let s = Square {
        x: 0.0f64,
        y: 0.0f64,
        side: 1.0f64,
    };

    print_area(c);
    print_area(s);
}Run

Ниже показан вывод программы:

Площадь этой фигуры равна 3.141593
Площадь этой фигуры равна 1

Как вы можете видеть, теперь print_area не только является обобщённой функцией, но и гарантирует, что будет получен корректный тип. Если же мы передадим некорректный тип:

print_area(5);Run

Мы получим ошибку времени компиляции:

error: the trait `HasArea` is not implemented for the type `_` [E0277]

До сих пор мы добавляли реализации типажей лишь для структур, но реализовать типаж можно для любого типа. Технически, мы могли бы реализовать HasArea для i32:

trait HasArea {
    fn area(&self) -> f64;
}

impl HasArea for i32 {
    fn area(&self) -> f64 {
        println!("это нелепо");

        *self as f64
    }
}

5.area();Run

Хотя технически это возможно, реализация методов для примитивных типов считается плохим стилем программирования.

Может показаться, что такой подход легко приводит к бардаку в коде, однако есть два ограничения, связанные с реализацией типажей, которые мешают коду выйти из-под контроля. Во-первых, если типаж не определён в нашей области видимости, он не применяется. Например, стандартная библиотека предоставляет типаж Write, который добавляет типу File функциональность ввода-вывода. По умолчанию у File не будет этих методов:

let mut f = std::fs::File::open("foo.txt").ok().expect("Не могу открыть foo.txt");
let buf = b"whatever"; // литерал строки байт. buf: &[u8; 8]
let result = f.write(buf);Run

Вот ошибка:

error: type `std::fs::File` does not implement any method in scope named `write`
let result = f.write(buf);
               ^~~~~~~~~~

Сначала мы должны сделать use для типажа Write:

use std::io::Write;

let mut f = std::fs::File::open("foo.txt").ok().expect("Не могу открыть foo.txt");
let buf = b"whatever";
let result = f.write(buf);Run

Это скомпилируется без ошибки.

Благодаря такой логике работы, даже если кто-то сделает что-то страшное — например, добавит методы i32, это не коснётся вас, пока вы не импортируете типаж.

Второе ограничение реализации типажей --- это то, что или типаж, или тип, для которого вы реализуете типаж, должен быть реализован вами. Мы могли бы определить HasArea для i32, потому что HasArea — это наш код. Но если бы мы попробовали реализовать для i32 ToString — типаж, предоставляемый Rust — мы бы не смогли сделать это, потому что ни типаж, ни тип не реализован нами.

Последнее, что нужно сказать о типажах: обобщённые функции с ограничением по типажам используют мономорфизацию (mono: один, morph: форма), поэтому они диспетчеризуются статически. Что это значит? Посмотрите главу Типажи-объекты, чтобы получить больше информации.

Множественные ограничения по типажам

Вы уже видели, как можно ограничить обобщённый параметр типа определённым типажом:

fn foo<T: Clone>(x: T) {
    x.clone();
}Run

Если вам нужно больше одного ограничения, вы можете использовать +:

use std::fmt::Debug;

fn foo<T: Clone + Debug>(x: T) {
    x.clone();
    println!("{:?}", x);
}Run

Теперь тип T должен реализовавать как типаж Clone, так и типаж Debug.

Утверждение where

Написание функций с несколькими обобщёнными типами и небольшим количеством ограничений по типажам выглядит не так уж плохо, но, с увеличением количества зависимостей, синтаксис получается более неуклюжим:

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}Run

Имя функции находится слева, а список параметров — далеко справа. Ограничения загромождают место.

Есть решение и для этой проблемы, и оно называется «утверждение where»:

use std::fmt::Debug;

fn foo<T: Clone, K: Clone + Debug>(x: T, y: K) {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn bar<T, K>(x: T, y: K) where T: Clone, K: Clone + Debug {
    x.clone();
    y.clone();
    println!("{:?}", y);
}

fn main() {
    foo("Привет", "мир");
    bar("Привет", "мир");
}Run

foo() использует синтаксис, показанный ранее, а bar() использует утверждение where. Все, что нам нужно сделать, это убрать ограничения при определении типов параметров, а затем добавить where после списка параметров. В более длинных списках можно использовать пробелы:

use std::fmt::Debug;

fn bar<T, K>(x: T, y: K)
    where T: Clone,
          K: Clone + Debug {

    x.clone();
    y.clone();
    println!("{:?}", y);
}Run

Такая гибкость может добавить ясности в сложных ситуациях.

На самом деле where это больше, чем просто упрощение синтаксиса. Например:

trait ConvertTo<Output> {
    fn convert(&self) -> Output;
}

impl ConvertTo<i64> for i32 {
    fn convert(&self) -> i64 { *self as i64 }
}

// может быть вызван с T == i32
fn normal<T: ConvertTo<i64>>(x: &T) -> i64 {
    x.convert()
}

// может быть вызван с T == i64
fn inverse<T>() -> T
        // использует ConvertTo как если бы это было «ConvertTo<i64>»
        where i32: ConvertTo<T> {
    1i32.convert()
}Run

Этот код демонстрирует дополнительные преимущества использования утверждения where: оно позволяет задавать ограничение, где с левой стороны располагается произвольный тип (в данном случае i32), а не только простой параметр типа (вроде T).

Методы по умолчанию

Есть еще одна особенность типажей, о которой стоит поговорить: методы по умолчанию. Проще всего показать это на примере:

trait Foo {
    fn is_valid(&self) -> bool;

    fn is_invalid(&self) -> bool { !self.is_valid() }
}Run

В типах, реализующих типаж Foo, нужно реализовать метод is_valid(), а is_invalid() будет реализован по-умолчанию. Его поведение можно переопределить:

struct UseDefault;

impl Foo for UseDefault {
    fn is_valid(&self) -> bool {
        println!("Вызван UseDefault.is_valid.");
        true
    }
}

struct OverrideDefault;

impl Foo for OverrideDefault {
    fn is_valid(&self) -> bool {
        println!("Вызван OverrideDefault.is_valid.");
        true
    }

    fn is_invalid(&self) -> bool {
        println!("Вызван OverrideDefault.is_invalid!");
        true // эта реализация противоречит сама себе!
    }
}

let default = UseDefault;
assert!(!default.is_invalid()); // печатает «Вызван UseDefault.is_valid.»

let over = OverrideDefault;
assert!(over.is_invalid()); // печатает «Вызван OverrideDefault.is_invalid!»Run

Наследование

Иногда чтобы реализовать один типаж, нужно реализовать типажи, от которых он зависит:

trait Foo {
    fn foo(&self);
}

trait FooBar : Foo {
    fn foobar(&self);
}Run

Типы, реализующие FooBar, должны реализовывать Foo:

struct Baz;

impl Foo for Baz {
    fn foo(&self) { println!("foo"); }
}

impl FooBar for Baz {
    fn foobar(&self) { println!("foobar"); }
}Run

Если мы забудем реализовать Foo, компилятор скажет нам об этом:

error: the trait `main::Foo` is not implemented for the type `main::Baz` [E0277]