Sobald ein Struct oder Enum einen Lifetime-Parameter trägt, muss jeder impl-Block diesen Parameter mitführen. Die Syntax ist impl<'a> MyType<'a> — der Lifetime wird in den impl-Klammern deklariert und am Typ aufgeführt. Methoden in solchen impl-Blöcken können zusätzlich eigene Lifetimes haben, und Trait-Implementations bringen weitere Pattern. Dieser Artikel zeigt die Grundsyntax, häufige Fallen und die typischen Pattern beim Schreiben von Methoden für Lifetime-tragende Typen.
Die Grundsyntax
struct Buffer<'a> {
data: &'a [u8],
}
impl<'a> Buffer<'a> {
pub fn neu(data: &'a [u8]) -> Self {
Buffer { data }
}
pub fn len(&self) -> usize {
self.data.len()
}
pub fn first(&self) -> Option<&'a u8> {
self.data.first()
}
}Drei Elemente:
impl<'a>— Lifetime-Parameter wird im impl-Block deklariertBuffer<'a>— Lifetime am Typ aufgeführt- Methoden können
'ain Signaturen nutzen — z.B.Option<&'a u8>als Rückgabe
Wichtig: das 'a im impl<'a> und das 'a in Buffer<'a> müssen denselben Namen tragen, weil sie dieselbe Lifetime sind.
Self-Lifetime in Methoden
In Methoden bezieht sich &self auf eine Referenz zur Struct-Instanz mit deren Lifetime. Das 'a der Struct-Definition gilt für die internen Refs der Instanz.
# struct Buffer<'a> { data: &'a [u8] }
impl<'a> Buffer<'a> {
// &self hat seine eigene (kurze) Lifetime, oft elidiert
// Output kann an 'a gebunden werden — interne Daten
pub fn data_ref(&self) -> &'a [u8] {
self.data
}
// Output an die kurze self-Lifetime gebunden
pub fn data_short(&self) -> &[u8] {
self.data // Wird elidiert zu &'short [u8]
}
}Zwei Outputs mit unterschiedlichen Lifetimes:
data_ref(&self) -> &'a [u8]— Output gebunden an die längere'a-Lifetime (Source-Daten)data_short(&self) -> &[u8]— Output gebunden an die kurze&self-Lifetime (durch Elision)
Bei Wahl 'a darf der Aufrufer den Output über die &self-Borrow-Lifetime hinaus nutzen — solange die ursprünglichen Source-Daten leben.
Methoden mit eigenen Lifetimes
Methoden können zusätzliche Lifetime-Parameter haben.
struct Container<'data> {
items: Vec<&'data str>,
}
impl<'data> Container<'data> {
pub fn neu() -> Self {
Container { items: Vec::new() }
}
// Methode mit zusätzlichem Lifetime-Parameter 'short
pub fn search<'short>(&self, needle: &'short str) -> Option<&'data str> {
self.items.iter()
.find(|s| s.contains(needle))
.copied()
}
}search<'short> hat einen eigenen Lifetime-Parameter 'short für das needle-Argument. Die Suche braucht das needle nur kurzfristig — der Output bezieht sich auf 'data (die Container-internen Items).
Mehrere Lifetimes im Typ
struct CrossRef<'src, 'dst> {
source: &'src str,
destination: &'dst mut String,
}
impl<'src, 'dst> CrossRef<'src, 'dst> {
pub fn neu(source: &'src str, destination: &'dst mut String) -> Self {
CrossRef { source, destination }
}
pub fn copy(&mut self) {
self.destination.push_str(self.source);
}
}Bei Typen mit mehreren Lifetimes werden alle in impl<'src, 'dst> und am Typ CrossRef<'src, 'dst> aufgeführt.
Trait-Implementations
Trait-Implementations für Lifetime-tragende Typen folgen den gleichen Regeln.
use std::fmt::{self, Display};
struct Annotated<'a> {
value: &'a str,
label: String,
}
// Trait-Impl mit Lifetime
impl<'a> Display for Annotated<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}] {}", self.label, self.value)
}
}impl<'a> Display for Annotated<'a> — Lifetime wird sowohl in impl<'a> als auch am Typ aufgeführt. Das Trait selbst (Display) hat keine Lifetime-Parameter.
trait Source<'a> {
fn get(&self) -> &'a str;
}
struct Wrapper<'a> { inner: &'a str }
// Trait-Impl mit zwei Lifetime-Parametern (auch wenn beide gleich)
impl<'a> Source<'a> for Wrapper<'a> {
fn get(&self) -> &'a str {
self.inner
}
}Wenn das Trait selbst einen Lifetime-Parameter hat, müssen die Lifetimes von Typ und Trait kompatibel sein. Hier wählen wir die gleiche Lifetime 'a für beide.
Anonyme Lifetime '_
In impl-Blöcken kannst du '_ nutzen, wenn der Lifetime-Name nicht in den Methoden verwendet wird.
struct Buffer<'a> { data: &'a [u8] }
// Statt impl<'a> Buffer<'a> { ... } wenn 'a in Methoden nicht referenziert wird
impl Buffer<'_> {
pub fn len(&self) -> usize {
self.data.len()
}
}impl Buffer<'_> sagt: „Buffer mit irgendeinem Lifetime — name brauche ich nicht." Sauberer als impl<'a> Buffer<'a>, wenn 'a im Body nicht erwähnt wird.
Praxis: impl-Pattern in echtem Code
Parser-Struct mit Methoden
pub struct Parser<'src> {
input: &'src str,
pos: usize,
}
impl<'src> Parser<'src> {
pub fn neu(input: &'src str) -> Self {
Parser { input, pos: 0 }
}
// Output an 'src gebunden — Slice in den Source
pub fn rest(&self) -> &'src str {
&self.input[self.pos..]
}
// Konsumiert ein Zeichen — Output an 'src gebunden
pub fn next_char(&mut self) -> Option<char> {
let c = self.input[self.pos..].chars().next()?;
self.pos += c.len_utf8();
Some(c)
}
// Sucht nach Pattern, gibt Slice zurück
pub fn consume_until(&mut self, target: char) -> &'src str {
let start = self.pos;
while let Some(c) = self.input[self.pos..].chars().next() {
if c == target { break; }
self.pos += c.len_utf8();
}
&self.input[start..self.pos]
}
}Zero-Copy-Parser mit allen Output-Refs an die Source-Lifetime gebunden.
Iterator-Implementation
pub struct Lines<'a> {
input: &'a str,
position: usize,
}
impl<'a> Lines<'a> {
pub fn neu(input: &'a str) -> Self {
Lines { input, position: 0 }
}
}
// Iterator-Impl mit Lifetime
impl<'a> Iterator for Lines<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<&'a str> {
if self.position >= self.input.len() { return None; }
let rest = &self.input[self.position..];
let line_end = rest.find('\n').unwrap_or(rest.len());
let line = &rest[..line_end];
self.position += line_end + 1;
Some(line)
}
}
fn main() {
let text = "first\nsecond\nthird";
for line in Lines::neu(text) {
println!("{line}");
}
}Custom-Iterator mit Lifetime — Items sind Refs in den Source-String.
Builder mit Method-Chain
pub struct QueryBuilder<'src> {
table: &'src str,
conditions: Vec<&'src str>,
}
impl<'src> QueryBuilder<'src> {
pub fn neu(table: &'src str) -> Self {
QueryBuilder { table, conditions: Vec::new() }
}
pub fn and(mut self, cond: &'src str) -> Self {
self.conditions.push(cond);
self
}
pub fn build(&self) -> String {
let mut q = format!("SELECT * FROM {}", self.table);
if !self.conditions.is_empty() {
q.push_str(" WHERE ");
q.push_str(&self.conditions.join(" AND "));
}
q
}
}
fn main() {
let q = QueryBuilder::neu("users")
.and("active = true")
.and("age > 18")
.build();
println!("{q}");
}Builder mit Lifetime, Method-Chain durch mut self und Self-Return.
Mehrere Lifetimes — Source + Sink
pub struct Pipeline<'src, 'sink> {
source: &'src str,
sink: &'sink mut String,
}
impl<'src, 'sink> Pipeline<'src, 'sink> {
pub fn neu(source: &'src str, sink: &'sink mut String) -> Self {
Pipeline { source, sink }
}
pub fn transfer(&mut self) {
self.sink.push_str(self.source);
}
pub fn transform<F: Fn(&str) -> String>(&mut self, f: F) {
self.sink.push_str(&f(self.source));
}
}
fn main() {
let src = String::from("hello");
let mut dst = String::new();
let mut p = Pipeline::neu(&src, &mut dst);
p.transform(|s| s.to_uppercase());
println!("{dst}"); // "HELLO"
}Pipeline mit unabhängigen Source- und Sink-Lifetimes. Beide werden im impl-Block deklariert.
Anonyme Lifetime im impl
pub struct Reader<'buf> { data: &'buf [u8] }
// Wenn 'buf in Methoden nicht erwähnt wird — '_ ist sauberer
impl Reader<'_> {
pub fn len(&self) -> usize { self.data.len() }
pub fn is_empty(&self) -> bool { self.data.is_empty() }
}Bei trivialen Methoden, die nur &self brauchen, ist impl Reader<'_> lesbarer.
Trait-Impl für Lifetime-tragenden Typ
use std::fmt::{self, Display};
pub struct Tagged<'a> {
tag: &'a str,
value: i32,
}
impl<'a> Display for Tagged<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}={}", self.tag, self.value)
}
}
fn main() {
let tag = String::from("count");
let t = Tagged { tag: &tag, value: 42 };
println!("{t}"); // "count=42"
}Display-Trait für Lifetime-Struct. Standard-Pattern für eigene Typen mit Refs.
Generic-Methode in Lifetime-Impl
pub struct Stream<'a> { data: &'a [u8] }
impl<'a> Stream<'a> {
// Methode mit Generic-Parameter und eigenem Bound
pub fn process<F, R>(&self, processor: F) -> R
where F: FnOnce(&'a [u8]) -> R
{
processor(self.data)
}
}
fn main() {
let data = vec![1, 2, 3, 4, 5];
let stream = Stream { data: &data };
let sum = stream.process(|d| d.iter().map(|&x| x as i32).sum::<i32>());
println!("Sum: {sum}");
}Methode mit Generic-Parameter F und Closure-Bound mit 'a-Lifetime. Sehr flexibel — der Aufrufer entscheidet, was mit den Daten geschieht.
Interessantes
impl mit Lifetime: impl<'a> Type<'a>.
Lifetime im impl-Klammer deklariert, am Typ aufgeführt. Beide müssen den gleichen Namen tragen — sind ja dieselbe Lifetime.
&self hat eigene (kurze) Lifetime, oft elidiert.
Bei Methoden ist die self-Lifetime typischerweise kürzer als die Struct-Lifetime. Du kannst sie explizit machen (&'short self) oder elidieren lassen.
Output kann an Struct-Lifetime oder self-Lifetime gebunden sein.
-> &'a T (Struct-Lifetime) oder -> &T (elidiert auf self-Lifetime). Erste Variante ist freier — Aufrufer darf Output länger nutzen.
Methoden können eigene Lifetime-Parameter haben.
fn search<'short>(&self, needle: &'short str) — kurze Lifetime nur für das Argument, Struct-Lifetime weiter aktiv für Output.
Mehrere Lifetimes im Typ: alle im impl-Block deklarieren.
impl<'src, 'dst> CrossRef<'src, 'dst> — beide Parameter müssen genannt werden, sonst Compile-Fehler.
Trait-Impls funktionieren genauso wie inherent Impls.
impl<'a> Display for MyType<'a> — Lifetime an Impl und Typ. Trait selbst kann mit oder ohne eigene Lifetime-Parameter sein.
Anonyme Lifetime '_ für saubere Syntax.
impl Type<'_> wenn die Lifetime im Body nicht referenziert wird. Kürzer und idiomatisch.
Iterator-Impl mit Lifetime — sehr verbreitet.
impl<'a> Iterator for MyIter<'a> mit type Item = &'a T. Standard-Pattern für Zero-Copy-Iteratoren.
Weiterführende Ressourcen
Externe Quellen
- Rust Reference – Implementations
- Rust by Example – Lifetimes in Methods
- The Rust Book – Method Syntax