An important thing to note here is that MyStruct<&String>
is just an abbreviation for MyStruct<&'_ String>
. There is still a place for a lifetime argument. The situation is completely analogous to
pub struct RefStruct<'l> {
pub a: &'l String
}
where you can write RefStruct<'_>
to “elide” the lifetime argument. Rust even allows you to write RefStruct
without any lifetime arguments, though using this feature is somewhat discouraged because hiding the presence of a lifetime argument like this can be confusing. (Still, it’s not a loophole.)
Actually it’s the other way around, a reference in a struct must outlive the struct it’s contained in. Also this hasn’t got too much to do with why a lifetime “annotation” is needed.
This is not quite accurate. The reason why a lifetime parameter is needed is to that there’s a way to specify the lifetime of the reference in the field a
of RefStruct
. If you want to put a Vec
into a struct field, you can’t write
pub struct VecStruct {
pub a: Vec
}
either. It has to be
pub struct VecStruct<T> {
pub a: Vec<T>
}
In general, lifetime annotations are never needed because you need to “tell the compiler that you aren’t doing something wrong”. The compiler will know to check for itself if you’re violating any important rules. Something like “we must […] tell compiler that a
outlives RefStruct
” doesn’t make too much sense because it seems to assume that “a
has to outlive RefStruct
in order to not violate Rust’s borrowing rules” and “we need to tell the compiler that we aren’t breaking the rules”. Instead, lifetime annotations are always needed to clarify or specify interactions of lifetimes. There’s usually some choice involved, you’ll tell the compiler which of multiple options you choose and the compiler will work with that. You’ll specify some interface (in the form of a function/method signature) and the compiler will ensure that both the implementation and the callers will not violate the signature you wrote.
In the case of structs, there’s the choice if the lifetime of the a: &'_ String
field is supposed to be a parameter or some fixed lifetime (in which case it could only be “'static
”). Once you have multiple fields with lifetime arguments, there’s the choice of whether to make them all use the same lifetime or to provide multiple lifetime as parameters of the struct, e.g.
pub struct StructOne<'a> {
pub a: &'a u8,
pub b: &'a u8,
}
vs.
pub struct StructTwo<'a, 'b> {
pub a: &'a u8,
pub b: &'b u8,
}
And for a function fn(&u8, &u8) -> &u8
you’ll need to choose which one of the input parameters the lifetime in the type of the return value should correspond to. Or whether any of them is supposed to be 'static
. There’s options to choose like
fn foo<'a, 'b>(x: &'a u8, y: &'b u8) -> &'a u8 { … }
fn foo<'a, 'b>(x: &'a u8, y: &'b u8) -> &'b u8 { … }
fn foo<'a, 'b, 'c>(x: &'a u8, y: &'b u8) -> &'c u8 { … }
fn foo<'a, 'a>(x: &'a u8, y: &'a u8) -> &'a u8 { … }
fn foo<'a, 'b>(x: &'a u8, y: &'b u8) -> &'static u8 { … }
// etc…
By the way, the use of type &String
(“shared reference to String
”) is also discouraged in Rust, one should use &str
instead. This and other idioms and conventions can be enforced easily by using clippy, something I’d recommend to every beginner / language learner of Rust, the tool can teach you a lot.
In general, reference types in Rust have the form &'a T
, with an explicit lifetime 'a
. There’s two situations when this lifetime can be left out. First, there’s lifetime elision rules, chapter 10 of the book talks about them. Second, you can leave out lifetimes in places where a type signature is not mandatory. The lifetime will then be inferred, this has nothing to do with the elision rules in function and method signatures. Your example
let a: MyStruct<&String>;
is such a case. let a;
without a signature is allowed, too, when the compiler can infer the type. You can help type inference by providing more information, e.g. let a: MyStruct<_>;
and the compiler will infer the type for where the “_
” is. Or let a: MyStruct<&'_ String>;
and the compiler will infer the lifetime for where the '_
is. Finally, &T
is an abbreviation for &'_ T
, so writing let a: MyStruct<&String>;
is the same.
Another thing you might notice with type inference is that while the compiler always complains when it cannot infer a type (because of some ambiguity, e.g. when you’re only using a variable with a generic function) e.g.
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let a;
| ^ consider giving `a` a type
or
error[E0282]: type annotations needed for `Box<_>`
--> src/main.rs:2:12
|
2 | let a: Box<_>;
| - ^^^^^^ cannot infer type
| |
| consider giving `a` the explicit type `Box<_>`, with the type parameters specified
it will never complain when a lifetime is underspecified, e.g.
fn foo() {
let a: &'_ u8; // compiles fine
}