I'm new to Rust and coming from C background. Trying to design an interface, I'm undecided between 2 approaches:
struct Handler {
name: String,
invoke: fn()
}
struct MyHandler1;
impl MyHandler1 {
fn new() -> Handler {
name: String::from("myhandler1"),
invoke: || { println!("hello") }
}
}
(...)
const MY_HANDLERS: &[Handler] = &[MyHandler1::new(), MyHandler2::new()];
and
trait Handler {
fn name(&self) -> String;
fn invoke(&self);
}
struct MyHandler1;
impl Handler for MyHandler1 {
fn name(&self) -> String { "myhandler1".to_string() }
fn invoke(&self) {
println!("hello");
}
}
(...)
// can't have const fn in traits so box'em up inside main...
let handlers: Vec<Box<dyn Handler>> = vec![Box::new(MyHandler1], Box::new(MyHandler2)];
I'm leaning towards the former example, but I can't shake the feeling that it's coming from a deeply ingrained C way of thinking, having all the functions and members nicely stacked up inside classes and simulating type erasure by the new
function that generates a "base class" instead of trying to get used to thinking in Rust.
It feels like Rust encourages using functions instead of objects (traits instead of inheritance). Is there an established good practice in Rust community to this end? Are both approaches considered OK and just a matter of preference? Or are there alternatives that you use to achieve this kind of design? (Please ignore trivial syntax errors)
CodePudding user response:
It's certainly perfectly idiomatic to use trait objects, there doesn't seem to be a good reason to do so in this instance.
So I would go with the first approach. However, it's not a Rust pattern to use "constructor structs" like your MyHandler1
. Instead, your Handler
struct should provide a constructor:
struct Handler {
name: String,
// Generally, it's better to take closures than function pointers as they
// are more versatile. This does necessitate to box it.
invoke: Box<dyn Fn()>,
}
impl Handler {
// Constructors are by convention called `new` in Rust.
fn new(name: String, invoke_with: String) -> Self {
Self {
name,
invoke: Box::new(move || {
println!("{}", invoke_with);
}),
}
}
// You can just add a method for any special type of Handler
// you want to commonly make.
fn new_handler_of_type_1() -> Self {
Self::new("myhandler1".to_string(), "hello".to_string())
}
fn new_handler_of_type_2() -> Self {
Self::new("myhandler2".to_string(), "goodbye".to_string())
}
}
And then you can call it like this:
fn main() {
let my_handlers = &[
Handler::new("mycustomhandler".to_string(), "custom msg".to_string()),
Handler::new_handler_of_type_1(),
Handler::new_handler_of_type_2(),
];
}
CodePudding user response:
Even if it doesn't need to grow, mapping the name (the string) to the closure might be a nice ease-of-use. Depending on how much data your Handler
struct needs which wasn't in your example, you might could combine the approaches:
#![feature(once_cell)]
use std::collections::BTreeMap;
use std::lazy::SyncLazy;
type Closure = dyn Fn() -> () Sync Send;
static HANDLERS: SyncLazy<BTreeMap<&str, Box<Closure>>> = SyncLazy::new(|| {
let mut map = BTreeMap::<&str, Box<Closure>>::new();
map.insert("handler1", Box::new(|| println!("hello")));
map.insert("handler2", Box::new(|| println!("goodbye")));
map
});
fn main() {
HANDLERS.get("handler1").unwrap()();
}
I'm using the SyncLazy
from the nightly compiler, but once_cell
and lazy_static
are other options, or you could just have it in main
.