[英]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:我尝试了两种方法:
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 时不知道它的确切大小。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.FnOnce
which is itself generic over the Dataset
trait in its first parameter”.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.