问题
When one already knows all the finite number of types involved in some code which needs dynamic polymorphism, using enum
can be better for performances compared to using Box
since the latter uses dynamic memory allocation and you'll need to use trait objects which have virtual function call as well.
That said, compared to the equivalent code in C++ using std::variant
and std::visit
, looks like Rust in this scenario has more boilerplate coding involved, at least for me (I have not yet learned to use procedural macros). Making some example here: I have a bunch of struct
types:
struct A {
...
}
struct B {
...
}
...
struct Z {
...
}
They all implement the trait AlphabetLetter
which is:
trait AlphabetLetter {
fn some_function(&self);
}
Since the set of types involved is known and limited, I want to use enum
:
enum Letter {
AVariant(A),
BVariant(B),
...
ZVariant(Z),
}
Already here we have the first boilerplate: I need to add a name for the enum
value for every type variant involved. But the real issue is: enum
Letter
is itself an AlphabetLetter
, it just represent the fact that we do not know at runtime which letter it is. So I started implementing the trait for it:
impl AlphabetLetter for Letter {
fn some_function(&self) {
match self {
Letter::AVariant(letter) => letter.some_function();
Letter::BVariant(letter) => letter.some_function();
...
Letter::ZVariant(letter) => letter.some_function();
}
}
}
And yes, this can become easily a lot of code, but I found no other way of doing it. In C++, thanks to generic lambdas, one can just std::visit
a std::variant
and it's a one liner. How can I do the same without manually writing all the pattern matching for every function in the traits X every variant in the enum
?
回答1:
You can use a macro by example (rather than a procedural macro) to avoid the boilerplate:
macro_rules! make_alphabet {
($($x:ident),*) => {
enum Letter {
$(
$x($x),
)*
}
impl AlphabetLetter for Letter {
fn some_function(&self) {
match self {
$(
Letter::$x(letter) => letter.some_function(),
)*
}
}
}
};
}
Then you call it to generate everything:
make_alphabet!(A, B, C, ..., Z);
And now you can visit it any time you have a letter: Letter
:
letter.some_function();
For methods that do not need to operate on all the variants, you can have an impl
outside.
回答2:
The polymorphic_enum
macro generates an enum with the chosen name and variants, as well as another macro with a chosen name.
This generated macro is specific to the generated enum since it repeats the same block of code (closure like) for all the variants (exactly what you did explicitly).
It supposes that all the variants can be used in the exact same manner; hence the name polymorphic_enum
.
You don't have to write a new macro for each enum you want to handle this way since the macro specific to each particular enum is generated. You don't even have to implement the trait on the enum (welcome back duck-typing ;^) but you can if you want to. You just have to declare your enum in an uncommon way...
The invocation of the code which is supposed to be polymorphic is similar to
what you do when providing a generic lambda-closure to std::visit()
on a
single std::variant
in C++ (no multiple dispatch here however).
trait AlphabetLetter {
fn some_function(&self) -> String;
fn something_else(
&self,
arg: usize,
) {
println!("--> {}, arg={}", self.some_function(), arg);
}
}
struct A {
// ...
}
struct B {
// ...
}
// ...
struct Z {
// ...
}
impl AlphabetLetter for A {
fn some_function(&self) -> String {
format!("some function on A")
}
}
impl AlphabetLetter for B {
fn some_function(&self) -> String {
format!("some function on B")
}
}
// ...
impl AlphabetLetter for Z {
fn some_function(&self) -> String {
format!("some function on Z")
}
}
macro_rules! polymorphic_enum {
($name:ident $macro:ident, $($variant:ident($type:path),)*) => {
enum $name { $($variant($type)),* }
macro_rules! $macro {
($on:expr, |$with:ident| $body:block) => {
match $on {
$($name::$variant($with) => $body )*
}
}
}
}
}
polymorphic_enum! {
Letter use_Letter,
AVariant(A),
BVariant(B),
// ...
ZVariant(Z),
}
fn main() {
let letters = vec![
Letter::AVariant(A {}),
Letter::BVariant(B {}),
// ...
Letter::ZVariant(Z {}),
];
for (i, l) in letters.iter().enumerate() {
let msg = use_Letter!(l, |v| { v.some_function() });
println!("msg={}", msg);
use_Letter!(l, |v| {
let msg = v.some_function();
v.something_else((i + 1) * msg.len())
});
}
}
来源:https://stackoverflow.com/questions/63848427/using-enums-for-dynamic-polymorphism-in-rust