Типажи: определение общего поведения

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

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

Определение типажа

Поведение типа определяется теми методами, которые мы можем вызвать у данного типа. Различные типы разделяют одинаковое поведение, если мы можем вызвать одни и те же методы у этих типов. Определение типажей - это способ сгруппировать сигнатуры методов вместе для того, чтобы описать общее поведение, необходимое для достижения определённой цели.

Например, скажем есть несколько структур, которые имеют различный тип и различное количество текста: структура NewsArticle, которая содержит новости, напечатанные в различных местах в мире; структура Tweet, которая содержит 280 символьную строку твита и мета-данные, обозначающие является ли твит новым или ответом на другой твит.

Мы хотим создать библиотеку медиа-агрегатора, которая может отображать сводку данных сохранённых в экземплярах структур NewsArticle или Tweet. Чтобы этого достичь, нам необходимо иметь возможность для каждой структуры сделать короткую сводку на основе имеющихся данных: надо, чтобы обе структуры реализовали общее поведение. Мы можем делать такую сводку вызовом метода summarize у экземпляра объекта. Пример листинга 10-12 иллюстрирует определение типажа Summary, который выражает данное поведение:

Файл: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

Листинг 10-12: Определение типажа Summary, который содержит поведение предоставленное методом summarize

Здесь мы объявляем типаж с использованием ключевого слова trait, а затем его название, которым является Summary в данном случае. Внутри фигурных скобок объявляются сигнатуры методов, которые описывают поведения типов, реализующих данный типаж, в данном случае поведение определяется только одной сигнатурой метода: fn summarize(&self) -> String.

После сигнатуры метода, вместо предоставления реализации в фигурных в скобках, мы используем точку с запятой. Каждый тип, реализующий данный типаж, должен предоставить своё собственное поведение для данного метода. Компилятор обеспечит, что любой тип содержащий типаж Summary, будет также иметь и метод summarize объявленный с точно такой же сигнатурой.

Типаж может иметь несколько методов в описании его тела: сигнатуры методов перечисляются по одной на каждой строке и должны закачиваться символом ;.

Реализация типажа у типа

Теперь, после того как мы определили желаемое поведение используя типаж Summary, можно реализовать его у типов в нашем медиа-агрегаторе. Листинг 10-13 показывает реализацию типажа Summary у структуры NewsArticle, которая использует для создания сводки в методе summarize заголовок, автора и место публикации статьи. Для структуры Tweet мы определяем реализацию summarize используя пользователя и полный текст твита, полагая содержание твита уже ограниченным 280 символами.

Файл: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Код программы 10-13: Реализация типажа Summary для структур NewsArticle и Tweet

Реализация типажа у типа аналогична реализации обычных методов. Разница в том, что после impl мы ставим имя типажа, который мы хотим реализовать, затем используем ключевое слово for, а затем указываем имя типа, для которого мы хотим сделать реализацию типажа. Внутри блока impl мы помещаем сигнатуру метода объявленную в типаже. Вместо добавления точки с запятой в конце, после каждой сигнатуры используются фигурные скобки и тело метода заполняется конкретным поведением, которое мы хотим получить у методов типажа для конкретного типа.

После того, как мы реализовали типаж, можно вызвать его методы у экземпляров NewsArticle и Tweet тем же способом, что и вызов обычных методов, например так:

