Defining Shared Behavior with Traits
Трейт определяет функциональность, которой обладает конкретный тип и которая может использоваться с другими типами. Мы можем использовать трейты для определения общего поведения абстрактным образом. Мы можем использовать ограничение по трейтам, чтобы указать, что обобщённый тип может быть заменён на любой тип с некоторым известным поведением.
Примечание: Трейты схожи с таким механизмом как интерфейсы, но всё же они различаются.
Определение трейта
Поведение типа определяется теми методами, которые мы можем вызвать на данном типе. Различные типы разделяют одинаковое поведение, если мы можем вызвать одни и те же методы у этих типов. Определение трейтов — это способ сгруппировать сигнатуры методов вместе для того, чтобы описать общее поведение, необходимое для достижения определённой цели.
For example, let’s say we have multiple structs that hold various kinds and amounts of text: a NewsArticle struct that holds a news story filed in a particular location and a SocialPost that can have, at most, 280 characters along with metadata that indicates whether it was a new post, a repost, or a reply to another post.
We want to make a media aggregator library crate named aggregator that can display summaries of data that might be stored in a NewsArticle or SocialPost instance. To do this, we need a summary from each type, and we’ll request that summary by calling a summarize method on an instance. Listing 10-12 shows the definition of a public Summary trait that expresses this behavior.
pub trait Summary {
fn summarize(&self) -> String;
}
Summary trait that consists of the behavior provided by a summarize methodЗдесь мы объявляем трейт с использованием ключевого слова trait, а затем указываем его название (в нашем случае — Summary). Также мы объявляем трейт как pub, что позволяет крейтам, зависящим от нашего крейта, использовать наш трейт, примеры чего мы увидим далее. Внутри фигурных скобок объявляются сигнатуры методов, которые описывают поведение типов, реализующих данный трейт; в данном случае, поведение определяется только одной сигнатурой метода fn summarize(&self) -> String.
После сигнатуры метода, вместо его тела мы пишем точку с запятой. Каждый тип, реализующий данный трейт, должен будет предоставить свою собственную реализацию данного метода. Компилятор обеспечит, что любой тип, реализующий трейт Summary, будет также иметь и метод summarize, объявленный с точно такой же сигнатурой.
A trait can have multiple methods in its body: The method signatures are listed one per line, and each line ends in a semicolon.
Реализация трейта для типа
Now that we’ve defined the desired signatures of the Summary trait’s methods, we can implement it on the types in our media aggregator. Listing 10-13 shows an implementation of the Summary trait on the NewsArticle struct that uses the headline, the author, and the location to create the return value of summarize. For the SocialPost struct, we define summarize as the username followed by the entire text of the post, assuming that the post content is already limited to 280 characters.
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!("{} ({} из {})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Summary trait on the NewsArticle and SocialPost typesРеализация трейта для типа аналогична реализации обычных методов. Разница в том, что после impl мы указываем имя трейта, который мы хотим реализовать, затем используем ключевое слово for, а затем указываем имя типа, для которого мы хотим сделать реализацию трейта. Внутри блока impl мы помещаем сигнатуру метода, объявленную в трейте. Вместо точки с запятой в конце, после каждой сигнатуры пишутся фигурные скобки, и тело метода заполняется конкретным кодом, реализующим поведение, которое мы хотим получить от методов трейта для конкретного типа.
Now that the library has implemented the Summary trait on NewsArticle and SocialPost, users of the crate can call the trait methods on instances of NewsArticle and SocialPost in the same way we call regular methods. The only difference is that the user must bring the trait into scope as well as the types. Here’s an example of how a binary crate could use our aggregator library crate:
use aggregator::{SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"конечно, вы уже наверное знаете, народ, ...",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
This code prints 1 new post: horse_ebooks: of course, as you probably already know, people.
Other crates that depend on the aggregator crate can also bring the Summary trait into scope to implement Summary on their own types. One restriction to note is that we can implement a trait on a type only if either the trait or the type, or both, are local to our crate. For example, we can implement standard library traits like Display on a custom type like SocialPost as part of our aggregator crate functionality because the type SocialPost is local to our aggregator crate. We can also implement Summary on Vec<T> in our aggregator crate because the trait Summary is local to our aggregator crate.
But we can’t implement external traits on external types. For example, we can’t implement the Display trait on Vec<T> within our aggregator crate, because Display and Vec<T> are both defined in the standard library and aren’t local to our aggregator crate. This restriction is part of a property called coherence, and more specifically the orphan rule, so named because the parent type is not present. This rule ensures that other people’s code can’t break your code and vice versa. Without the rule, two crates could implement the same trait for the same type, and Rust wouldn’t know which implementation to use.
Using Default Implementations
Иногда бывает полезно задать поведение по умолчанию для некоторых или всех методов трейта вместо того, чтобы требовать реализации всех методов для каждого типа. Такое поведение по умолчанию можно будет (уже непосредственно при реализации трейта) как оставить, так и переопределить.
В Листинге 10-14 показано, мы определяем сводку по умолчанию для метода summarize трейта Summary, вместо того, чтобы определять лишь сигнатуру метода, как мы делали ранее в Листинге 10-12.
pub trait Summary {
fn summarize(&self) -> String {
String::from("(Читать далее...)")
}
}
pub struct NewsArticle {
pub headline: String,
pub location: String,
pub author: String,
pub content: String,
}
impl Summary for NewsArticle {}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
Summary trait with a default implementation of the summarize methodДля использования реализации по умолчанию при создании сводки экземпляров NewsArticle, мы указываем пустой блок при impl Summary for NewsArticle {}.
Хотя мы больше не определяем метод summarize непосредственно на NewsArticle, мы предоставили реализацию по умолчанию и указали, что NewsArticle реализует трейт Summary. В результате мы всё ещё можем вызвать метод summarize у экземпляра NewsArticle; например, вот так:
use aggregator::{self, NewsArticle, Summary};
fn main() {
let article = NewsArticle {
headline: String::from("«Питтсбург Пингвинз» выиграли Кубок Стэнли!"),
location: String::from("Питтсбург, штат Пенсильвания, США"),
author: String::from("Пингвин Айсбург"),
content: String::from(
"«Питтсбург Пингвинз» вновь оказалась лучшей \
хоккейной командой в НХЛ.",
),
};
println!("Новости! {}", article.summarize());
}
Этот код печатает Новости! (Читать далее...).
Creating a default implementation doesn’t require us to change anything about the implementation of Summary on SocialPost in Listing 10-13. The reason is that the syntax for overriding a default implementation is the same as the syntax for implementing a trait method that doesn’t have a default implementation.
Реализации по умолчанию могут вызывать другие методы в том же трейте, даже если эти другие методы не имеют реализации по умолчанию. Таким образом, трейт может предоставить много полезной функциональности, а от разработчиков требует указывать только небольшую его часть. Например, мы могли бы определить трейт Summary, имеющий метод summarize_author без реализации по умолчанию, а затем определить метод summarize который имеет реализацию по умолчанию, вызывающую метод summarize_author:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Читать далее, от {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
Чтобы использовать такую версию трейта Summary, при реализации трейта для типа нужно определить только метод summarize_author:
pub trait Summary {
fn summarize_author(&self) -> String;
fn summarize(&self) -> String {
format!("(Читать далее, от {}...)", self.summarize_author())
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize_author(&self) -> String {
format!("@{}", self.username)
}
}
After we define summarize_author, we can call summarize on instances of the SocialPost struct, and the default implementation of summarize will call the definition of summarize_author that we’ve provided. Because we’ve implemented summarize_author, the Summary trait has given us the behavior of the summarize method without requiring us to write any more code. Here’s what that looks like:
use aggregator::{self, SocialPost, Summary};
fn main() {
let post = SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"конечно, вы уже наверное знаете, народ, ...",
),
reply: false,
repost: false,
};
println!("1 new post: {}", post.summarize());
}
This code prints 1 new post: (Read more from @horse_ebooks...).
Обратите внимание, что невозможно вызвать реализацию по умолчанию из переопределённой реализации того же метода.
Using Traits as Parameters
Now that you know how to define and implement traits, we can explore how to use traits to define functions that accept many different types. We’ll use the Summary trait we implemented on the NewsArticle and SocialPost types in Listing 10-13 to define a notify function that calls the summarize method on its item parameter, which is of some type that implements the Summary trait. To do this, we use the impl Trait syntax, like this:
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!("{} ({} из {})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
pub fn notify(item: &impl Summary) {
println!("Срочные новости! {}", item.summarize());
}
Instead of a concrete type for the item parameter, we specify the impl keyword and the trait name. This parameter accepts any type that implements the specified trait. In the body of notify, we can call any methods on item that come from the Summary trait, such as summarize. We can call notify and pass in any instance of NewsArticle or SocialPost. Code that calls the function with any other type, such as a String or an i32, won’t compile, because those types don’t implement Summary.
Ограничение по трейтам
Синтаксис impl Trait работает для простых случаев, но на самом деле является синтаксическим сахаром для более длинной конструкции — ограничения по трейтам:
pub fn notify<T: Summary>(item: &T) {
println!("Срочные новости! {}", 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 должны быть одинаковыми.
Multiple Trait Bounds with the + Syntax
We can also specify more than one trait bound. Say we wanted notify to use display formatting as well as summarize on item: We specify in the notify definition that item must implement both Display and Summary. We can do so using the + syntax:
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!()
}
This function’s signature is less cluttered: The function name, parameter list, and return type are close together, similar to a function without lots of trait bounds.
Возврат значений типа, реализующего определённые трейты
Также можно использовать запись 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!("{} ({} из {})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable() -> impl Summary {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"конечно, вы уже наверное знаете, народ, ...",
),
reply: false,
repost: false,
}
}
By using impl Summary for the return type, we specify that the returns_summarizable function returns some type that implements the Summary trait without naming the concrete type. In this case, returns_summarizable returns a SocialPost, but the code calling this function doesn’t need to know that.
Возможность возвращать тип, который определяется только реализуемым им трейтом, особенно полезна в контексте замыканий и итераторов, которые мы рассмотрим в Главе 13. Замыкания и итераторы создают типы, которые знает только компилятор, или же типы, которые очень долго указывать. Запись impl Trait позволяет кратко указать, что функция возвращает некоторый тип, который реализует трейт Iterator без необходимости писать очень длинный тип.
However, you can only use impl Trait if you’re returning a single type. For example, this code that returns either a NewsArticle or a SocialPost with the return type specified as impl Summary wouldn’t work:
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!("{} ({} из {})", self.headline, self.author, self.location)
}
}
pub struct SocialPost {
pub username: String,
pub content: String,
pub reply: bool,
pub repost: bool,
}
impl Summary for SocialPost {
fn summarize(&self) -> String {
format!("{}: {}", self.username, self.content)
}
}
fn returns_summarizable(switch: bool) -> impl Summary {
if switch {
NewsArticle {
headline: String::from(
"«Питтсбург Пингвинз» выиграли Кубок Стэнли!",
),
location: String::from("Питтсбург, штат Пенсильвания, США"),
author: String::from("Пингвин Айсбург"),
content: String::from(
"«Питтсбург Пингвинз» вновь оказалась лучшей \
хоккейной командой в НХЛ.",
),
}
} else {
SocialPost {
username: String::from("horse_ebooks"),
content: String::from(
"конечно, вы уже наверное знаете, народ, ...",
),
reply: false,
repost: false,
}
}
}
Returning either a NewsArticle or a SocialPost isn’t allowed due to restrictions around how the impl Trait syntax is implemented in the compiler. We’ll cover how to write a function with this behavior in the “Using Trait Objects to Abstract over Shared Behavior” section of Chapter 18.
Использование ограничений по трейту для избирательной реализации методов
By using a trait bound with an impl block that uses generic type parameters, we can implement methods conditionally for types that implement the specified traits. For example, the type Pair<T> in Listing 10-15 always implements the new function to return a new instance of Pair<T> (recall from the “Method Syntax” section of Chapter 5 that Self is a type alias for the type of the impl block, which in this case is Pair<T>). But in the next impl block, Pair<T> only implements the cmp_display method if its inner type T implements the PartialOrd trait that enables comparison and the Display trait that enables printing.
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!("Слева ({}) — наибольшее", self.x);
} else {
println!("Справа ({}) — наибольшее", self.y);
}
}
}
Мы также можем избирательно реализовать трейт для всех типов, которые реализуют другой трейт. Реализация трейта для всех типов, которые удовлетворяют ограничениям по трейтам, называются сплошной реализацией, и она широко используется в стандартной библиотеке Rust. Например, стандартная библиотека реализует трейт ToString для всех типов, которые реализуют трейт Display. Блок impl, делающий это, выглядит примерно так:
impl<T: Display> ToString for T {
// --код сокращён--
}
Благодаря такой сплошной реализации можно вызвать метод to_string, определённый трейтом ToString, для любого типа, который реализует трейт Display. Например, мы можем превратить целые числа в их соответствующие значения String, потому что целые числа реализуют трейт Display:
#![allow(unused)]
fn main() {
let s = 3.to_string();
}
В документации к трейтам, их сплошные реализации можно увидеть в разделе “Implementors”.
Traits and trait bounds let us write code that uses generic type parameters to reduce duplication but also specify to the compiler that we want the generic type to have particular behavior. The compiler can then use the trait bound information to check that all the concrete types used with our code provide the correct behavior. In dynamically typed languages, we would get an error at runtime if we called a method on a type that didn’t define the method. But Rust moves these errors to compile time so that we’re forced to fix the problems before our code is even able to run. Additionally, we don’t have to write code that checks for behavior at runtime, because we’ve already checked at compile time. Doing so improves performance without having to give up the flexibility of generics.