Is there any difference between the two implementation of the struct?

72 Views Asked by At
struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level(&'a self) -> i32 {
        3
    }
}

vs

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a, 'b> ImportantExcerpt<'a> {
    fn level(self: &'b ImportantExcerpt<'a>) -> i32 {
        3
    }
}

2

There are 2 best solutions below

1
prog-fh On

In the example below, your two versions of level() are level_1() and level_2().
There is not really a difference since these functions return a value, not a reference, thus the subtleties about lifetime do not change anything: the returned value will live on its own.
These two versions are used in the example, and we cannot see any difference about lifetime constraints.
However, if we want to return a reference, as in text_1() to text_4(), then the lifetimes must be considered with attention.

text_1() is the immediate adaptation of level_1().
The lifetime elision rules tell us that the lifetime of the resulting &str is that of the self parameter ('a explicitly given here).
(Note that I'm not certain if it makes sense to write the same lifetime for the structure itself and a reference to it; someone with a better knowledge of Rust than me could develop around that)
This means that the resulting &str must not live longer than the structure; this is shown as an error in the example.

text_2() is the immediate adaptation of level_2().
The lifetime elision rules tell us that the lifetime of the resulting &str is that of the self parameter ('b explicitly given here).
Then, this is equivalent to text_3() and the consequence is the same as with text_1().
This means that the resulting &str must not live longer than the structure; this is shown as an error in the example.

When it comes to text_4(), we explicitly provide a lifetime for the resulting &str which is different from the lifetime of the self reference.
This is actually the same lifetime used internally for part in the structure.
This means that the resulting &str must not live longer than the original string used to initialise part, but it is not at all linked to the structure itself; the example shows that it is still possible to use the resulting &str after the structure is dropped.

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a, 'b> ImportantExcerpt<'a> {
    fn level_1(&'a self) -> i32 {
        3
    }
    fn text_1(&'a self) -> &str {
        self.part
    }
    fn level_2(self: &'b ImportantExcerpt<'a>) -> i32 {
        3
    }
    fn text_2(self: &'b ImportantExcerpt<'a>) -> &str {
        self.part
    }
    fn text_3(self: &'b ImportantExcerpt<'a>) -> &'b str {
        self.part
    }
    fn text_4(self: &'b ImportantExcerpt<'a>) -> &'a str {
        self.part
    }
}

fn main() {
    let f_txt = "first".to_owned();
    let f_out = {
        let ie = ImportantExcerpt {
            part: f_txt.as_str(),
        };
        // (ie.text_1(), ie.level_1()) // `ie` dropped here while still borrowed
        // (ie.text_2(), ie.level_1()) // `ie` dropped here while still borrowed
        // (ie.text_3(), ie.level_1()) // `ie` dropped here while still borrowed
        (ie.text_4(), ie.level_1()) // correct
    };
    println!("{:?}", f_out);

    let s_txt = "second".to_owned();
    let s_out = {
        let ie = ImportantExcerpt {
            part: s_txt.as_str(),
        };
        // (ie.text_1(), ie.level_2()) // `ie` dropped here while still borrowed
        // (ie.text_2(), ie.level_2()) // `ie` dropped here while still borrowed
        // (ie.text_3(), ie.level_2()) // `ie` dropped here while still borrowed
        (ie.text_4(), ie.level_2()) // correct
    };
    println!("{:?}", s_out);
}
/*
("first", 3)
("second", 3)
*/
0
true equals false On

Here is code equivalent to your example with usage example (playground)

struct ImportantExcerpt<'a> {
    part: &'a str,
}

impl<'a> ImportantExcerpt<'a> {
    fn level1(self: &'a Self) -> i32 {
        3
    }

    fn level2<'b>(self: &'b Self) -> i32 {
        3
    }
}

fn main() {
    let string: &'static str = "string";
    let ie: ImportantExcerpt<'static> = ImportantExcerpt {part: string};
    dbg!(ie.level1());
    dbg!(ie.level2());
}

This compiles fine, but only because of a certain feature of rust, variance (see reference and rustnomicon). ie has type ImportantExcerpt<'static>, therefore you could think that level1 expects a static reference to ie. But before the function gets called, the type of ie changes to ImportantExcerpt<'a> where 'a is a much more local lifetime, shorter than 'static. This means that in this example, where owned values are returned, the functions have no difference. But more generally, there could be a difference if ImportantExcerpt<'a> is invariant (or contravariant) over 'a. For example (playground):

use std::cell::Cell;

struct ImportantExcerpt<'a> {
    part: Cell<&'a str>,
}

impl<'a> ImportantExcerpt<'a> {
    fn level1(self: &'a Self) -> i32 {
        3
    }

    fn level2<'b>(self: &'b Self) -> i32 {
        3
    }
}

fn main() {
    let string: &'static str = "string";
    let ie: ImportantExcerpt<'static> = ImportantExcerpt {part: Cell::new(string)};
    dbg!(ie.level1());
    dbg!(ie.level2());
}

Here dbg!(ie.level2()) compiles, but dbg!(ie.level1()) fails with the error that ie doesn't live long enough. Here the conversion described above can't happen, so level1 expects a reference with the same lifetime as Self, which is 'static.

In general I would recommend the second function, as it should work in more cases.