use chapter10::{self, Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

Данный код напечатает: 1 new tweet: horse_ebooks: of course, as you probably already know, people.

Обратите внимание, что поскольку мы определили типаж Summary и типы NewsArticle и Tweet в одном и том же файле lib.rs примера 10-13, все они находятся в одной области видимости. Допустим, что lib.rs предназначен для крейта, который мы назвали aggregator и кто-то ещё хочет использовать функциональность нашего крейта для реализации типажа Summary у структуры, определённой в области видимости внутри их библиотеки. Им нужно будет сначала подключить типаж в их область видимости. Они сделали бы это, указав use aggregator::Summary;, что позволит реализовать Summary для их типа. Типажу Summary также необходимо быть публичным для реализации в других крейтах, потому мы поставили ключевое слово pub перед trait в листинге 10-12.

Одно ограничение, на которое следует обратить внимание при реализации типажей это то, что мы можем реализовать типаж для типа, только если либо типаж, либо тип являются локальным для нашего крейта. Например, можно реализовать типажи из стандартной библиотеки, такие как Display для пользовательского типа Tweet являющимся частью функциональности крейта aggregator, потому что тип Tweet является локальным в крейте aggregator. Мы также можем реализовать типаж Summary для Vec<T> в нашем крейте aggregator, потому что типаж Summary является локальным для крейта aggregator.

Но мы не можем реализовать внешние типажи для внешних типов. Например, мы не можем реализовать функцию Display для Vec<T> в нашем крейте aggregator, потому что и типаж Display и тип Vec<T> определены в стандартной библиотеке, а не локально в нашем крейте aggregator. Это ограничение является частью свойства программы называемое согласованность, а точнее сиротское правило (orphan rule), называемое так, потому что родительский тип не представлен. Это правило гарантирует, что код других людей не может сломать ваш код и наоборот. Без этого правила два крейта могли бы реализовать один типаж для одинакового типа и Rust не будет знать, какой реализацией пользоваться.

Реализация поведения по умолчанию

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

В примере 10-14 показано, как указать строку по умолчанию для метода summarize из типажа Summary вместо определения только сигнатуры метода, как мы сделали в примере 10-12.

Файл: src/lib.rs

pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

Листинг 10-14. Определение типажа Summary с реализацией метода summarize по умолчанию

Для использования реализации по умолчанию при создании сводки у экземпляров NewsArticle вместо определения пользовательской реализации, мы указываем пустой блок impl с impl Summary for NewsArticle {}.

Хотя мы больше не определяем метод summarize непосредственно в NewsArticle, мы предоставили реализацию по умолчанию и указали, что NewsArticle реализует типаж Summary. В результате мы всё ещё можем вызвать метод summarize у экземпляра NewsArticle, например так:

use chapter10::{self, NewsArticle, Summary};

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
}

Этот код печатает New article available! (Read more...) .

Создание реализации по умолчанию для метода summarize не требует от нас изменений чего-либо в реализации Summary для типа Tweet в листинге 10-13. Причина заключается в том, что синтаксис для переопределения реализации по умолчанию является таким же, как синтаксис для реализации метода типажа, который не имеет реализации по умолчанию.

Реализации по умолчанию могут вызывать другие методы в том же типаже, даже если эти другие методы не имеют реализации по умолчанию. Таким образом, типаж может предоставить много полезной функциональности и только требует от разработчиков указывать небольшую его часть. Например, мы могли бы определить типаж Summary имеющий метод summarize_author, реализация которого требуется, а затем определить метод summarize который имеет реализацию по умолчанию, которая внутри вызывает метод summarize_author :

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

Чтобы использовать такую версию типажа Summary, нужно только определить метод summarize_author, при реализации типажа для типа:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

После того, как мы определим summarize_author, можно вызвать summarize для экземпляров структуры Tweet и реализация по умолчанию метода summarize будет вызывать определение summarize_author которое мы уже предоставили. Так как мы реализовали метод summarize_author типажа Summary, то типаж даёт нам поведение метода summarize без необходимости писать код.

use chapter10::{self, Summary, Tweet};

fn main() {
    let tweet = Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    };

    println!("1 new tweet: {}", tweet.summarize());
}

Этот код печатает 1 new tweet: (Read more from @horse_ebooks...) .

Обратите внимание, что невозможно вызвать реализацию по умолчанию из переопределённой реализации того же метода.

Типажи как параметры

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

