Разрешены ли полиморфные переменные?
У меня есть разные структуры, которые реализуют один и тот же признак. Я хочу разветкиться на каком-то условии, решив во время выполнения, какую из этих структур создать. Затем, независимо от того, в какой ветке я следил, я хочу вызвать методы из этой черты.
Возможно ли это в 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
мы используем только одну распределенную стек, содержащую экземпляр нашего пользовательского перечисления.