Как я могу иметь коллекцию объектов, которые отличаются по их связанному типу?
У меня есть программа, которая включает в себя изучение сложной структуры данных, чтобы увидеть, есть ли у нее какие-либо дефекты. (Это довольно сложно, поэтому я публикую пример кода.) Все проверки не связаны друг с другом, и все они будут иметь свои собственные модули и тесты.
Что еще более важно, каждая проверка имеет свой собственный тип ошибки, который содержит различную информацию о том, как проверка провалилась для каждого номера. Я делаю это таким образом, вместо того, чтобы просто возвращать строку ошибки, чтобы я мог проверить ошибки (именно поэтому 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 type?
for number in numbers {
for check in checks {
if let Some(error) = check.check_number(number) {
println!("{:?} - {}", error, error.description())
}
}
}
}
Вы можете увидеть код на детской площадке 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
из-за PartialEq
в признаке Error
. До сих пор Rust был для меня велик, и я действительно надеюсь, что смогу согнать систему типов в поддержку чего-то подобного!
Ответы
Ответ 1
В конце концов я нашел способ сделать это, и я доволен. Вместо того, чтобы иметь вектор объектов 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
предоставлять свой собственный связанный с ошибкой тип, который не требуется реализовать PartialEq
. Несколько as
es выглядят немного грязно, но в целом это не так уж плохо.
Ответ 2
Когда вы пишете impl Check
и специализируете свой type Error
на конкретном типе, вы получаете разные типы.
Другими словами, Check<Error = NegativeError>
и Check<Error = EvenError>
являются статически разными типами. Хотя вы можете ожидать, что Check<Error>
описывает оба, обратите внимание, что в Rust NegativeError
и EvenError
не являются EvenError
Error
. Они гарантированно реализуют все методы, определенные свойством Error
, но тогда вызовы этих методов будут статически отправляться физически различным функциям, которые создает компилятор (у каждой будет версия для NegativeError
, одна для EvenError
).
Следовательно, вы не можете поместить их в тот же Vec
, даже в штучной упаковке (как вы обнаружили). Это не столько вопрос знания, сколько места нужно выделить, это то, что Vec
требует, чтобы его типы были однородными (у вас также не может быть vec![1u8, 'a']
, хотя char
может быть представлен как u8
в объем памяти).
Простой способ "стереть" некоторую информацию о типе и получить динамическую диспетчеризирующую часть подтипирования - это, как вы обнаружили, черты объектов.
Если вы хотите еще раз попробовать подход к объекту черты, вы можете найти его более привлекательным с помощью нескольких настроек...
-
Возможно, вам будет гораздо проще, если вы используете черту Error
в std::error
вместо своей собственной версии.
Возможно, вам потребуется impl Display
для создания описания с динамически 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
чтобы понять, какая ошибка была PartialEq
. Error
имеет различные типы downcast и проверки типов, если вам нужно извлечь конкретный тип 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)
}
}
}
}
полный пример на детской площадке
Ответ 3
Я бы предложил вам несколько рефакторингов.
Во-первых, я уверен, что векторы должны быть однородными в Rust, поэтому нет возможности поставлять для них элементы разных типов. Кроме того, вы не можете понижать черты, чтобы свести их к общей базовой чертой (как я помню, был вопрос об этом на SO).
Поэтому я бы использовал алгебраический тип с явным соответствием для этой задачи, например:
enum Checker {
Even(EvenCheck),
Negative(NegativeCheck),
}
let checks = vec![
Checker::Even(EvenCheck),
Checker::Negative(NegativeCheck),
];
Что касается обработки ошибок, рассмотрите возможность использования FromError, поэтому вы сможете привлечь try! в вашем коде и для преобразования типов ошибок из одного в другой.