I'm learning about named lifetimes in Rust, and I'm having trouble understanding what they represent when they are used in the implementation of a trait. Specifically, I'm having trouble understanding this piece of code from libserialize/hex.rs. I've removed some comments for brevity's sake.
pub trait ToHex {
fn to_hex(&self) -> ~str;
}
static CHARS: &'static[u8] = bytes!("0123456789abcdef");
impl<'a> ToHex for &'a [u8] {
fn to_hex(&self) -> ~str {
let mut v = slice::with_capacity(self.len() * 2);
for &byte in self.iter() {
v.push(CHARS[(byte >> 4) as uint]);
v.push(CHARS[(byte & 0xf) as uint]);
}
unsafe {
str::raw::from_utf8_owned(v)
}
}
}
I understand the 'static
lifetime in the CHARS definition, but I'm stumped on the lifetime defined in the ToHex implementation. What do named lifetimes represent in the implementation of a trait?
Lifetimes are what the Rust compiler uses to keep track of how long references are valid for. Checking references is one of the borrow checker's main responsibilities. Lifetimes help the borrow checker ensure that you never have invalid references.
Rust uses lifetime parameters to avoid such potential run-time errors. Since the compiler doesn't know in advance whether the if or the else block will execute, the code above won't compile and an error message will be printed that says, “a lifetime parameter is expected in compare 's signature.”
Trait and lifetime bounds provide a way for generic items to restrict which types and lifetimes are used as their parameters. Bounds can be provided on any type in a where clause.
Static items have the static lifetime, which outlives all other lifetimes in a Rust program. Static items may be placed in read-only memory if the type is not interior mutable. Static items do not call drop at the end of the program.
In that particular case—not much. &[u8]
is not a completely specified type because the lifetime is missing, and implementations must be for fully specified types. Thus, the implementation is parameterised over the arbitrary (for the generic parameter is unconstrained) lifetime 'a
.
In this case, you don't use it again. There are cases where you will, however—when you wish to constrain a function argument or return value to the same lifetime.
You can then write things like this:
impl<'a, T> ImmutableVector<'a, T> for &'a [T] {
fn head(&self) -> Option<&'a T> {
if self.len() == 0 { None } else { Some(&self[0]) }
}
…
}
That means that the return value will have the same lifetime as self
, 'a
.
Incidentally, just to mess things up, the lifetime could be written manually on each function:
impl<'a, T> ImmutableVector<'a, T> for &'a [T] {
fn head<'a>(&'a self) -> Option<&'a T> {
if self.len() == 0 { None } else { Some(&self[0]) }
}
…
}
… and that demonstrates that having to specify the lifetime of the type that you are implementing for is just so that the type is indeed fully specified. And it allows you to write a little bit less for all the functions inside that use that lifetime.
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