Эта ошибка даёт понять, что либо мы передаём в компонент
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 {}
В листинге 17-11 показано, что мы хотим иметь возможность вызывать метод add_text и
передать ему
&str
, которое добавляется к текстовому содержимому записи блога. Мы реализуем эту возможность как метод, а не делаем поле content публично доступным,
используя pub
. Это означает, что позже мы сможем написать метод, который будет контролировать, как именно читаются данные из поля content
. Метод add_text довольно прост, поэтому давайте добавим его реализацию в блок impl Post листинга 17-
13 :
Файл: src/lib.rs
Листинг 17-13. Реализация
add_text
для добавления текста к
content
(содержимому записи)
Метод add_text принимает изменяемую ссылку на self
, потому что мы меняем экземпляр
Post
, для которого вызываем add_text
. Затем мы вызываем push_str для
String у поля content и передаём text аргументом для добавления к сохранённому content
. Это поведение не зависит от состояния, в котором находится запись, таким образом оно не является частью шаблона "Состояние". Метод add_text вообще не взаимодействует с полем state
, но это часть поведения, которое мы хотим поддерживать.
Убедимся, что содержание черновика будет пустым
Даже после того, как мы вызвали add_text и добавили некоторый контент в нашу запись, мы хотим, чтобы метод content возвращал пустой фрагмент строки, так как запись всё ещё находится в черновом состоянии, как это показано в строке 7 листинга
17-11. А пока давайте реализуем метод content наиболее простым способом, который будет удовлетворять этому требованию: будем всегда возвращать пустой фрагмент строки. Мы изменим код позже, как только реализуем возможность изменить состояние записи, чтобы она могла бы быть опубликована. Пока что записи могут находиться только в черновом состоянии, поэтому содержимое записи всегда должно быть пустым.
Листинг 17-14 показывает такую реализацию-заглушку:
Файл: src/lib.rs impl
Post {
// --snip-- pub fn add_text
(&
mut self
, text: &
str
) { self
.content.push_str(text);
}
}
Листинг 17-14. Добавление реализации-заглушки для метода
content
в
Post
, которая всегда возвращает
пустой фрагмент строки.
С добавленным таким образом методом content всё в листинге 17-11 работает, как задумано, вплоть до строки 7.
Запрос на проверку записи меняет её состояние
Далее нам нужно добавить функциональность для запроса проверки записи, который должен изменить её состояние с
Draft на
PendingReview
. Листинг 17-15 показывает такой код:
Файл: src/lib.rs
Листинг 17-15. Реализация методов
request_review
в структуре
Post
и типаже
State impl
Post {
// --snip-- pub fn content
(&
self
) -> &
str
{
""
}
} impl
Post {
// --snip-- pub fn request_review
(&
mut self
) { if let
Some
(s) = self
.state.take() { self
.state =
Some
(s.request_review())
}
}
} trait
State
{ fn request_review
(
self
:
Box
<
Self
>) ->
Box
<
dyn
State>;
} struct
Draft
{} impl
State for
Draft { fn request_review
(
self
:
Box
<
Self
>) ->
Box
<
dyn
State> {
Box
::new(PendingReview {})
}
} struct
PendingReview
{} impl
State for
PendingReview { fn request_review
(
self
:
Box
<
Self
>) ->
Box
<
dyn
State> { self
}
}