【问题标题】:How can I have a collection of objects that differ by their associated type?我怎样才能拥有一组因关联类型而异的对象?
【发布时间】:2015-03-08 22:03:15
【问题描述】:

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

更重要的是,每个检查都有自己的错误类型,其中包含有关每个数字的检查如何失败的不同信息。我正在这样做,而不是仅仅返回一个错误字符串,以便我可以测试错误(这就是为什么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看到代码。

我尝试过的解决方案

我最接近解决方案的是删除关联类型并让检查返回Option&lt;Box&lt;Error&gt;&gt;。但是,我得到了这个错误:

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 对我来说一直很棒,我真的希望我能够将类型系统弯曲成支持这样的东西!

【问题讨论】:

    标签: collections types rust


    【解决方案1】:

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

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

    因此,您不能将它们放在同一个Vec 中,即使是装箱的(如您所见)。这不是知道要分配多少空间的问题,而是Vec 要求它的类型是同质的(你也不能有vec![1u8, 'a'],尽管char 可以表示为u8记忆)。

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

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

    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 返回一个特征对象:

      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 具有各种类型的向下转换和类型检查,如果您确实需要从 trait 对象中检索具体的 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

    【讨论】:

    • 三年后我最终回到了这个问题,你的回答被证明非常有用!谢谢。
    【解决方案2】:

    我最终找到了一种令我满意的方法。与其拥有Box&lt;Check&lt;???&gt;&gt; 对象的向量,不如拥有一个具有相同类型的闭包向量,抽象出被调用的函数:

    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&lt;Error&gt; 对象的向量,还允许Check 对象提供自己的错误关联类型,而无需实现PartialEq。多个ases 看起来有点乱,但总体来说还不错。

    【讨论】:

      【解决方案3】:

      我建议你进行一些重构。

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

      所以我会为此任务使用具有显式匹配的代数类型,如下所示:

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

      至于错误处理,请考虑使用FromError 框架,这样您就可以在代码中包含try! 宏并将错误类型从一种转换为另一种。

      【讨论】:

      • 您好,感谢您的建议。你是正确的,矢量在 Rust 中是同质的,但是可以将元素转换为相同的类型(例如 Box 特征对象)以使它们都适合。我考虑创建一个新的enum,其中包含所有可能的检查器,但我计划拥有数十个这样的检查器,每个检查器都有很多行代码,并且代码很快就会变得重复和笨拙。基本上,我想要一种使用特征对象的方法来为我编写所有这些代码!
      • 我不相信,在您的情况下,有一种方法可以将所有元素转换为相同的类型。试想一下,如果您是编译器,那么您会为循环中的check.check_number(number) 结果分配多少堆栈空间?它绝对应该是一个恒定大小的东西,所以我们又回到了联合类型(枚举),或者指针,就像你的 Option&lt;Box&lt;Check&gt;&gt; 版本。因此,也许您应该考虑摆脱 PartialEq impl 以使其正常工作。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2019-09-18
      • 2011-01-07
      • 1970-01-01
      • 1970-01-01
      • 2016-08-28
      • 1970-01-01
      • 2013-04-06
      相关资源
      最近更新 更多