简体   繁体   English

如何在 Rust 中没有特征对象的特征的多个实现者之间进行选择?

[英]How can I choose between multiple implementors of a trait without trait objects in Rust?

I have a trait called Dataset which has a single method to retrieve the contents of a file by its name and an associated Error type.我有一个名为Dataset的特征,它有一个方法可以通过文件名和关联的Error类型检索文件的内容。 The trait is implemented for different filesystem structures, eg a PathBuf for directory-based access or a ZipArchive for access in a compressed zip archive.该特征针对不同的文件系统结构实现,例如用于基于目录访问的PathBuf或用于访问压缩 zip 存档的ZipArchive For testing purposes also exists an implementation for HashMap .出于测试目的,还存在HashMap的实现。

trait Dataset {
    type Error: Error + 'static;

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

Now, I want to deduce depending on a given file name which implementation to use: A directory if the path is an directory, a zip archive if the extension is .zip or .bzip , etc. Consider a function fn run(dataset: impl Dataset) which operates on a dataset.现在,我想根据给定的文件名推断要使用哪个实现:如果路径是目录,则为目录;如果扩展名为.zip.bzip ,则为 zip 存档,等等。考虑 function fn run(dataset: impl Dataset)对数据集进行操作。 Having the choosing-code inlined, everything works: (Using a Option<PathBuf> is just for this example, the real code looks at the properties of the given path as described above)内联选择代码后,一切正常:(使用Option<PathBuf>仅用于此示例,实际代码会查看给定路径的属性,如上所述)

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

However, I'd prefer to decouple the functionality of (1) “choose an implementor for Dataset ” from the (2) “give the Dataset to the run function”.但是,我更愿意将 (1)“为Dataset选择一个实现者”的功能与 (2)“将Dataset提供给run函数”分离。 I hope to make this function more reusable (as not only the run function could make use of (1)) and be more independend on changes of run – consider changes to the signature like adding more parameters.我希望使这个 function 更具可重用性(因为不仅run function 可以利用 (1))并且更加独立于run的更改——考虑更改签名,例如添加更多参数。

I tried two approaches:我尝试了两种方法:

  1. Trait objects特征对象

    While I'm not quite happy to use trait objects just for reasons of decoupling, I wouldn't refuse this option ideologically.虽然我不太乐意仅仅出于解耦的原因而使用特征对象,但我不会在意识形态上拒绝这个选项。
     fn with_trait_object(optional: Option<PathBuf>) -> Box<dyn Dataset> { match optional { Some(dataset) => Box::new(dataset), None => Box::new(static_dataset), } }
    But this is rejected with the error message:但这被错误消息拒绝:
     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> { |
    It makes sense to me, as the Dataset::Error type is in a return position and we don't know the exact size of it when using a trait object.这对我来说很有意义,因为Dataset::Error类型在返回值 position 中,我们在使用特征 object 时不知道它的确切大小。
  2. Closure as parameter闭包作为参数

    My second approach is what I would prefer regarding its idea: Instead of returning a Dataset to my caller, the caller gives me a closure which will accept the Dataset .我的第二种方法是我更喜欢的想法:调用者没有将Dataset返回给我的调用者,而是给我一个将接受Dataset的闭包。 Then, the closure can be used to pass additional arguments to the actual processing function. I could also use different closures so I could reuse my “choose the correct implementor” functionality with ease.然后,可以使用闭包将额外的 arguments 传递给实际处理 function。我还可以使用不同的闭包,这样我就可以轻松地重用我的“选择正确的实现者”功能。 However, I can't express “this function takes a closure FnOnce which is itself generic over the Dataset trait in its first parameter”.但是,我无法表达“这个 function 接受一个闭包FnOnce ,它本身在其第一个参数中的Dataset特征上是通用的”。 At the moment, I use Result<(), Box<dyn Error>> as return type for the closure, but it would be nice to be generic there, too.目前,我使用Result<(), Box<dyn Error>>作为闭包的返回类型,但也可以是通用的。 Here the erroneous code:这里是错误的代码:
     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), } }

I am quite curious that it looks like I can't factor out this piece of code as I can't describe the correct signature of a consumer function. Is there a different way to order the type parameters?我很好奇,看起来我无法分解出这段代码,因为我无法描述消费者 function 的正确签名。是否有不同的方式来订购类型参数? Or is there an entirely different approach possible?或者是否有可能采用完全不同的方法?

A small example can be found on Rust Playground .可以在Rust Playground上找到一个小例子。

You could start out with this:你可以从这个开始:

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()),
    }
}

If you want to change the None arm, you would have to create a (generic) method to generate a dataset.如果要更改None arm,则必须创建一个(通用)方法来生成数据集。 You could do so by extending the trait Dataset with a fn static_dataset() -> Self .您可以通过使用fn static_dataset() -> Self扩展特征Dataset来实现。

The reason the None does not work with a closure is that rust (at least as of now) treats closures as accepting fixed parameters. None不适用于闭包的原因是 rust(至少到目前为止)将闭包视为接受固定参数。 If consumer accepts a DS in the first branch, it cannot simply accept HashMap in the second - it still must accept the same type: DS .如果consumer在第一个分支中接受DS ,则它不能在第二个分支中简单地接受HashMap - 它仍然必须接受相同的类型: DS

You could work around this by defining your own trait:您可以通过定义自己的特征来解决这个问题:

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(())
}

That said, it is more cumbersome to use DatasetConsumer than to use plain closures.也就是说,使用DatasetConsumer比使用普通闭包更麻烦。

You could also try to implement with_consumer_function as a macro, but I'm not sure I would follow this path.您也可以尝试将with_consumer_function实现为宏,但我不确定我是否会遵循这条路径。

Also I had a bash at wrestling the compiler into accepting.另外,我有一个 bash 试图让编译器接受。 Here is the Rust Playground这里是Rust游乐场

In but I think @phimuemue has has a pretty succinct response.但我认为@phimuemue 的回应非常简洁。 In my case I added an enum for convenience instead of an associated type.在我的例子中,为了方便我添加了一个枚举而不是关联类型。 And simply within the code avoided the generic by mapping a running closure over the Option .并且只是在代码中通过在Option上映射一个正在运行的闭包来避免泛型。 I am not sure if it fits the criterion of factoring out the match arm, but it feels convenient and avoids dynamic dispatch.我不确定它是否符合分解出匹配arm的标准,但感觉很方便并且避免了动态调度。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM