Векторы трейтов
Часть раздела Object Oriented Programming путешествия по Rust на Coddy — урок 46 из 61.
Вы видели, как Box<dyn Trait> позволяет одной переменной хранить разные типы. Настоящая мощь проявляется, когда вы комбинируете это с коллекциями — хранением нескольких различных типов в одном векторе.
Обычный вектор требует, чтобы все элементы были одного типа. У вас не может быть Vec<Dog>, который также содержит кошек. Но с помощью объектов-типажей вы можете создать вектор, который содержит что угодно, реализующее общий типаж:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
struct Bird;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
impl Speak for Bird {
fn speak(&self) -> String { String::from("Tweet!") }
}
fn main() {
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog),
Box::new(Cat),
Box::new(Bird),
];
}
Тип Vec<Box<dyn Speak>> означает «вектор упакованных (boxed) объектов-типажей». Каждый элемент — это Box, указывающий на что-то, что реализует Speak, — фактические типы могут различаться. Вы оборачиваете каждое значение с помощью Box::new() при добавлении его в вектор.
Этот паттерн необходим для создания гибких систем, в которых вам нужно управлять коллекциями связанных, но различных объектов — таких как компоненты пользовательского интерфейса, игровые сущности или системы плагинов.
Задание
ЛегкоДавайте создадим систему уведомлений, которая может обрабатывать различные типы оповещений! Вы создадите коллекцию, в которой хранятся различные типы уведомлений — электронные письма, SMS-сообщения и push-уведомления — все в одном векторе с использованием объектов типажа (trait objects).
Вы организуете свой код в двух файлах:
notifications.rs: Определите публичный типажNotifyс методомsend, который принимает&selfи возвращаетString, описывающую отправляемое уведомление. Затем создайте три публичные структуры:Email— с публичным полемrecipient(String). Его методsendдолжен возвращать"Email to: {recipient}"Sms— с публичным полемphone(String). Его методsendдолжен возвращать"SMS to: {phone}"Push— с публичным полемdevice(String). Его методsendдолжен возвращать"Push to: {device}"
main.rs: Подключите модуль notifications и создайте вектор типаVec<Box<dyn Notify>>, который содержит все три типа уведомлений. Используйте предоставленные входные данные для создания по одному экземпляру каждого типа уведомления, добавьте их в вектор по порядку (Email, Sms, Push), а затем выведите результат вызоваsend()для первого элемента вектора.
Ключевая концепция здесь заключается в том, что, несмотря на то что Email, Sms и Push являются совершенно разными структурами, они все могут находиться в одном векторе, так как они реализуют типаж Notify. Каждый элемент оборачивается в Box::new() для создания объекта типажа.
Ваш вывод должен показать первое уведомление:
Email to: {recipient}Например, при входных данных "alice@example.com", "555-1234" и "iPhone-12":
Email to: alice@example.comВы получите три входных значения: получателя электронного письма, номер телефона и название устройства.
Шпаргалка
Vec<Box<dyn Trait>> позволяет хранить несколько различных типов в одном векторе, при условии, что все они реализуют один и тот же типаж (trait).
Обычные векторы требуют, чтобы все элементы были одного типа, но объекты-типажи (trait objects) позволяют создавать гетерогенные коллекции:
trait Speak {
fn speak(&self) -> String;
}
struct Dog;
struct Cat;
impl Speak for Dog {
fn speak(&self) -> String { String::from("Woof!") }
}
impl Speak for Cat {
fn speak(&self) -> String { String::from("Meow!") }
}
fn main() {
let animals: Vec<Box<dyn Speak>> = vec![
Box::new(Dog),
Box::new(Cat),
];
}
При добавлении в вектор каждый элемент оборачивается в Box::new(). Фактические типы могут отличаться, если они реализуют общий типаж.
Попробуйте сами
mod notifications;
use notifications::{Notify, Email, Sms, Push};
fn main() {
// Чтение входных данных
let mut recipient = String::new();
std::io::stdin().read_line(&mut recipient).expect("Failed to read line");
let recipient = recipient.trim().to_string();
let mut phone = String::new();
std::io::stdin().read_line(&mut phone).expect("Failed to read line");
let phone = phone.trim().to_string();
let mut device = String::new();
std::io::stdin().read_line(&mut device).expect("Failed to read line");
let device = device.trim().to_string();
// TODO: Создать Vec<Box<dyn Notify>> для хранения различных типов уведомлений
// TODO: Создать экземпляры Email, Sms и Push, используя входные данные
// TODO: Добавить их в вектор по порядку (Email, Sms, Push), используя Box::new()
// TODO: Вывести результат вызова send() для первого элемента
}
В этом уроке есть небольшой тест. Начните урок, чтобы ответить на вопросы и сохранить прогресс.
Все уроки раздела Object Oriented Programming
1Методы и поведение
Введение в блоки реализацииПараметр SelfИзменяемые методыАссоциированные функцииНесколько блоков реализацииЦепочки вызовов методовИтоги: действия с Rectangle4Проект: Виртуальный питомец
Определение питомцаКормление питомца2Инкапсуляция и модули
Основы модулейКлючевое слово PublicПриватные поляГеттерыСеттерыИтоги — Безопасный сейф5Обобщения
Обобщённые структурыОбобщённые методыНесколько обобщённых типовОбобщённые функцииПовторение — Координатная точка8Трейты как ограничения
Синтаксис ограничений трейтовМножественные ограниченияКонструкция whereВозврат типов с трейтамиИтоги — Обобщённый принтер11Паттерны проектирования в Rust
Паттерн NewtypeКомпозицияТрейт DropFrom и IntoИтоги — Мок-объект Smart Pointer3Продвинутые перечисления
Перечисления с даннымиМетоды в перечисленияхСопоставление вариантов с даннымиСнова об Option EnumИтоги — перечисление Shape6Определение трейтов
Что такое трейт?Реализация трейтовРеализации по умолчаниюПереопределение реализаций по умолчаниюТрейты с параметрамиИтоги — Media Player9Трейт-объекты
Динамическая диспетчеризацияВекторы трейтовИтерация по трейт-объектамПовторение — Менеджер зоопарка