I had another look, and yeah, you're trying to use Rust references like they were Python object values or such, so that multiple data structures can all simultaneously hold on to a handle to the same Network
, etc. In the end, everything ends up having a handle to everything else through one path or another.
That's not what Rust references are for. The closest thing would be Arc<Mutex<_>>
or Rc<RefCell<_>>
. Or just Rc<_>
or Arc<_>
if you don't need &mut _
s.
So one option would be to convert most your reference-holding data structures to use those instead. Look into Weak
for cycles so that you don't leak memory. You might have to contend with panics (RefCell
) or deadlocks (Mutex
) instead of borrow-checker errors, depending on how exactly things are used.
LazyLock
or similar is another alternative for global resources.
A third option is to use something besides the "everything has a handle to everything else" model, which I touch on below. It may be a larger redesign. It would probably be more idiomatic.
In more detail, you've created a big self-referencial ball of mud.
Here are a bunch of probable reference cycles I found:
Network Server
Network Server OffPeakStrategy
Network HomeAssistantAPI
Network NodesHeap PublicPowerGrid NodesBase
Network NodesHeap PublicPowerGrid NodesBase SourceFeeder HomeAssistantAPI
Network NodesHeap PublicPowerGrid NodesBase Feeder SourceFeeder HomeAssistantAPI
Network NodesHeap Switch NodesBase
Network NodesHeap Switch NodesBase SourceFeeder HomeAssistantAPI
Network NodesHeap Switch NodesBase Feeder SourceFeeder HomeAssistantAPI
Instead of going all-in on reference counting, you could instead try to redesign things to have a clear ownership story. Even if you end up with Network
in an Rc<Mutex<_>>
or such, you could at least reduce the need for reference counting everything.
Let's look at a specific example:
pub struct HomeAssistantAPI<'a, 'b:'a> {
network: Option<&'b Network<'a>>,
// ...
}
A side bar about the lifetime bounds (click to expand)
The presence of &'b Network<'a>
in a fields means that there is an implicit 'a: 'b
bound. In combination with your explicit 'b: 'a
bound, that means that 'b
and 'a
must always be the same lifetime. Probably you added the explicit bound because the compiler suggested it somewhere, and the compiler probably suggested it because the self-referencial nature of your data structures made it required somewhere.
This pattern where the two lifetimes have to be the same is repeated on all of your two-lifetime structs I looked at except NodesHeapIterator
. Should you make them all one lifetime? Probably it would be fine to do so, but it's hard to say with certainty, so I'd just leave it be until you -- hopefully -- refactor all or most of them away.
In any case, the "lifetimes were forced to be the same" pattern is a yellow flag to be aware of.
At least in home_assistant_api.rs
, it doesn't look like you use this field. So get rid of it. Then in places where you might have used it, take &Network<'_>
as an argument instead.
Then pick another lifetime-carrying struct, especially if it's part of a cycle, and see if you can get rid of the lifetimes on it, too. You'll have to figure out what types own resources and what types take borrows of them as arguments in the process.
Some other drive-by comments follow.
pub struct Network<'a> {
server: Option<Rc<&'a Server<'a, 'a>>>
Rc<&T>
offers no real benefit over &T
.
pub trait EnergyStrategy<'a, 'b:'a, 'c:'b> {
fn new(network:&'c Network<'a>, id:&str, config:&LinkedHashMap<Yaml, Yaml>) -> ResultOpenHems<OffPeakStrategy<'a, 'a>>;
The implicit 'a: 'c
from the nested lifetime in new
, plus the explicit 'c: 'b, 'b: 'a
from the trait, means that 'a
and 'c
(and 'b
) have to be the same in the call to new
. Probably this mostly just mirrors what's going on with your struct
s.
However:
- A trait with this many lifetimes is a yellow flag generally
'b
isn't used for anything else, so drop it
- Probably you can put the remaining lifetime(s) you need on
new
instead, which I recommend
(Though this may all be short-term busy work as you refactor lifetimes away anyway.)
impl<'a, 'b:'a, 'c:'b> fmt::Display for HomeAssistantAPI<'a, 'b> {
You generally don't need to repeat lifetime bounds from the struct
definition on implementations. And why is there a 'c
here? Most of your implementations should not involve bounds:
impl fmt::Display for HomeAssistantAPI<'_, '_> {
// Or if you actually need to name the lifetimes
impl<'a, 'b> SomeTrait<'a, 'b> for HomeAssistantAPI<'a, 'b> {
fn some_method(&self, whatever: &'a Whatever<'b>) {