I have this if
statement where both branches have to return a &HashMap<..>
. In one of the branches though I have an owned HashMap
and the other one I'm accessing one from a reference to some struct. Currently, I'm being forced to define an unbound let
variable outside the if
block to hold the owned map from the first branch (otherwise it would be freed at the end of the if
block and the reference would be invalid).
struct Descriptor {
env: HashMap<String, String>
}
fn merge_env(_a: &HashMap<..>, _b: &HashMap<..>) -> HashMap<String, String> {
todo!()
}
fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
// Can I somehow avoid having to declare this?
let holder;
let env = if feature_on {
holder = merge_env(&d1.env, &d2.env);
&holder
} else {
&d1.env
};
read_env(&env);
}
fn read_env(_env: &HashMap<String, String>) { todo!() }
Playground
Is there way to do this avoiding the unbound let
? Maybe some wrapper that can hold both an owned and borrowed value? Also, is the current way idiomatic?
Variables have scope in rust. Once their scope ends, they are de-allocated. Thus if you declare your holder
inside the if
like that:
if feature_on {
let holder = merge_env(&d1.env, &d2.env);
&holder
}
// holder has already been dropped at that point. It would be "use after free" error to use it here
It would be impossible to use it outside the if
block, because it would have been dropped at that point. Thus you must make sure that the owned object lives long enough.
One of the possible solutions is what you have already done - by moving the owning variable before the if
you have guaranteed that the holder instance will not be dealocated , thus avoiding the use after free error.
The other solution is to use the enum std::borrow::Cow
, which has two variants - Cow::Borrowed
- for holding references and Cow::Owned
for holding the actual instance. This perfectly fits your use case:
fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
let env = if feature_on {
Cow::Owned(merge_env(&d1.env, &d2.env))
} else {
Cow::Borrowed(&d1.env)
};
read_env(&env);
}
You can refactor holder
to be an Option
and have the None
case be what is currently your else-branch. Then, a second variable can be a ref into that Option
or to an argument (type annotations are not needed, just for explanation):
fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
let holder: Option<HashMap<_, _>> = feature_on.then(|| merge_env(&d1.env, &d2.env));
let env: &HashMap<_, _> = holder.as_ref().unwrap_or(&d1.env);
read_env(env);
}
If you really want to get rid of the temporary variable, you can inline all of this:
fn example(d1: &Descriptor, d2: &Descriptor, feature_on: bool) {
read_env(
feature_on
.then(|| merge_env(&d1.env, &d2.env))
.as_ref()
.unwrap_or(&d1.env),
);
}
Considering the "is this idiomatic": I would say, that your current code is absolutely fine too.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With