A way to get the best of both enums and dynamic pointers?

Here's my situation:

  • My program provides pieces of data which the user can control through a GUI
  • These pieces of data can be of several different data types, but all must implement a common set of functions.
  • During startup, a file is read to determine what pieces of data should be created.
  • Later during startup, other files are read to create GUIs that modify certain pieces of data. At this point, it needs to be checked that any particular piece of data that the file refers to is of the correct type. A text box cannot be used to modify an integer, for example.
  • During the main loop of the program, there should be a single list of all of these pieces of data such that one or more of the mandatory functions can be called on all entries in the list.
  • During runtime, the widgets should store references to the pieces of data they are assigned so that they can modify them.

Here are a couple approaches I was thinking of and their shortcomings:

enum approach

Make something like this:

enum UserData {
    Int(IntImpl),
    String(StringImpl),
    Color(ColorImpl),
}

impl UserData {
    fn common_fn(&mut self) {
        match self { /* ... */ }
    }
}

During the first part of startup, everything would be collected into a Vec<Rc<RefCell<UserData>>>. This list would also be used to apply common_fn to all elements during runtime. Type checks during the second part of startup can be done with if let. The downside of this approach is that I cannot store a reference to the actual underlying data, necessitating an if let whenever I want to access or modify the actual data. For example, I would have to do something like this to render a text box:

if let UserData::String(simpl) = self.assigned_data.borrow() {
    graphics.render_string(simpl.string_data);
} else {
    unreachable!();
}

I also considered making the enum entries pointers themselves like String(Rc<RefCell<StringImpl>>>) but unfortunately one of my common functions is written to return a reference to data inside the struct. This reference would not be valid outside the body of the function in this case since getting a reference to data inside StringImpl would require calling Rc::borrow which returns a value which would be dropped once the function exits.

trait approach

Make something like this:

trait UserData {
    fn common_fn(&mut self);
}

struct IntUserData(pub i32);

impl UserData for IntUserData {
    fn common_fn(&mut self) { /* ... */ }
}

// And so on for StringUserData and ColorUserData

The advantage of this is that I can have Rc<RefCell<dyn UserData>> and Rc<RefCell<IntUserData>> pointers pointing to the same object. The former would be used in that runtime list used for applying common_fn to all pieces of user data. The latter would be used by widgets such that they can access data without if let in a manner like self.assigned_data.borrow().0. The disadvantage of this is that the statically-typed pointer can be cast to a dynamically typed pointer, but not vice versa, so I cannot store everything specified during the first part of startup as a single list. One way to do it would be to create a list for every type of UserData, but this would be cumbersome to pass around between the parts of the code that need it and would require quite a bit of modification every time I want to add a new kind of data. Another approach, which I think is the best out of everything I have laid out so far, would be to create an enum which would only be used during startup:

enum UserDataPtr {
    IntPtr(Rc<RefCell<IntUserData>>),
    StringPtr(Rc<RefCell<StringUserData>>),
    ColorPtr(Rc<RefCell<ColorUserData>>),
}

During the first part of startup, everything would be stored as a Vec<UserDataPtr>. During the second phase of startup, GUI widgets can do type checking and get a reference to the actual underlying data:

let color_data = if let UserDataPtr::ColorPtr(ptr) = &data_item_specified_by_file {
    Rc::clone(ptr)
} else {
   return Err("The data item must be a color");
}

After all the GUI widgets are created, a match statement can be used on each item of the vector to create a new vector of Rc<RefCell<dyn UserData>> to be used during runtime. The only downside to this approach is the boilerplate, both in the enum definition and the conversion.

Having written this all out I think I'll just go with the last option of a trait-based approach with an enum used during startup. The boilerplate is unfortunate but far better than most of the alternatives. However, if there's something I'm missing that could make this all work even better I would greatly appreciate hearing it.

There’s ways around this, check this out:

use std::ops::Deref;
use std::cell::RefCell;
use std::rc::Rc;
use std::any::Any;

// put the RefCells inside here
struct IntUserData(RefCell<i32>);

impl UserData for IntUserData {
    fn custom_fn(&self) {
        *self.0.borrow_mut() += 1;
    }
}

trait UserData: AsAny {
    fn custom_fn(&self);
}

// approach inspired by `downcast_rs` crate
trait AsAny: Any {
    fn as_any(&self) -> &dyn Any;
    fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any>;
}
impl<T: Any> AsAny for T {
    fn as_any(&self) -> &dyn Any {
        self
    }
    fn into_any_rc(self: Rc<Self>) -> Rc<dyn Any> {
        self
    }
}
impl dyn UserData {
    fn downcast_clone_rc<T: Any>(self: &Rc<dyn UserData>) -> Option<Rc<T>> {
        if self.deref().as_any().is::<T>() {
            self.clone().into_any_rc().downcast::<T>().ok()
        } else {
            None
        }
    }
}

fn main() {
    let x: Vec<Rc<dyn UserData>> = vec![Rc::new(IntUserData(RefCell::new(0)))];
    
    // some dynamic call for good measure
    x[0].custom_fn();
    
    if let Some(data) = x[0].downcast_clone_rc::<IntUserData>() {
        // data really has the correct type
        let d: Rc<IntUserData> = data;
        println!("yay, {}", d.0.borrow())
        // build your widget here with direct access to the IntUserData
    }
}
1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.