Трейти: визначення загальної поведінки
Трейт визначає функціональність, якою володіє визначений тип та якою він може ділитися з іншими типами. Ми можемо використовувати трейти, щоб визначати спільну поведінку абстрактним способом. Також маємо змогу застосувати обмеження трейту, щоб вказати, що загальний тип, може бути будь-яким типом, який реалізує певну поведінку.
Note: Traits are similar to a feature often called interfaces in other languages, although with some differences.
Визначення трейту
Поведінка типу визначається тими методами, які ми можемо викликати у цього типу. Різні типи розділяють однакову поведінку, якщо ми можемо викликати одні й ті самі методи цих типів. Визначення трейтів - це спосіб згрупувати сигнатури методів разом, заради того, щоб описати загальну поведінку, необхідну для досягнення певної мети.
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 Tweet
that can have at most 280 characters along with metadata that indicates whether it was a new tweet, a retweet, or a reply to another tweet.
Ми хочемо створити бібліотечний крейт медіа агрегатору під назвою 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
визначений з точно такою сигнатурою.
A trait can have multiple methods in its body: the method signatures are listed one per line and each line ends in a semicolon.
Реалізація трейту для типів
Тепер, після того, як ми визначили бажану поведінку, використовуючи трейт 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 не знав би, яку реалізацію використовувати.
Реалізація поведінки за замовчуванням
Іноді корисно мати поведінку за замовчуванням для деяких чи всіх методів трейту замість того, щоб вимагати реалізації всіх методів у кожному типі, що реалізує цей трейт. Потім, коли ми реалізуємо трейт для певного типу, можна зберегти чи перевизначити поведінку кожного методу за замовчуванням вже всередині типів.
In Listing 10-14 we specify a default string for the summarize
method of the Summary
trait instead of only defining the method signature, as we did in Listing 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)
}
}
To use a default implementation to summarize instances of NewsArticle
, we specify an empty impl
block with 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)
}
}
To use this version of Summary
, we only need to define summarize_author
when we implement the trait on a type:
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
працює для простих випадків, але насправді є синтаксичним цукром для більш довгої форми, яка називається обмеження трейту, це виглядає ось так:
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) {
With the two trait bounds specified, the body of notify
can call summarize
and use {}
to format 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
{
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.
Повертання значень типу, що реалізує певний трейт
We can also use the impl Trait
syntax in the return position to return a value of some type that implements a trait, as shown here:
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);
}
}
}
Ми також можемо умовно реалізувати трейт для будь-якого типу, який реалізує інший трейт. Реалізація трейту для будь-якого типу, що задовольняє обмеженням трейту називається загальною реалізацією (blanket implementations) й широко використовується в стандартній бібліотеці 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 перекладає ці помилки на час компіляції, тому ми повинні виправити проблеми, перш ніж наш код почне працювати. Крім того, ми не повинні писати код, який перевіряє свою поведінку під час компіляції, тому що це вже перевірено під час компіляції. Це підвищує швидкодію без необхідності відмовлятися від гнучкості узагальнених типів.