我有一个程序,涉及检查复杂的数据结构,看它是否有任何缺陷。 (这很复杂,所以我发布了示例代码。)所有检查都是彼此无关的,并且都有自己的模块和测试。
更重要的是,每个检查都有自己的错误类型,其中包含有关每个数字检查失败方式的不同信息。我这样做而不是只返回一个错误字符串,所以我可以测试错误(这就是为什么Error
依赖于PartialEq
)。
我有Check
和Error
的特征:
trait Check {
type Error;
fn check_number(&self, number: i32) -> Option<Self::Error>;
}
trait Error: std::fmt::Debug + PartialEq {
fn description(&self) -> String;
}
两个示例检查,带有错误结构。在此示例中,如果数字为负数或偶数,我想显示错误:
#[derive(PartialEq, Debug)]
struct EvenError {
number: i32,
}
struct EvenCheck;
impl Check for EvenCheck {
type Error = EvenError;
fn check_number(&self, number: i32) -> Option<EvenError> {
if number < 0 {
Some(EvenError { number: number })
} else {
None
}
}
}
impl Error for EvenError {
fn description(&self) -> String {
format!("{} is even", self.number)
}
}
#[derive(PartialEq, Debug)]
struct NegativeError {
number: i32,
}
struct NegativeCheck;
impl Check for NegativeCheck {
type Error = NegativeError;
fn check_number(&self, number: i32) -> Option<NegativeError> {
if number < 0 {
Some(NegativeError { number: number })
} else {
None
}
}
}
impl Error for NegativeError {
fn description(&self) -> String {
format!("{} is negative", self.number)
}
}
我知道在这个例子中,两个结构看起来完全相同,但在我的代码中,有许多不同的结构,所以我不能合并它们。最后,一个示例main
函数,来说明我想要做的事情:
fn main() {
let numbers = vec![1, -4, 64, -25];
let checks = vec![
Box::new(EvenCheck) as Box<Check<Error = Error>>,
Box::new(NegativeCheck) as Box<Check<Error = Error>>,
]; // What should I put for this Vec's type?
for number in numbers {
for check in checks {
if let Some(error) = check.check_number(number) {
println!("{:?} - {}", error, error.description())
}
}
}
}
你可以在the Rust playground看到代码。
我最接近解决方案是删除相关类型并让检查返回Option<Box<Error>>
。但是,我得到了这个错误:
error[E0038]: the trait `Error` cannot be made into an object
--> src/main.rs:4:55
|
4 | fn check_number(&self, number: i32) -> Option<Box<Error>>;
| ^^^^^ the trait `Error` cannot be made into an object
|
= note: the trait cannot use `Self` as a type parameter in the supertraits or where-clauses
因为PartialEq
特征中的Error
。到目前为止,Rust对我来说很棒,我真的希望我能够将类型系统弯曲成支持这样的东西!
当你写一个impl Check
并使用具体类型专门化你的type Error
时,你最终会得到不同的类型。
换句话说,Check<Error = NegativeError>
和Check<Error = EvenError>
是静态不同的类型。虽然你可能期望Check<Error>
描述两者,但请注意,在Rust NegativeError
和EvenError
不是Error
的子类型。它们保证实现Error
特性定义的所有方法,但是对这些方法的调用将被静态分派给编译器创建的物理上不同的函数(每个函数都有NegativeError
的版本,EvenError
的版本)。
因此,你不能把它们放在同一个Vec
,甚至盒装(你发现)。知道要分配多少空间并不是一个问题,而是Vec
要求它的类型是同质的(你也不能有vec![1u8, 'a']
,尽管char
可以代表记忆中的u8
)。
正如您所发现的,Rust的“擦除”某些类型信息并获得子类型的动态调度部分的方法是特征对象。
如果你想再尝试一下trait对象的方法,你可能会发现它有一些调整更吸引人......
Error
中使用std::error
特性而不是您自己的版本,您可能会发现它更容易。
你可能需要使用impl Display
来创建一个动态构建的String
的描述,如下所示:
impl fmt::Display for EvenError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} is even", self.number)
}
}
impl Error for EvenError {
fn description(&self) -> &str { "even error" }
}
Check
返回一个特征对象:
trait Check {
fn check_number(&self, number: i32) -> Option<Box<Error>>;
}
你的Vec
现在有一个可表达的类型:
let mut checks: Vec<Box<Check>> = vec![
Box::new(EvenCheck) ,
Box::new(NegativeCheck) ,
];
std::error::Error
最好的部分......
现在你不需要使用PartialEq
来理解抛出的错误。如果您确实需要从特征对象中检索具体的Error
类型,Error
有各种类型的向下和类型检查。
for number in numbers {
for check in &mut checks {
if let Some(error) = check.check_number(number) {
println!("{}", error);
if let Some(s_err)= error.downcast_ref::<EvenError>() {
println!("custom logic for EvenErr: {} - {}", s_err.number, s_err)
}
}
}
}
我建议你做一些重构。
首先,我很确定,Rust中的向量应该是同构的,因此无法为它们提供不同类型的元素。你也不能低估特质以将它们降低到一个共同的基本特征(我记得,在SO上有一个关于它的问题)。
所以我会使用代数类型与此任务的显式匹配,如下所示:
enum Checker {
Even(EvenCheck),
Negative(NegativeCheck),
}
let checks = vec![
Checker::Even(EvenCheck),
Checker::Negative(NegativeCheck),
];
我终于找到了一种方法,我很高兴。不是拥有Box<Check<???>>
对象的向量,而是有一个闭包向量,它们都具有相同的类型,抽象出被调用的函数:
fn main() {
type Probe = Box<Fn(i32) -> Option<Box<Error>>>;
let numbers: Vec<i32> = vec![ 1, -4, 64, -25 ];
let checks = vec![
Box::new(|num| EvenCheck.check_number(num).map(|u| Box::new(u) as Box<Error>)) as Probe,
Box::new(|num| NegativeCheck.check_number(num).map(|u| Box::new(u) as Box<Error>)) as Probe,
];
for number in numbers {
for check in checks.iter() {
if let Some(error) = check(number) {
println!("{}", error.description());
}
}
}
}
这不仅允许返回Box<Error>
对象的向量,它允许Check
对象提供它们自己的Error关联类型,不需要实现PartialEq
。多个as
es看起来有点乱,但总的来说并不是那么糟糕。