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