Реализация примеси
Теперь мы добавим несколько типов, которые реализуют примесь Draw
. Мы предоставим тип Button
. Опять же, фактическая реализация GUI библиотеки выходит за рамки этого учебника, поэтому метод draw
не будет иметь полезной реализации в теле. Чтобы представить, как может выглядеть реализация, структура Button
может иметь поля для width
, height
и label
, как показано в Листинге 17-7.
Имя файла: src/lib.rs
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// код для фактического рисования кнопки
}
}
Листинг 17-7: Структура Button
, которая реализует примесь Draw
Поля width
, height
и label
на Button
будут отличаться от полей на других компонентах; например, тип TextField
может иметь те же поля плюс поле placeholder
. Каждый из типов, которые мы хотим нарисовать на экране, будет реализовывать примесь Draw
, но будет использовать разные коды в методе draw
, чтобы определить, как нарисовать этот конкретный тип, как это делает Button
здесь (без фактического кода GUI, как упоминалось). Тип Button
, например, может иметь дополнительный блок impl
, содержащий методы, связанные с тем, что происходит, когда пользователь нажимает кнопку. Эти виды методов не применимые к типам, таким как TextField
.
Если кто-то, используя нашу библиотеку, решит реализовать структуру SelectBox
, которая имеет поля width
, height
и options
, они также реализуют примесь Draw
для типа SelectBox
, как показано в Листинге 17-8.
Имя файла: src/main.rs
use gui::Draw;
struct SelectBox {
width: u32,
height: u32,
options: Vec<String>,
}
impl Draw for SelectBox {
fn draw(&self) {
// код для фактического рисования выпадающего списка
}
}
Листинг 17-8: Другой крейт, использующий gui
и реализующий примесь Draw
для структуры SelectBox
Пользователь нашей библиотеки теперь может написать свою функцию main
, чтобы создать экземпляр Screen
. К экземпляру Screen
они могут добавить SelectBox
и Button
, поместив каждый в Box<T>
, чтобы получить объект-примесь. Затем они могут вызвать метод run
на экземпляре Screen
, который вызовет draw
для каждого из компонентов. Листинг 17-9 показывает эту реализацию.
Имя файла: src/main.rs
use gui::{Button, Screen};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No"),
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
Листинг 17-9: Использование объектов-примесей для хранения значений разных типов, которые реализуют одну и ту же примесь
Когда мы писали библиотеку, мы не знали, что кто-то может добавить тип SelectBox
, но наша реализация Screen
смогла работать с новым типом и нарисовать его, потому что SelectBox
реализует примесь Draw
, что означает, что он реализует метод draw
.
Этот концепт - быть concerned только с теми сообщениями, на которые отвечает значение, а не с конкретным типом значения - похож на концепцию duck typing в динамически типизированных языках: если оно ходит как утка и крякает как утка, то оно, должно быть, утка! В реализации run
на Screen
в Листинге 17-5 run
не нужно знать, какой конкретный тип каждого компонента. Он не проверяет, является ли компонент экземпляром Button
или SelectBox
, он просто вызывает метод draw
на компоненте. Задав Box<dyn Draw>
в качестве типа значений в векторе components
, мы определили Screen
так, чтобы он требовал значений, для которых мы можем вызвать метод draw
.
Преимущество использования объектов-примесей и типовой системы Rust для написания кода, похожего на код, использующий duck typing, заключается в том, что мы никогда не должны проверять, реализует ли значение определенный метод во время выполнения или беспокоиться о том, чтобы получить ошибки, если значение не реализует метод, но мы все равно вызываем его. Rust не скомпилирует наш код, если значения не реализуют примеси, которые требуются объектами-примесями.
Например, Листинг 17-10 показывает, что происходит, если мы пытаемся создать Screen
с String
в качестве компонента.
Имя файла: src/main.rs
use gui::Screen;
fn main() {
let screen = Screen {
components: vec![Box::new(String::from("Hi"))],
};
screen.run();
}
Листинг 17-10: Попытка использовать тип, который не реализует примесь объекта-примеси
Мы получим эту ошибку, потому что String
не реализует примесь Draw
:
error[E0277]: the trait bound `String: Draw` is not satisfied
--> src/main.rs:5:26
|
5 | components: vec![Box::new(String::from("Hi"))],
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Draw` is
not implemented for `String`
|
= note: required for the cast to the object type `dyn Draw`
Эта ошибка сообщает нам, что либо мы передаем что-то в Screen
, что не хотели передавать, и поэтому должны передать другой тип, либо мы должны реализовать Draw
для String
, чтобы Screen
смог вызвать draw
для него.