Разрешены ли полиморфные переменные?

У меня есть различные структуры, которые реализуют одну и ту же черту. Я хочу выполнить ветвление при некоторых условиях, решая во время выполнения, какую из этих структур создать. Затем, независимо от того, за какой ветвью я следовал, я хочу вызывать методы из этой черты.

Возможно ли это в Rust? Я надеюсь достичь чего-то вроде следующего (который не компилируется):

trait Barks {
    fn bark(&self);
}

struct Dog;

impl Barks for Dog {
    fn bark(&self) {
        println!("Yip.");
    }
}

struct Wolf;

impl Barks for Wolf {
    fn bark(&self) {
        println!("WOOF!");
    }
}

fn main() {
    let animal: Barks;
    if 1 == 2 {
        animal = Dog;
    } else {
        animal = Wolf;
    }
    animal.bark();
}

3 ответа

Решение

Да, но не так легко. Что вы там написали animal должна быть переменной типа Barks, но Barks это черта; описание интерфейса. Черты не имеют статически определенного размера, так как может возникнуть тип любого размера и impl Barks, Компилятор понятия не имеет, насколько большой сделать animal,

Что вам нужно сделать, это добавить слой косвенности. В этом случае вы можете использовать Box, хотя вы также можете использовать такие вещи, как Rc или простые ссылки:

fn main() {
    let animal: Box<Barks>;
    if 1 == 2 {
        animal = Box::new(Dog);
    } else {
        animal = Box::new(Wolf);
    }
    animal.bark();
}

Здесь я размещаю Dog или же Wolf на кучу, а затем приведение к Box<Barks>, Это похоже на приведение объекта к интерфейсу в C# или Java, или приведение Dog* к Barks* в C++.

Совершенно другой подход, который вы могли бы также использовать, был бы перечислениями. Вы могли бы иметь enum Animal { Dog, Wolf } затем определить impl Animal { fn bark(&self) { ... } }, Зависит от того, нужен ли вам полностью открытый набор животных и / или несколько черт характера.

Наконец, обратите внимание, что "вид" выше. Существуют разные вещи, которые не работают так, как в Java/C#/C++. Например, у Rust нет downcasting (вы не можете перейти от Box<Barks> вернуться к Box<Dog> или от одной черты к другой). Кроме того, это работает только в том случае, если признак "объектобезопасен" (нет обобщений, не используется self или же Self по значению).

У ДК есть хорошее объяснение, я просто приведу пример, где мы выделяем Dog или же Wolf в стеке, избегая выделения кучи:

fn main() {
    let dog;
    let wolf;
    let animal: &Barks;
    if 1 == 2 {
        dog = Dog;
        animal = &dog;
    } else {
        wolf = Wolf;
        animal = &wolf;
    }
    animal.bark();
}

Это немного некрасиво, но ссылки делают то же самое, что и Box с чуть-чуть меньше над головой.

Определение пользовательского перечисления является наиболее эффективным способом сделать это. Это позволит вам выделить в стеке именно столько места, сколько вам нужно, то есть размер наибольшего параметра, плюс 1 дополнительный байт, чтобы отслеживать, какой параметр хранится. Это также позволяет прямой доступ без уровня косвенности, в отличие от решений, использующих Box или признак черты.

К сожалению, это требует больше котла:

enum WolfOrDog {
    IsDog(Dog),
    IsWolf(Wolf)
}
use WolfOrDog::*;

impl Barks for WolfOrDog {
    fn bark(&self) {
        match *self {
            IsDog(ref d) => d.bark(),
            IsWolf(ref w) => w.bark()
        }
    }
}

fn main() {
    let animal: WolfOrDog;
    if 1 == 2 {
        animal = IsDog(Dog);
    } else {
        animal = IsWolf(Wolf);
    }
    animal.bark();
}

В main мы используем только одну выделенную переменную стека, содержащую экземпляр нашего пользовательского перечисления.

Другие вопросы по тегам