Возможно ли иметь переменную, содержащую что-то, соответствующее признаку с универсальным признаком, без необходимости указания универсального типа?
Общей чертой интерфейсов и полиморфизма в других языках, таких как Java и JavaScript, является возможность обрабатывать экземпляр только на основе реализуемого им интерфейса. В следующем коде я создал признак Site
и фабрику make_site
для создания экземпляра, реализующего признак Site
. С чертой Site
связан общий шаблон, но код, который обрабатывает Site
, может обрабатывать E
непрозрачно — ему просто нужно отслеживать его и передавать обратно другим функциям того же экземпляра Site
.
pub struct CrawlResult<E> {
pub discovered_queue_entries: Vec<E>,
pub result: Option<String>,
}
pub trait Site<E> {
fn start_queue(&self) -> Vec<E>;
fn crawl(&self, queue_entry: &E) -> CrawlResult<E>;
}
fn make_site<E>() -> Box<dyn Site<E>> {
if rand::random() {
Box::new(SiteA { add: 100 })
} else {
Box::new(SiteB { sub: 1 })
}
}
fn main() {
let site = make_site();
process(&site);
}
Я опустил часть вспомогательного кода, чтобы попытаться выделить части, которые кажутся наиболее важными, но есть полностью воспроизводимый пример на игровой площадке Rust.
Приведенный выше код не может скомпилироваться в функции make_site
.
error[E0277]: the trait bound `SiteA: Site<E>` is not satisfied
--> src/main.rs:103:9
|
103 | Box::new(SiteA { add: 100 })
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Site<E>` is not implemented for `SiteA`
|
= note: required for the cast from `Box<SiteA>` to `Box<(dyn Site<E> + 'static)>`
help: consider introducing a `where` clause, but there might be an alternative better way to express this requirement
|
101 | fn make_site<E>() -> Box<dyn Site<E>> where SiteA: Site<E> {
| ++++++++++++++++++++
error[E0277]: the trait bound `SiteB: Site<E>` is not satisfied
--> src/main.rs:105:9
|
105 | Box::new(SiteB { sub: 1 })
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Site<E>` is not implemented for `SiteB`
|
= note: required for the cast from `Box<SiteB>` to `Box<(dyn Site<E> + 'static)>`
help: consider introducing a `where` clause, but there might be an alternative better way to express this requirement
|
101 | fn make_site<E>() -> Box<dyn Site<E>> where SiteB: Site<E> {
| ++++++++++++++++++++
error[E0277]: the trait bound `Box<dyn Site<_>>: Site<_>` is not satisfied
--> src/main.rs:112:13
|
112 | process(&site);
| ^^^^^ the trait `Site<_>` is not implemented for `Box<dyn Site<_>>`
|
= help: the following other types implement trait `Site<E>`:
<SiteA as Site<SiteAEntry>>
<SiteB as Site<SiteBEntry>>
= note: required for the cast from `&Box<dyn Site<_>>` to `&dyn Site<_>`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` (bin "playground") due to 3 previous errors
Этот код предназначен для поддержки веб-сканера, и идея состоит в том, что у каждого сайта будет своя стратегия сканирования. E
обычно представляет собой тип перечисления, который описывает запись в очереди сканирования и может отличаться в зависимости от сканируемого сайта. Кажется, что большинство проблем возникает из-за универсального типа записи сканирования, но было бы просто невозможно определить реализацию Site
, если бы у нас не было этого универсального типа.
Можно ли перевести эти устоявшиеся шаблоны на Rust или их просто невозможно реализовать в Rust?
Это правда, но реализовать этот шаблон в Java также может быть невозможно. В последнее время мой опыт больше связан с динамическими языками, где такой контракт, как «этот универсальный код будет согласован при работе с этим API», гарантируется программистом, а не компилятором. Нечто подобное было бы тривиально реализовать на JavaScript или Python.
Возможно ли иметь переменную, содержащую что-то, соответствующее признаку с универсальным признаком, без необходимости указания универсального типа?
Только если тип реализует какую-то особенность, которую вы можете указать, и все ветки используют один и тот же тип. В вашем случае ни то, ни другое не соответствует действительности.
fn make_site<E>() -> Box<dyn Site<E>>
Эта подпись позволяет вызывающему абоненту решить, что такое E
. Что будет, если позвонят make_site::<String>()
? Вам нужен способ, чтобы сама функция могла сказать, что такое E
. Вы можете сделать это без указания внутреннего типа, вернув Box<dyn Site<impl SiteEntry>>
и убедившись, что универсальный тип реализует SiteEntry
. Вы также можете удалить общий тип E
из функции.
Обратите внимание, что это ограничит то, что вызывающая сторона может делать с типом, только тем, что предоставляет SiteEntry
. Давайте попробуем это с пустым признаком, чтобы посмотреть, работает ли это:
trait SiteEntry {}
impl SiteEntry for SiteAEntry {}
impl SiteEntry for SiteBEntry {}
fn make_site() -> Box<dyn Site<impl SiteEntry>> {
// ...
Мы обменяли одну ошибку на другую. Теперь вместо этого мы получаем следующее:
error[E0277]: the trait bound `SiteB: Site<SiteAEntry>` is not satisfied
--> src/main.rs:109:9
|
109 | Box::new(SiteB { sub: 1 })
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Site<SiteAEntry>` is not implemented for `SiteB`
|
= help: the trait `Site<SiteBEntry>` is implemented for `SiteB`
= help: for that trait implementation, expected `SiteBEntry`, found `SiteAEntry`
= note: required for the cast from `Box<SiteB>` to `Box<(dyn Site<SiteAEntry> + 'static)>`
Теперь мы сталкиваемся со вторым ограничением, о котором я упоминал выше: все ветки должны возвращать один и тот же тип. Здесь у нас есть первая ветвь, возвращающая Box<dyn Site<SiteAEntry>>
, и это совершенно нормально, но затем компилятор делает вывод, что другая ветвь также должна вернуть Box<dyn Site<SiteAEntry>>
, и справедливо жалуется, что SiteB
не реализует Site<SiteAEntry>
, поэтому приведение невозможно.
Ваши возможности здесь несколько ограничены. Одним из вариантов было бы создать версию Site
со стертым типом, которая не имеет универсального аргумента и работает исключительно с объектами типажей:
trait ErasedSite {
fn start_queue(&self) -> Vec<Box<dyn SiteEntry>>;
fn crawl(&self, queue_entry: &dyn SiteEntry) -> CrawlResult<Box<dyn SiteEntry>>;
}
Тогда ваша функция make_site
сможет вернуть Box<dyn ErasedSite>
(при условии, что вы создадите соответствующий новый тип с реализацией ErasedSite
).
Этот подход потребует SiteEntry
реализации метода, позволяющего определить тип вашей записи, а также, возможно, других методов.
То, что вы ищете, возможно в Rust, но вполне вероятно, что этот конкретный подход на самом деле может оказаться не тем, что вам нужно. Например, Site
, вероятно, не должен быть универсальным, а вместо этого должен позволять реализации определять тип записи, используя связанные типы:
trait Site {
type Entry;
fn start_queue(&self) -> Vec<Self::Entry>;
fn crawl(&self, queue_entry: &Self::Entry) -> CrawlResult<Self::Entry>;
}
Обычно это предпочтительнее, если только вы не предполагаете, что один тип реализует Site<E>
несколько раз с разными типами для E
.
Обратите внимание, что это в конечном итоге не решает проблему, с которой вы столкнулись, а просто перемещает ее - когда вы говорите dyn SomeTrait
, вам необходимо включить связанные типы SomeTrait
, поэтому вам нужно будет сказать Box<dyn Site<Entry = impl SiteEntry>>
, и тогда вы вернетесь туда, где вы начал.
В конечном итоге у вас действительно есть два варианта:
SiteEntry
. (Это динамическая диспетчеризация во время выполнения.)Что вы выберете, может зависеть от вкуса, характеристик производительности или простоты модели программирования.
Даже в Java нельзя считать, что первый интерфейс с объявленной функцией, возвращающей список целых чисел, и второй интерфейс с той же объявленной функцией, но возвращающий список строк, эквивалентны. Если бы это было так, что можно было бы ожидать при вызове такой функции? целые числа или строки?