ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 11.01.2024
Просмотров: 1157
Скачиваний: 5
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
Листинг 17-8: Другой крейт, использующий
gui
и реализующий типаж
Draw
у структуры
SelectBox
Пользователь нашей библиотеки теперь может написать свою функцию main для создания экземпляра
Screen
. К экземпляру
Screen он может добавить
SelectBox и
Button
, поместив каждый из них в
Box
, чтобы он стал типаж-объектом. Затем он может вызвать метод run у экземпляра
Screen
, который вызовет draw для каждого из компонентов. Листинг 17-9 показывает эту реализацию:
Файл: src/main.rs
Листинг 17-9: Использование типаж-объектов для хранения значений разных типов, реализующих один и
тот же типаж
use gui::Draw; struct
SelectBox
{ width: u32
, height: u32
, options:
Vec
<
String
>,
} impl
Draw for
SelectBox { fn draw
(&
self
) {
// code to actually draw a select box
}
} 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();
}
Когда мы писали библиотеку, мы не знали, что кто-то может добавить тип
SelectBox
, но наша реализация
Screen могла работать с новым типом и рисовать его, потому что
SelectBox реализует типаж
Draw
, что означает, что он реализует метод draw
Эта концепция, касающаяся только сообщений на которые значение отвечает, в отличии от конкретного тип у значения, аналогична концепции duck typing в динамически типизированных языках: если что-то ходит как утка и крякает как утка, то она должна быть утка! В реализации метода run у
Screen в листинге 17-5, run не нужно знать каким будет конкретный тип каждого компонента. Он не проверяет, является ли компонент экземпляром
Button или
SelectBox
, он просто вызывает метод draw компонента.
Указав
Box
в качестве типа значений в векторе components
, мы определили
Screen для значений у которых мы можем вызвать метод draw
Преимущество использования типаж-объектов и системы типов Rust для написания кода,
похожего на код с использованием концепции duck typing состоит в том, что нам не нужно во время выполнения проверять реализует ли значение в векторе конкретный метод или беспокоиться о получении ошибок, если значение не реализует метод, мы все равно вызываем метод. Rust не скомпилирует наш код, если значения не реализуют типаж, который нужен типаж-объектам.
Например, листинг 17-10 демонстрирует, что случится если мы попытаемся добавить
Screen{/code0 с
String в качестве компонента вектора:
Файл: src/main.rs
Листинг 17-10: Попытка использования типа, который не реализует типаж для типаж-объекта
Мы получим ошибку, потому что
String не реализует типаж
Draw
:
use gui::Screen; fn main
() { let screen = Screen { components: vec!
[
Box
::new(
String
::from(
"Hi"
))],
}; screen.run();
}
Эта ошибка даёт понять, что либо мы передаём в компонент
Screen что-то, что мы не собирались передавать и мы тогда должны передать другой тип, либо мы должны реализовать типаж
Draw у типа
String
, чтобы
Screen мог вызывать draw у него.
Типаж-объекты выполняют динамическую диспетчеризацию
(связывание)
Напомним, в разделе
«Производительность кода с использованием обобщений»
главы
10 обсуждается процесс мономорфизации выполняемый компилятором, когда мы используем ограничения типажей для обобщённых типов: компилятор генерирует конкретные реализации функций и методов для каждого конкретного типа, который мы используем вместо параметра обобщённого типа. Код, полученный в результате мономорфизации, выполняет статическую диспетчеризацию, когда компилятор знает какой метод вы вызываете во время компиляции. Это противоположно подходу
динамической диспетчеризации, когда компилятор не может сказать во время компиляции, какой метод вы вызываете. В случаях динамической диспетчеризации компилятор генерирует код, который во время выполнения определяет, какой метод необходимо вызывать.
Когда мы используем типаж-объекты, Rust должен использовать динамическую диспетчеризацию. Компилятор не знает всех типов, которые могут быть использованы с кодом, использующим типаж-объекты, поэтому он не знает, какой метод реализован для какого типа при вызове. Вместо этого, во время выполнения, Rust использует указатели внутри типаж-объекта, чтобы узнать какой метод вызвать. Такой поиск вызывает дополнительные затраты во время исполнения, которые не требуются при статической диспетчеризации. Динамическая диспетчеризация также не позволяет компилятору выбрать встраивание кода метода, что в свою очередь делает невозможными некоторые оптимизации. Однако мы получили дополнительную гибкость в коде, который мы написали в листинге 17-5, и которую смогли поддержать в листинге 17-9, поэтому все "за"
и "против" нужно рассматривать в комплексе.
$
cargo run
Compiling gui v0.1.0 (file:///projects/gui) 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`
|
= help: the trait `Draw` is implemented for `Button`
= note: required for the cast to the object type `dyn Draw`
For more information about this error, try `rustc --explain E0277`. error: could not compile `gui` due to previous error
Реализация объектно-ориентированного шаблона
проектирования
Шаблон "Состояние" — это объектно-ориентированный шаблон проектирования. Суть шаблона заключается в том, что мы определяем набор состояний, которые может иметь внутреннее значение. Состояния представлены набором объектов состояния, а поведение элемента изменяется в зависимости от его состояния. Мы рассмотрим пример структуры записи в блоге, в которой есть поле для хранения состояния, которое будет объектом состояния из набора «черновик», «обзор» или «опубликовано».
Объекты состояния имеют общую функциональность: конечно в Rust мы используем структуры и типажи, а не объекты и наследование. Каждый объект состояния отвечает за своё поведение и сам определяет, когда он должен перейти в другое состояние. Элемент,
который содержит объект состояния, ничего не знает о различиях в поведении состояний или о том, когда одно состояние должно перейти в другое.
Преимуществом шаблона "Состояние" является то, что при изменении требований заказчика программы не требуется изменять код элемента, содержащего состояние, или код, использующий такой элемент. Нам нужно только обновить код внутри одного из объектов состояния, чтобы изменить его порядок действий, либо, возможно, добавить больше объектов состояния.
Для начала реализуем шаблон "Состояние" более традиционным объектно- ориентированным способом, а затем воспользуемся подходом, более естественным для
Rust. Давайте шаг за шагом реализуем поток действий для записи в блоге, использующий шаблон "Состояние".
Окончательный функционал будет выглядеть так:
1. Запись в блоге создаётся как пустой черновик.
2. Когда черновик готов, запрашивается его проверка.
3. После проверки происходит публикация записи.
4. Только опубликованные записи блога возвращают содержимое записи на печать,
поэтому сообщения, не прошедшие проверку, не могут быть опубликованы случайно.
Любые другие изменения, сделанные в записи, не должны иметь никакого эффекта.
Например, если мы попытаемся подтвердить черновик записи в блоге до того, как запросим проверку, запись должна остаться неопубликованным черновиком.
В листинге 17-11 показан этот поток действий в виде кода: это пример использования
API, который мы собираемся реализовать в библиотеке (крейте) с именем blog
. Он пока не компилируется, потому что крейт blog ещё не создан.
Файл: src/main.rs
Листинг 17-11: Код, демонстрирующий желаемое поведение, которое мы хотим получить в крейте
blog
Мы хотим, чтобы пользователь мог создать новый черновик записи в блоге с помощью
Post::new
. Затем мы хотим разрешить добавление текста в запись блога. Если мы попытаемся получить содержимое записи сразу, до её проверки, мы не должны получить никакого текста на выходе, потому что запись все ещё является черновиком. Мы добавили утверждение (
assert_eq!
) в коде для демонстрационных целей. Утверждение
(assertion), что черновик записи блога должен возвращать пустую строку из метода content было бы отличным модульным тестом, но мы не собираемся писать тесты для этого примера.
Далее мы хотим разрешить сделать запрос на проверку записи и хотим, чтобы content возвращал пустую строку, пока проверки не завершена. Когда запись пройдёт проверку,
она должна быть опубликована, то есть при вызове content будет возвращён текст записи.
Обратите внимание, что единственный тип из крейта, с которым мы взаимодействуем - это тип
Post
. Этот тип будет использовать шаблон "Состояние" и будет содержать значение, которое будет являться одним из трёх объектов состояний, представляющих различные состояния, в которых может находиться запись: "черновик", "ожидание проверки" или "опубликовано". Управление переходом из одного состояния в другое будет осуществляться внутренней логикой типа
Post
. Состояния будут переключаться в результате реакции на вызов методов экземпляра
Post пользователями нашей библиотеки, но пользователи не должны управлять изменениями состояния напрямую.
Кроме того, пользователи не должны иметь возможность ошибиться с состояниями,
например, опубликовать сообщение до его проверки.
1 ... 43 44 45 46 47 48 49 50 ... 62
Определение Post и создание нового экземпляра в состоянии
черновика
Приступим к реализации библиотеки! Мы знаем, что нам нужна публичная структура
Post
, хранящая некоторое содержимое, поэтому мы начнём с определения структуры и use blog::Post; fn main
() { let mut post = Post::new(); post.add_text(
"I ate a salad for lunch today"
); assert_eq!
(
""
, post.content()); post.request_review(); assert_eq!
(
""
, post.content()); post.approve(); assert_eq!
(
"I ate a salad for lunch today"
, post.content());
}
связанной с ней публичной функцией new для создания экземпляра
Post
, как показано в листинге 17-12. Мы также сделаем приватный типаж
State
, который будет определять поведение, которое должны будут иметь все объекты состояний структуры
Post
Затем
Post будет содержать типаж-объект
Box
внутри
Option
в закрытом поле state для хранения объекта состояния. Чуть позже вы поймёте, зачем нужно использовать
Option
Файл: src/lib.rs
Листинг 17-12. Определение структуры
Post
и функции
new
, которая создаёт новый экземпляр
Post
,
типажа
State
и структуры
Draft
Типаж
State определяет поведение, совместно используемое различными состояниями поста. Все объекты состояний (
Draft
- "черновик",
PendingReview
- "ожидание проверки"
и
Published
- "опубликовано") будут реализовывать типаж
State
. Пока у этого типажа нет никаких методов, и мы начнём с определения состояния
Draft
, просто потому, что это первое состояние, с которого, как мы хотим, публикация будет начинать свой путь.
Когда мы создаём новый экземпляр
Post
, мы устанавливаем его поле state в значение
Some
, содержащее
Box
. Этот
Box указывает на новый экземпляр структуры
Draft
. Это гарантирует, что всякий раз, когда мы создаём новый экземпляр
Post
, он появляется как черновик. Поскольку поле state в структуре
Post является приватным, нет никакого способа создать
Post в каком-либо другом состоянии! В функции
Post::new мы инициализируем поле content новой пустой строкой типа
String
Хранение текста содержимого записи
pub struct
Post
{ state:
Option
<
Box
<
dyn
State>>, content:
String
,
} impl
Post { pub fn new
() -> Post {
Post { state:
Some
(
Box
::new(Draft {})), content:
String
::new(),
}
}
} trait
State
{} struct
Draft
{} impl
State for
Draft {}
Post
, как показано в листинге 17-12. Мы также сделаем приватный типаж
State
, который будет определять поведение, которое должны будут иметь все объекты состояний структуры
Post
Затем
Post будет содержать типаж-объект
Box
внутри
Option
в закрытом поле state для хранения объекта состояния. Чуть позже вы поймёте, зачем нужно использовать
Option
Файл: src/lib.rs
Листинг 17-12. Определение структуры
Post
и функции
new
, которая создаёт новый экземпляр
Post
,
типажа
State
и структуры
Draft
Типаж
State определяет поведение, совместно используемое различными состояниями поста. Все объекты состояний (
Draft
- "черновик",
PendingReview
- "ожидание проверки"
и
Published
- "опубликовано") будут реализовывать типаж
State
. Пока у этого типажа нет никаких методов, и мы начнём с определения состояния
Draft
, просто потому, что это первое состояние, с которого, как мы хотим, публикация будет начинать свой путь.
Когда мы создаём новый экземпляр
Post
, мы устанавливаем его поле state в значение
Some
, содержащее
Box
. Этот
Box указывает на новый экземпляр структуры
Draft
. Это гарантирует, что всякий раз, когда мы создаём новый экземпляр
Post
, он появляется как черновик. Поскольку поле state в структуре
Post является приватным, нет никакого способа создать
Post в каком-либо другом состоянии! В функции
Post::new мы инициализируем поле content новой пустой строкой типа
String
Хранение текста содержимого записи
pub struct
Post
{ state:
Option
<
Box
<
dyn
State>>, content:
String
,
} impl
Post { pub fn new
() -> Post {
Post { state:
Some
(
Box
::new(Draft {})), content:
String
::new(),
}
}
} trait
State
{} struct
Draft
{} impl
State for
Draft {}