Например, в листинге 10-13 мы реализовали типаж Summary для типов структур NewsArticle и Tweet. Можно определить функцию notify которая вызывает метод summarize с параметром item, который имеет тип реализующий типаж Summary . Для этого можно использовать синтаксис &impl Trait, например так:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Вместо конкретного типа у параметра item указывается ключевое слово impl и имя типажа. Этот параметр принимает любой тип, который реализует указанный типаж. В теле notify мы можем вызывать любые методы у экземпляра item, которые должны быть определены при реализации типажа Summary, например можно вызвать метод summarize. Мы можем вызвать notify и передать в него любой экземпляр NewsArticle или Tweet. Код, который вызывает данную функцию с любым другим типом, таким как String или i32, не будет компилироваться, потому что эти типы не реализуют типаж Summary.

Синтаксис ограничения типажа

Синтаксис impl Trait работает для простых случаев, но на самом деле является синтаксическим сахаром для более длинной формы, которая называется ограничением типажа; это выглядит так:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Эта более длинная форма эквивалентна примеру в предыдущем разделе, но она более многословна. Мы помещаем объявление параметра обобщённого типа с ограничением типажа после двоеточия внутри угловых скобок.

Синтаксис impl Trait удобен и делает более выразительным код в простых случаях. Синтаксис ограничений типажа может выразить большую сложность в других случаях. Например, у нас может быть два параметра, которые реализуют типаж Summary. Использование синтаксиса impl Trait выглядит следующим образом:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

Если бы мы хотели, чтобы эта функция позволяла иметь item1 и item2 разных типов, то использование impl Trait было бы уместно (до тех пор, пока оба типа реализуют Summary). Если мы хотим форсировать, чтобы оба параметра имели одинаковый тип, то это можно выразить только с использованием ограничения типажа, например так:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

Обобщённый тип T указан для типов параметров item1 и item2 и ограничивает функцию так, что конкретные значения типов переданные аргументами в item1 и item2 должны быть одинаковыми.

Задание нескольких границ типажей с помощью синтаксиса +

Также можно указать более одного ограничения типажа. Скажем, мы хотели бы использовать в методе notify для параметра item с форматированием отображения, также как метод summarize: для этого мы указываем в определении notify, что item должен реализовывать как типаж Display так и Summary. Мы можем сделать это используя синтаксис +:

pub fn notify(item: &(impl Summary + Display)) {

Синтаксис + также допустим с ограничениями типажа для обобщённых типов:

pub fn notify<T: Summary + Display>(item: &T) {

При наличии двух ограничений типажа, тело метода notify может вызывать метод summarize и использовать {} для форматирования item при его печати.

Более ясные границы типажа с помощью where

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

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

можно использовать предложение where , например так:

fn some_function<T, U>(t: &T, u: &U) -> i32
    where T: Display + Clone,
          U: Clone + Debug
{

Сигнатура этой функции менее загромождена: название функции, список параметров, и возвращаемый тип находятся рядом, а сигнатура не содержит в себе множество ограничений типажа.

Возврат значений типа реализующего определённый типаж

Также можно использовать синтаксис impl Trait в возвращаемой позиции, чтобы вернуть значение некоторого типа реализующего типаж, как показано здесь:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    Tweet {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        retweet: false,
    }
}

Используя impl Summary для возвращаемого типа, мы указываем, что функция returns_summarizable возвращает некоторый тип, который реализует типаж Summary без обозначения конкретного типа. В этом случае returns_summarizable возвращает Tweet, но код, вызывающий эту функцию, этого не знает.

Возможность возвращать тип, который определяется только реализуемым им признаком, особенно полезна в контексте замыканий и итераторов, которые мы рассмотрим в Главе 13. Замыкания и итераторы создают типы, которые знает только компилятор или типы, которые очень долго указывать. Синтаксис impl Trait позволяет кратко указать, что функция возвращает некоторый тип, который реализует типаж Iterator без необходимости писать очень длинный тип.

Однако, impl Trait возможно использовать, если возвращаете только один тип. Например, данный код, который возвращает значения или типа NewsArticle или типа Tweet, но в качестве возвращаемого типа объявляет impl Summary, не будет работать:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct Tweet {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub retweet: bool,
}

impl Summary for Tweet {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        Tweet {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            retweet: false,
        }
    }
}

Возврат либо NewsArticle либо Tweet не допускается из-за ограничений того, как реализован синтаксис impl Trait в компиляторе. Мы рассмотрим, как написать функцию с таким поведением в разделе "Использование объектов типажей, которые разрешены для значений или разных типов" Главы 17.

Исправление кода функции largest с помощью ограничений типажа

Теперь, когда вы знаете, как указать поведение, которое вы хотите использовать для ограничения параметра обобщённого типа, давайте вернёмся к листингу 10-5 и исправим определение функции largest. В прошлый раз мы пытались запустить этот код, но получили ошибку:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0369]: binary operation `>` cannot be applied to type `T`
 --> src/main.rs:5:17
  |
