Как я могу выбрать между несколькими разработчиками трейта без трейт-объектов в Rust? - PullRequest
0 голосов
/ 03 августа 2020

У меня есть черта под названием Dataset, которая имеет единственный метод для извлечения содержимого файла по его имени и связанному типу Error. Эта характеристика реализована для различных структур файловой системы, например, PathBuf для доступа на основе каталога или ZipArchive для доступа в сжатом zip-архиве. Для целей тестирования также существует реализация для HashMap.

trait Dataset {
    type Error: Error + 'static;

    fn read(&mut self, name: &str) -> Result<String, Self::Error>;
}

Теперь я хочу определить, в зависимости от данного имени файла, какую реализацию использовать: каталог, если путь - это каталог, zip-архив если расширение .zip или .bzip, и т. д. c. Рассмотрим функцию fn run(dataset: impl Dataset), которая работает с набором данных. Имея встроенный код выбора, все работает: (Использование Option<PathBuf> только для этого примера, реальный код смотрит на свойства данного пути, как описано выше)

let optional = Some(PathBuf::from("/path/to/dataset/.txt"));
match optional.clone() {
    Some(dataset) => run(dataset)?,
    None => run(static_dataset())?,
}

Однако я ' Я предпочитаю отделить функциональность (1) «выбрать разработчика для Dataset» от (2) «передать Dataset функции run». Я надеюсь сделать эту функцию более многоразовой (поскольку не только функция run может использовать (1)) и быть более независимой от изменений run - рассмотрите изменения в подписи, такие как добавление дополнительных параметров.

Я пробовал два подхода:

  1. Объекты-черты

    Хотя я не очень рад использовать объекты-черты только по причинам развязки, я бы не стал отказываться от этого варианта идеологически.
    fn with_trait_object(optional: Option<PathBuf>) -> Box<dyn Dataset> {
        match optional {
            Some(dataset) => Box::new(dataset),
            None => Box::new(static_dataset),
        }
    }
    
    Но это отклоняется с сообщением об ошибке:
      error[E0191]: the value of the associated type `Error` (from trait `Dataset`) must be specified
      --> src/main.rs:68:60
       |
    9  |     type Error: Error + 'static;
       |     ---------------------------- `Error` defined here
    ...
    68 | fn with_trait_object(optional: Option<PathBuf>) -> Box<dyn Dataset> {
       | 
    
    Это имеет смысл для меня, поскольку тип Dataset::Error находится в позиции возврата, и мы не знаем его точный размер при использовании объекта-признака.
  2. Закрытие как параметр

    Мой второй подход - это то, что я предпочел бы в отношении его идеи: вместо того, чтобы возвращать Dataset моему вызывающему, вызывающий дает мне закрытие, которое примет Dataset . Затем замыкание можно использовать для передачи дополнительных аргументов фактической функции обработки. Я также мог использовать различные замыкания, чтобы с легкостью повторно использовать свои функции «выбрать правильного разработчика». Однако я не могу express «эта функция принимает закрытие FnOnce, которое само является общим c над чертой Dataset в своем первом параметре». На данный момент я использую Result<(), Box<dyn Error>> как возвращаемый тип для закрытия, но было бы неплохо, если бы и здесь был generi c. Вот ошибочный код:
    fn with_consumer_function(
        optional: Option<PathBuf>,
        consumer: impl FnOnce(impl Dataset) -> Result<(), Box<dyn Error>>,
    ) -> Result<(), Box<dyn Error>> {
        match optional {
            Some(dataset) => consumer(dataset),
            None => consumer(static_dataset),
        }
    }
    

Мне очень любопытно, что похоже, что я не могу вычленить этот фрагмент кода, так как я не могу описать правильную подпись потребительской функции. Есть ли другой способ упорядочить параметры типа? Или возможен совершенно другой подход?

Небольшой пример можно найти на Rust Playground .

Ответы [ 2 ]

2 голосов
/ 03 августа 2020

Вы можете начать с этого:

fn with_consumer_function<DS: Dataset>(
    optional: Option<DS>,
    consumer: impl FnOnce(DS) -> Result<(), Box<dyn Error>>,
) -> Result<(), Box<dyn Error>> {
    match optional {
        Some(dataset) => consumer(dataset),
        None => todo!(), //consumer(static_dataset()),
    }
}

Если вы хотите изменить руку None, вам нужно будет создать метод (generi c) для создания набора данных. Вы можете сделать это, расширив черту Dataset с помощью fn static_dataset() -> Self.

Причина, по которой None не работает с замыканием, заключается в том, что ржавчина (по крайней мере, на данный момент) рассматривает замыкания как принятие исправленных параметры. Если consumer принимает DS в первой ветви, он не может просто принимать HashMap во второй - он все равно должен принимать тот же тип: DS.

Вы можете обойти это, определив ваша собственная черта:

trait DatasetConsumer {
    fn call<DS: Dataset>(self, ds: DS) -> Result<(), Box<dyn Error>>;
}

struct DatasetConsumerRun;
impl DatasetConsumer for DatasetConsumerRun {
    fn call<DS: Dataset>(self, mut dataset: DS) -> Result<(), Box<dyn Error>> {
        println!("{}", dataset.read("info.txt")?);
        Ok(())
    }
}
    
fn with_consumer_function<DS: Dataset>(
    optional: Option<DS>,
    consumer: impl DatasetConsumer,
) -> Result<(), Box<dyn Error>> {
    match optional {
        Some(dataset) => consumer.call(dataset),
        None => consumer.call(static_dataset()),
    }
}

fn main() -> Result<(), Box<dyn Error>> {
    let optional = Some(PathBuf::from("/path/to/dataset/.txt"));
    with_consumer_function(optional.clone(), DatasetConsumerRun)?;
    Ok(())
}

Тем не менее, использовать DatasetConsumer труднее, чем использовать простые замыкания.

Вы также можете попробовать реализовать with_consumer_function как макрос, но я не уверен, что пойду по этому пути.

0 голосов
/ 03 августа 2020

Также у меня было bash при попытке компилятора принять. Вот Rust Playground

, но я думаю, что у @phimuemue довольно лаконичный ответ. В моем случае я для удобства добавил перечисление вместо связанного типа. И просто внутри кода избегал generi c, отображая текущее закрытие поверх Option. Я не уверен, соответствует ли он критерию исключения спички, но он удобен и позволяет избежать динамической c отправки.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...