我怎样才能拥有一组因关联类型不同而不同的对象?

How can I have a collection of objects that differ by their associated type?

我有一个程序涉及检查复杂的数据结构以查看它是否有任何缺陷。 (这很复杂,所以我发布了示例代码。)所有检查彼此无关,并且都有自己的模块和测试。

更重要的是,每个检查都有自己的错误类型,其中包含有关每个号码检查失败的不同信息。我正在这样做,而不是仅仅 return 一个错误字符串,这样我就可以测试错误(这就是 Error 依赖 PartialEq 的原因)。

到目前为止我的代码

我有 CheckError 的特征:

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

我试过的解决方案

我最接近的解决方案是删除关联类型并进行检查 return 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

因为 Error 特征中的 PartialEq。到目前为止,Rust 对我来说很棒,我真的希望我能够改变类型系统来支持这样的东西!

我建议你进行一些重构。

首先,我很确定,向量在 Rust 中应该是同质的,所以没有办法为它们提供不同类型的元素。此外,您不能将特征降低为共同的基本特征(我记得,SO 上有一个关于它的问题)。

所以我会使用显式匹配的代数类型来完成这项任务,如下所示:

enum Checker {
    Even(EvenCheck),
    Negative(NegativeCheck),
}

let checks = vec![
    Checker::Even(EvenCheck),
    Checker::Negative(NegativeCheck),
];

至于错误处理,请考虑在您的代码中使用 FromError framework, so you will able to involve try! 宏并将错误类型从一种转换为另一种。

我最终找到了一种让我满意的方法。与其拥有 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 个对象提供它们自己的不需要实现 [=14] 的错误关联类型=].多个as看起来有点乱,但总体来说差。

当您编写 impl Check 并使用具体类型专门化您的 type Error 时,您最终会得到不同的类型。

换句话说,Check<Error = NegativeError>Check<Error = EvenError>是静态不同的类型。尽管您可能希望 Check<Error> 描述两者,但请注意在 Rust 中 NegativeErrorEvenError 不是 Error 子类型 。它们保证实现由 Error 特性定义的所有方法,但随后对这些方法的调用将被静态分派到编译器创建的物理上不同的函数(每个函数都有一个 NegativeError 的版本,一个用于EvenError).

因此,您不能将它们放在同一个 Vec 中,即使是装箱(正如您发现的那样)。知道要分配多少 space 并不是什么大问题,而是 Vec 要求它的类型是同质的(你也不能有 vec![1u8, 'a'],尽管 [=28= =] 在内存中表示为 u8)。

Rust "erase" 一些类型信息并获得子类型的动态调度部分的方法是,正如您发现的那样,特征对象。

如果您想再次尝试特征对象方法,您可能会发现它通过一些调整更有吸引力...

  1. 如果您在 std::error 中使用 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" }
    }
    
  2. 现在您可以删除关联类型并拥有 Check return 特征对象:

    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) ,
    ];
    
  3. 使用 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)                    
                }
            }
        }
    }
    

full example on the playground