如何使用相关类型的对象向量?

Ben*_*n S 11 collections types rust

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

更重要的是,每个检查都有自己的错误类型,其中包含有关每个数字检查失败方式的不同信息.我这样做而不是只返回一个错误字符串,所以我可以测试错误(这就是为什么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;
}
Run Code Online (Sandbox Code Playgroud)

两个示例检查,带有错误结构.在此示例中,如果数字为负数或偶数,我想显示错误:


#[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)
    }
}
Run Code Online (Sandbox Code Playgroud)

我知道在这个例子中,两个结构看起来完全相同,但在我的代码中,有许多不同的结构,所以我不能合并它们.最后,一个示例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())
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

您可以在Rust游乐场中看到代码.

我试过的解决方案

我最接近解决方案是删除相关类型并返回检查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
Run Code Online (Sandbox Code Playgroud)

因为PartialEqError特质.到目前为止,Rust对我来说很棒,我真的希望我能够将类型系统弯曲成支持这样的东西!

Pao*_*lla 7

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

换句话说,Check<Error = NegativeError>Check<Error = EvenError>是静态不同的类型。虽然你可能期望Check<Error>描述两,请注意,锈NegativeErrorEvenError子类型Error。它们保证实现Errortrait定义的所有方法,但是对这些方法的调用将被静态分派到编译器创建的物理上不同的函数(每个函数都有一个 for NegativeError,一个 forEvenError)。

因此,您不能将它们放在同一个Vec甚至装箱中(如您所见)。与其说是知道要分配多少空间,不如说是Vec要求其类型是同类的(您也不能拥有 a vec![1u8, 'a'],尽管 achar可以表示为 au8内存中的)。

正如您所发现的,Rust 用于“擦除”一些类型信息并获得子类型的动态分派部分的方法是 trait 对象。

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

  1. 如果您使用Errortraitstd::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" }
    }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 现在你可以删除关联的类型并Check返回一个 trait 对象:

    trait Check  {
        fn check_number(&self, number: i32) -> Option<Box<Error>>;
    }
    
    Run Code Online (Sandbox Code Playgroud)

    Vec现在有一个可表达的类型:

    let mut checks: Vec<Box<Check>> = vec![
        Box::new(EvenCheck) ,
        Box::new(NegativeCheck) ,
    ];
    
    Run Code Online (Sandbox Code Playgroud)
  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)                    
                }
            }
        }
    }
    
    Run Code Online (Sandbox Code Playgroud)

操场上的完整示例


Ben*_*n S 2

我最终找到了一种令我满意的方法。不是拥有一个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());
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Box<Error>这不仅允许返回对象向量,还允许Check对象提供自己的 Error 关联类型,而无需实现PartialEq. 多个ases看起来有点乱,但总体来说还不错