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

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

Возможно ли это в 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();
}

Ответ 1

Да, но не так легко. То, что вы там написали, состоит в том, что 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>. Это похоже на то, что вы добавляете объект к интерфейсу в нечто вроде С# или Java или добавляете Dog* в Barks* в С++.

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

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

Ответ 2

У DK есть хорошее объяснение, я просто перезвоню с примером, где мы выделяем 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 с меньшим количеством служебных сообщений.

Ответ 3

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