Как я могу иметь коллекцию объектов, которые отличаются по их связанному типу?

У меня есть программа, которая включает в себя изучение сложной структуры данных, чтобы увидеть, есть ли у нее какие-либо дефекты. (Это довольно сложно, поэтому я публикую пример кода.) Все проверки не связаны друг с другом, и все они будут иметь свои собственные модули и тесты.

Что еще более важно, каждая проверка имеет свой собственный тип ошибки, который содержит различную информацию о том, как проверка провалилась для каждого номера. Я делаю это таким образом, вместо того, чтобы просто возвращать строку ошибки, чтобы я мог проверить ошибки (именно поэтому 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 в объем памяти).

Простой способ "стереть" некоторую информацию о типе и получить динамическую диспетчеризирующую часть подтипирования - это, как вы обнаружили, черты объектов.

Если вы хотите еще раз попробовать подход к объекту черты, вы можете найти его более привлекательным с помощью нескольких настроек...

  1. Возможно, вам будет гораздо проще, если вы используете черту 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" }
    }
    
  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 чтобы понять, какая ошибка была 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! в вашем коде и для преобразования типов ошибок из одного в другой.