5 |         if item > largest {
  |            ---- ^ ------- T
  |            |
  |            T
  |
help: consider restricting type parameter `T`
  |
1 | fn largest<T: std::cmp::PartialOrd>(list: &[T]) -> T {
  |             ^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to previous error

For more information about this error, try `rustc --explain E0369`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

В теле функции largest мы хотели сравнить два значения типа T используя оператор больше чем ( > ). Так как этот оператор определён у типажа std::cmp::PartialOrd из стандартной библиотеки как метод по умолчанию, то нам нужно указать PartialOrd в качестве ограничения для типа T: благодаря этому функция largest сможет работать со срезами любого типа, значения которого мы можем сравнить. Нам не нужно подключать PartialOrd в область видимости, потому что он есть в авто-импорте. Изменим сигнатуру largest, чтобы она выглядела так:

fn largest<T: PartialOrd>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

На этот раз при компиляции кода мы получаем другой набор ошибок:

$ cargo run
   Compiling chapter10 v0.1.0 (file:///projects/chapter10)
error[E0508]: cannot move out of type `[T]`, a non-copy slice
 --> src/main.rs:2:23
  |
2 |     let mut largest = list[0];
  |                       ^^^^^^^
  |                       |
  |                       cannot move out of here
  |                       move occurs because `list[_]` has type `T`, which does not implement the `Copy` trait
  |                       help: consider borrowing here: `&list[0]`

error[E0507]: cannot move out of a shared reference
 --> src/main.rs:4:18
  |
4 |     for &item in list {
  |         -----    ^^^^
  |         ||
  |         |data moved here
  |         |move occurs because `item` has type `T`, which does not implement the `Copy` trait
  |         help: consider removing the `&`: `item`

error: aborting due to 2 previous errors

Some errors have detailed explanations: E0507, E0508.
For more information about an error, try `rustc --explain E0507`.
error: could not compile `chapter10`

To learn more, run the command again with --verbose.

Ключевая строка в этой ошибке cannot move out of type [T], a non-copy slice. В нашей необобщённой версии функции largest мы пытались найти самый большой элемент только для типа i32 или char. Как обсуждалось в разделе "Данные только для стека: Копирование" Главы 4, типы подобные i32 и char, имеющие известный размер, могут храниться в стеке, поэтому они реализуют типаж Copy. Но когда мы сделали функцию largest обобщённой, для параметра list стало возможным иметь типы, которые не реализуют типаж Copy. Следовательно, мы не сможем переместить значение из переменной list[0] в переменную largest, в результате чего появляется эта ошибка.

Чтобы вызывать этот код только с теми типами, которые реализуют типаж Copy, можно добавить типаж Copy в список ограничений типа T! Листинг 10-15 показывает полный код обобщённой функции largest, которая будет компилироваться, пока типы значений среза передаваемых в функцию, реализуют одновременно типажи PartialOrd и Copy, как это делают i32 и char.

Файл: src/main.rs

fn largest<T: PartialOrd + Copy>(list: &[T]) -> T {
    let mut largest = list[0];

    for &item in list {
        if item > largest {
            largest = item;
        }
    }

    largest
}

fn main() {
    let number_list = vec![34, 50, 25, 100, 65];

    let result = largest(&number_list);
    println!("The largest number is {}", result);

    let char_list = vec!['y', 'm', 'a', 'q'];

    let result = largest(&char_list);
    println!("The largest char is {}", result);
}

Листинг 10-15: Объявление функции largest работающей с любыми обобщёнными типами, которые реализуют типажи PartialOrd и Copy

Если мы не хотим ограничить функцию largest типами, которые реализуют типаж Copy, мы можем указать, что T имеет ограничение типажа Clone вместо Copy. Затем мы могли бы клонировать каждое значение в срезе, если бы хотели чтобы функция largest забирала владение. Использование функции clone означает, что потенциально делается больше операций выделения памяти в куче для типов, которые владеют данными в куче, например для String. В то же время стоит помнить о том, что выделение памяти в куче может быть медленным, если мы работаем с большими объёмами данных.

Ещё один способ, который мы могли бы реализовать в largest - это создать функцию возвращающую ссылку на значение T из среза. Если мы изменим возвращаемый тип на &T вместо T, то тем самым изменим тело функции, чтобы она возвращала ссылку, тогда нам были бы не нужны ограничения входных значений типажами Clone или Copy и мы могли бы избежать выделения памяти в куче. Попробуйте реализовать эти альтернативные решения самостоятельно!

Использование ограничений типажа для условной реализации методов

Используя ограничение типажа с блоком impl, который использует параметры обобщённого типа, можно реализовать методы условно, для тех типов, которые реализуют указанный типаж. Например, тип Pair<T> в листинге 10-16 всегда реализует функцию new. Но Pair<T> реализует метод cmp_display только если его внутренний тип T реализует типаж PartialOrd (позволяющий сравнивать) и типаж Display (позволяющий выводить на печать).

Файл: src/lib.rs

use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}

Листинг 10-17: Условная реализация методов у обобщённых типов в зависимости от ограничений типажа

Мы также можем условно реализовать типаж для любого типа, который реализует другой типаж. Реализации типажа для любого типа, который удовлетворяет ограничениям типажа называются общими реализациями и широко используются в стандартной библиотеке Rust. Например, стандартная библиотека реализует типаж ToString для любого типа, который реализует типаж Display. Блок impl в стандартной библиотеке выглядит примерно так:

impl<T: Display> ToString for T {
    // --snip--
}

Поскольку стандартная библиотека имеет эту общую реализацию, то можно вызвать метод to_string определённый типажом ToString для любого типа, который реализует типаж Display. Например, мы можем превратить целые числа в их соответствующие String значения, потому что целые числа реализуют типаж Display:


#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Общие реализации приведены в документации к типажу в разделе "Implementors".

Типажи и ограничения типажей позволяют писать код, который использует параметры обобщённого типа для уменьшения дублирования кода, а также указывая компилятору, что мы хотим обобщённый тип, чтобы иметь определённое поведение. Затем компилятор может использовать информацию про ограничения типажа, чтобы проверить, что все конкретные типы, используемые с нашим кодом, обеспечивают правильное поведение. В динамически типизированных языках мы получили бы ошибку во время выполнения, если бы вызвали метод для типа, который не реализует тип определяемый методом. Но Rust перемещает эти ошибки на время компиляции, поэтому мы вынуждены исправить проблемы, прежде чем наш код начнёт работать. Кроме того, мы не должны писать код, который проверяет своё поведение во время выполнения, потому что это уже проверено во время компиляции. Это повышает производительность без необходимости отказываться от гибкости обобщённых типов.

Другой тип обобщения, который мы уже использовали, называется временами жизни (lifetimes). Вместо гарантирования того, что тип ведёт себя так, как нужно, время жизни гарантирует что ссылки действительны до тех пор, пока они нужны. Давайте посмотрим, как времена жизни это делают.