How to easily and graceful hold reference in a struct?

Hi Everyone:

To improve code readability and maintainability, often need to separate a whole logic code to different unit like code below:

struct Device {
    name: String,
}

struct PartialLogicProcessor<'a> {
    dev: &'a Device,
}

struct BigService<'a> {
    dev: Device,
    plpor: PartialLogicProcessor<'a>,
}

impl<'a> BigService<'a> {
    fn new() -> BigService<'a> 
    {
        let d = Device { name:"aaa".to_string() };
        BigService {
            dev: d,
            plpor: PartialLogicProcessor { dev: &d },
        }
    }
}

fn main() 
{
    let _ = BigService::new();
}

The code will get compile errors cause reference borrowing, btw:

  1. The code may be run in multi-thread, so std::rc::Rc is not suitable.
  2. I don't want to wrap the dev in Arc<mutex<>>(need to get mutable reference of Device so must wrap it in mutex too) because the user of BigService will guarantee the code thread-safe(e.g. wrapping it in Arc<mutex<>>).

Such questions like this more closely a question about lifetime not thread-safe, but it looks like I must find the solution about thread-safe to solve, so what do I do?

Don't create self-referential types.

Drop the plpor field altogether, and only create that instance, temporarily, when you actually need to do something with it.

1 Like

I would argue that your example is reducing code readibility and maintainabily. By creating a shared reference to the Device you're clarly stating that it won't change in the lifetime of that reference, but then you claim you also want to get a mutable reference to it to change it. Thus you are creating a situation where there is aliasing between the two references, where one of them allows mutation and there's no restrictions on how you can use them (e.g. a Mutex/RwLock would restrict who can read and write at a certain point in time, but references don't have such restrictions for obvious reasons). This is exactly the combination of factors that create spaghetti code, because the impact of some mutation is no longer immediately clear and you must instead trace in your code every reference that could possibly be impactede, thus losing local reasoning.

If your goal is to just separate the namespaces of Device and PartialLogicProcessor's methods, then create a PartialLogicProcessor every time needed, without storing it. So for example every time you would do big_service.plpor.some_method() you would do big_service.plpor().some_method() where plpor is a method that creates a fresh new PartialLogicProcessor every time it's called. This makes it clearer where the reference in the PartialLogicProcessor comes from, enabling local reasoning again.

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.