Lifetime issue assigning reference from conditional

Viewed 56

I'm quite new in Rust and I'm having an issue with lifetimes that I believe I can understand what is happening and why, but can't get around in my head how can I solve it.

For simplicity I created this short "clone" of what I'm actually trying to do, but the real code is using asyc-stripe. Will annotate the example code with the real one in case is relevant.

There is the following structure:

// https://github.com/arlyon/async-stripe/blob/9f1a84144a23cc7b2124a1252ee15dc646ce0215/src/resources/generated/subscription.rs#L385
struct ObjectA<'a> {
    field: i32,
    object_b_id: Option<&'a str>,
}

// https://github.com/arlyon/async-stripe/blob/9f1a84144a23cc7b2124a1252ee15dc646ce0215/src/resources/generated/subscription.rs#L570
impl<'a> ObjectA<'a> {
    fn new(field: i32) -> Self {
        return Self {
            field,
            object_b_id: Default::default(),
        };
    }
}

// https://github.com/arlyon/async-stripe/blob/9f1a84144a23cc7b2124a1252ee15dc646ce0215/src/resources/generated/subscription.rs#L210
fn persist_obj_a(obj_a: ObjectA<'_>) {}

// ---

// https://github.com/arlyon/async-stripe/blob/9f1a84144a23cc7b2124a1252ee15dc646ce0215/src/resources/generated/payment_method.rs#L18
struct ObjectB {
    id: ObjectBId,
}

// https://github.com/arlyon/async-stripe/blob/9f1a84144a23cc7b2124a1252ee15dc646ce0215/src/ids.rs#L518
struct ObjectBId {
    value: String,
}

impl ObjectBId {
    fn as_str(&self) -> &str {
        return self.value.as_str();
    }
}

// This is a wrapper around https://github.com/arlyon/async-stripe/blob/9f1a84144a23cc7b2124a1252ee15dc646ce0215/src/resources/generated/payment_method.rs#L128 that just returns the first one found (id any, hence the Option)
fn load_object_b() -> Option<ObjectB> {
    return Some(ObjectB {
        id: ObjectBId {
            value: String::from("some_id"),
        },
    });
}

And what I'm trying to do is: load the ObjectB with load_object_b and use its ID into a ObjectA.

Ok, so on to my attempts.

First attempt

fn first_try(condition: bool) {
    let mut obj_a = ObjectA::new(1);

    if condition {
        match load_object_b() {
            Some(obj_b) => obj_a.object_b_id = Some(obj_b.id.as_str()),
            None => (),
        }
    }

    persist_obj_a(obj_a);
}

In here I get

obj_b.id does not live long enough

Which I can understand, since from what I can understand the obj_b only exists during the match arm and is droped by the end of it.

Second attempt

fn second_try(condition: bool) {
    let mut obj_a = ObjectA::new(1);

    if condition {
        let obj_b = load_object_b();
        match obj_b {
            Some(ref obj_b) => obj_a.object_b_id = Some(obj_b.id.as_str()),
            None => (),
        }
    }

    persist_obj_a(obj_a);
}

Here I get

obj_b.0 does not live long enough

Which I guess it is still the same idea, just in a different place. Since again, from my understanding, obj_b now only lives within the scope of the if condition.

Third and last attempt

I ended up "solving" it with:

fn third_try(condition: bool) {
    let mut obj_a = ObjectA::new(1);

    let obj_b = load_object_b();
    let obj_b_id = match obj_b {
        Some(ref obj_b) => Some(obj_b.id.as_str()),
        None => None,
    };

    if condition {
        obj_a.object_b_id = obj_b_id;
    }

    persist_obj_a(obj_a);
}

In here I moved the obj_b to have the same lifetime as obj_a. So it solves the issue that I was having. My problem with this solution is that I feel that I'm wasting resource doing the (possible expensive) request to load_object_b even if I'm not gonna use it based on the condition. Not sure if I'm missing something very obvious or just going on the overall wrong direction, but would appreciate some light on what I might be doing wrong.

2 Answers

This should work, I think:

fn third_try(condition: bool) {
    let mut obj_a = ObjectA::new(1);

    let obj_b = if condition { load_object_b() } else { None };
    obj_a.object_b_id = obj_b.as_ref().map (|o| o.id.as_str());

    persist_obj_a(obj_a);
}

Rust allows you to have conditionally initialized variables. You can declare obj_b ouside of the if, but only initialize it inside the if. The compiler will ensure you can use it only if it is initialized.

fn second_try(condition: bool) {
    let mut obj_a = ObjectA::new(1);

    let obj_b;
    if condition {
        obj_b = load_object_b();
        match obj_b {
            Some(ref obj_b) => obj_a.object_b_id = Some(obj_b.id.as_str()),
            None => (),
        }
    }

    persist_obj_a(obj_a);
}
Related