rustenumsreference

Simplifying getting a specific type from an enum


I have a set of classes linked into an enum:

#[derive(Default)]
pub struct A {
}

#[derive(Default)]
pub struct B {
}

#[derive(Default)]
pub struct C {
}

enum Classes {
    A(A),
    B(B),
    C(C),
}

This enum is then stored in an Rc<RefCell<Classes>>, later if I want to get an instance of A or return an error if the object doesn't contain A I can do:

fn main() -> Result<(), String> {
    let obj: Rc<RefCell<Classes>> = Rc::new(RefCell::new(Classes::A(A::default())));
    match &*obj.borrow() {
        Classes::A(_a) => println!("has A"),
        _ => return Err("not A".to_string()),
    }
    Ok(())
}

This works but is a little awkward. What I'd like to do is implement a function like this:

fn get_a(reference: &Rc<RefCell<Classes>>) -> Option<&A> {
    match &*reference.borrow() {
        Classes::A(o) => return Some(o),
        _ => return None,
    }
}

Which would allow:

fn main() -> Result<(), String> {
    let obj: Rc<RefCell<Classes>> = Rc::new(RefCell::new(Classes::A(A::default())));
    let a: &A = get_a(&obj).ok_or("not A".to_string())?;
    Ok(())
}

Is this possible or is there another way to achieve this?

Rust playground


Solution

  • Your approach won't work because the &A reference would be tied to the temporary Ref<Classes> from reference.borrow(), which gets dropped at the end of the function.

    The best solution is to use Ref::map to create a mapped reference:

    rust

    use std::cell::Ref;
    
    fn get_a(reference: &Rc<RefCell<Classes>>) -> Option<Ref<A>> {
        let borrowed = reference.borrow();
        if matches!(*borrowed, Classes::A(_)) {
            Some(Ref::map(borrowed, |classes| match classes {
                Classes::A(a) => a,
                _ => unreachable!(),
            }))
        } else {
            None
        }
    }
    

    Usage:

    rust

    fn main() -> Result<(), String> {
        let obj: Rc<RefCell<Classes>> = Rc::new(RefCell::new(Classes::A(A::default())));
        let a_ref = get_a(&obj).ok_or("not A".to_string())?;
        // a_ref behaves like &A and keeps the borrow alive
        println!("Got A: {:?}", *a_ref);
        Ok(())
    }
    

    Alternative approaches:

    1. Use a closure (most flexible):

    rust

    fn with_a<R>(reference: &Rc<RefCell<Classes>>, f: impl FnOnce(&A) -> R) -> Option<R> {
        match &*reference.borrow() {
            Classes::A(a) => Some(f(a)),
            _ => None,
        }
    }
    
    // Usage:
    with_a(&obj, |a| println!("Value: {:?}", a))?;
    
    1. Add methods to your enum:

    rust

    impl Classes {
        fn as_a(&self) -> Option<&A> {
            match self { Classes::A(a) => Some(a), _ => None }
        }
    }
    
    // Usage:
    let a = obj.borrow().as_a().ok_or("not A")?;
    

    The Ref::map approach is closest to your desired API while respecting Rust's borrowing rules.