Типажи: определение общего поведения
Типаж сообщает компилятору Rust о функциональности, которой обладает определённый тип и которой он может поделиться с другими типами. Можно использовать типажи, чтобы определять общее поведение абстрактным способом. Мы можем использовать ограничение типажа (trait bounds) чтобы указать, что общим типом может быть любой тип, который имеет определённое поведение.
Примечание: Типажи похожи на функциональность часто называемую интерфейсами в других языках программирования, хотя и с некоторыми отличиями.
Определение типажа
Поведение типа определяется теми методами, которые мы можем вызвать у данного типа. Различные типы разделяют одинаковое поведение, если мы можем вызвать одни и те же методы у этих типов. Определение типажей - это способ сгруппировать сигнатуры методов вместе для того, чтобы описать общее поведение, необходимое для достижения определённой цели.
Например, пусть есть несколько структур, которые имеют различный тип и различный размер текста: структура NewsArticle
, которая содержит новость, напечатанную в каком-то месте мира; структура Tweet
, которая содержит 280 символьную строку твита и мета-данные, обозначающие является ли твит новым или ответом на другой твит.
Мы хотим создать крейт библиотеки медиа-агрегатора aggregator
, которая может отображать сводку данных сохранённых в экземплярах структур NewsArticle
или Tweet
. Чтобы этого достичь, нам необходимо иметь возможность для каждой структуры получить короткую сводку на основе имеющихся данных, и для этого мы запросим сводку вызвав метод summarize
. Листинг 10-12 показывает определение типажа Summary
, который выражает это поведение.
Файл: src/lib.rs
pub trait Summary {
fn summarize(&self) -> String;
}
Здесь мы объявляем типаж с использованием ключевого слова trait
, а затем его название, которым в нашем случае является Summary
. Также мы объявляем крейт как pub
что позволяет крейтам, зависящим от нашего крейта, тоже использовать наш крейт, что мы увидим в последующих примерах. Внутри фигурных скобок объявляются сигнатуры методов, которые описывают поведения типов, реализующих данный типаж, в данном случае поведение определяется только одной сигнатурой метода 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)
}
}
Реализация типажа у типа аналогична реализации обычных методов. Разница в том что после impl
мы ставим имя типажа, который мы хотим реализовать, затем используем ключевое слово for
, а затем указываем имя типа, для которого мы хотим сделать реализацию типажа. Внутри блока impl
мы помещаем сигнатуру метода объявленную в типаже. Вместо добавления точки с запятой в конце, после каждой сигнатуры используются фигурные скобки и тело метода заполняется конкретным поведением, которое мы хотим получить у методов типажа для конкретного типа.
Теперь когда библиотека реализовала типаж Summary
для NewsArticle
и Tweet
, программисты использующие крейт могут вызывать методы типажа у экземпляров типов NewsArticle
и Tweet
точно так же как если бы это были обычные методы. Единственное отличие состоит в том, что программист должен ввести типаж в область видимости точно так же как и типы. Здесь пример того как бинарный крейт может использовать наш aggregator
:
use aggregator::{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
.
Другие крейты, которые зависят от aggregator
, тоже могу включить типаж Summary
в область видимости для реализации Summary
в их собственных типах. Одно ограничение, на которое следует обратить внимание, заключается в том, что мы можем реализовать типаж для типа только в том случае, если хотя бы один из типажей типа является локальным для нашего крейта. Например, мы можем реализовать стандартный библиотечный типаж Display
на собственном типе Tweet
как часть функциональности нашего крейта aggregator
потому что тип Tweet
является локальным для крейта aggregator
. Также мы можем реализовать Summary
для Vec<T>
в нашем крейте aggregator
, потому что типаж Summary
является локальным для нашего крейта aggregator
.
Но мы не можем реализовать внешние типажи для внешних типов. Например, мы не можем реализовать типаж Display
для Vec<T>
внутри нашего крейта aggregator
, потому что Display
и Vec<T>
оба определены в стандартной библиотеке а не локально в нашем крейте aggregator
. Это ограничение является частью свойства называемого согласованность (coherence), а ещё точнее сиротское правило (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)
}
}
Для использования реализации по умолчанию при создании сводки у экземпляров NewsArticle
вместо определения пользовательской реализации, мы указываем пустой блок impl
с impl Summary for NewsArticle {}
.
Хотя мы больше не определяем метод summarize
непосредственно в NewsArticle
, мы предоставили реализацию по умолчанию и указали, что NewsArticle
реализует типаж Summary
. В результате мы всё ещё можем вызвать метод summarize
у экземпляра NewsArticle
, например так:
use aggregator::{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...)
.
Создание реализации по умолчанию не требует от нас изменений чего-либо в реализации 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 aggregator::{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...)
.
Обратите внимание, что невозможно вызвать реализацию по умолчанию из переопределённой реализации того же метода.
Типажи как параметры
Теперь, когда вы знаете, как определять и реализовывать типажи, можно изучить, как использовать типажи, чтобы определить функции, которые принимают много различных типов. Мы будем использовать типаж Summary
, реализованный для типов NewsArticle
и Tweet
в листинге 10-13, чтобы определить функцию 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
работает для простых случаев, но на самом деле является синтаксическим сахаром для более длинной формы, которая называется ограничением типажа (trait bound); это выглядит так:
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) {
Использовать impl Trait
удобнее если мы хотим разрешить функции иметь разные типы для item1
и item2
(но оба типа должны реализовывать Summary
). Если же мы хотим заставить оба параметра иметь один и тот же тип, то мы должны использовать ограничение типажа так:
pub fn notify<T: Summary>(item1: &T, item2: &T) {
Обобщённый тип T
указан для типов параметров item1
и item2
и ограничивает функцию так, что конкретные значения типов переданные аргументами для item1
и item2
должны быть одинаковыми.
Задание нескольких границ типажей с помощью синтаксиса +
Также можно указать более одного ограничения типажа. Допустим, мы хотели бы чтобы notify
использовал как форматирование вывода так и summarize
для параметра item
:
тогда мы указываем что в 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,
{
unimplemented!()
}
Сигнатура этой функции менее загромождена: название функции, список параметров, и возвращаемый тип находятся рядом, а сигнатура не содержит в себе множество ограничений типажа.
Возврат значений типа реализующего определённый типаж
Также можно использовать синтаксис 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.
Использование ограничений типажа для условной реализации методов
Используя ограничение типажа с блоком impl
, который использует параметры обобщённого типа, можно реализовать методы условно, для тех типов, которые реализуют указанный типаж. Например, тип Pair<T>
в листинге 10-15 всегда реализует функцию new
для возврата нового экземпляра Pair<T>
(вспомните раздел “Определение методов” Главы 5 где Self
является псевдонимом типа для типа блока impl
, который в данном случае является Pair<T>
). Но в следующем блоке impl
тип 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);
}
}
}
Мы также можем условно реализовать типаж для любого типа, который реализует другой типаж. Реализации типажа для любого типа, который удовлетворяет ограничениям типажа, называются общими реализациями и широко используются в стандартной библиотеке 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 перемещает эти ошибки на время компиляции, поэтому мы вынуждены исправить проблемы, прежде чем наш код начнёт работать. Кроме того, мы не должны писать код, который проверяет своё поведение во время выполнения, потому что это уже проверено во время компиляции. Это повышает производительность без необходимости отказываться от гибкости обобщённых типов.