Bisher haben wir Lifetimes immer als konkrete Variablen behandelt: 'a ist eine bestimmte Lifetime, gewählt bei jedem Funktions-Aufruf. Higher-Ranked Trait Bounds (HRTB) sind eine Erweiterung: sie sagen „dieses Bound gilt für alle Lifetimes" — quantifiziert über die Lifetime, nicht über einen einzelnen Wert. Die Syntax ist for<'a>. HRTB tauchen klassisch bei Closures auf, die mit beliebigen Eingabe-Lifetimes arbeiten müssen. Wer das Konzept beherrscht, versteht den letzten verbleibenden Lifetime-Mysterium.
Das Problem ohne HRTB
Schauen wir uns ein Beispiel an, bei dem normale Lifetime-Bounds nicht reichen.
// Funktion akzeptiert eine Closure, die einen &str nimmt
fn use_closure<F>(f: F) where F: Fn(&str) -> usize {
let s1 = String::from("hello");
let s2 = String::from("longer-string");
println!("{}", f(&s1));
println!("{}", f(&s2));
}
fn main() {
use_closure(|s| s.len());
}Diese Funktion compiliert! Aber: was ist die Lifetime des &str in Fn(&str)? Sie ist nicht eine bestimmte Lifetime, sondern eine für jeden Aufruf neue. Die Closure muss für jede mögliche Lifetime funktionieren.
Das ist eigentlich HRTB im Hintergrund — der Compiler interpretiert Fn(&str) als for<'a> Fn(&'a str). Es funktioniert ohne explizite Syntax, weil die Elision die HRTB-Form ableitet.
Wann HRTB explizit nötig wird
Bei komplexeren Signaturen scheitert die Elision. Dann musst du HRTB explizit ausschreiben.
// Closure mit komplexem Output, der die Input-Lifetime erbt
fn use_extractor<F>(f: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
let s = String::from("hello world");
let first = f(&s);
println!("{first}");
}
fn main() {
use_extractor(|s| s.split_whitespace().next().unwrap_or(""));
}for<'a> Fn(&'a str) -> &'a str sagt: für jede mögliche Lifetime 'a, die Closure nimmt eine Ref mit 'a und gibt eine Ref mit derselben 'a zurück.
Das ist nicht das Gleiche wie <'a, F: Fn(&'a str) -> &'a str>. Letzteres würde bedeuten: es gibt eine bestimmte Lifetime 'a, für die das gilt. Mit HRTB sagst du: für jede.
for-Syntax visualisiert
for<'a> Trait<...> ist eine universelle Quantifizierung über 'a.
// Mathematisch:
// ∀ 'a. F: Fn(&'a str) -> &'a str
//
// In Rust:
// for<'a> Fn(&'a str) -> &'a str
//
// Bedeutung: Für ALLE Lifetimes 'a, die Closure muss vom Typ
// Fn(&'a str) -> &'a str sein.
fn process<F: for<'a> Fn(&'a str) -> &'a str>(f: F) {
// Die Closure muss mit JEDER beliebigen Lifetime arbeiten können
let s1 = String::from("short");
let s2 = String::from("longer");
let _ = f(&s1);
let _ = f(&s2);
}Anders ausgedrückt: die Closure ist lifetime-polymorph. Sie kann mit beliebigen Lifetimes umgehen, ohne dass beim Aufruf eine bestimmte fixiert wird.
Wann braucht man HRTB wirklich?
HRTB wird explizit nötig in diesen Fällen:
Closures mit Lifetime-verbindendem Output
// OHNE HRTB: würde nicht funktionieren, weil 'a ist nicht im Scope
// fn apply<'a, F: Fn(&'a str) -> &'a str>(f: F, ...)
// MIT HRTB: 'a wird beim Closure-Aufruf gewählt, nicht beim Funktions-Aufruf
fn apply<F>(f: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
let s1 = String::from("a");
let s2 = String::from("b");
let _r1 = f(&s1);
let _r2 = f(&s2);
}Closure als Argument für mehrere unterschiedliche Lifetimes
Wenn die Closure innerhalb der Funktion mit verschiedenen Ref-Lifetimes aufgerufen wird, muss sie HRTB sein.
Trait-Objekte mit Lifetime-Methoden
trait Parser {
fn parse<'a>(&self, input: &'a str) -> &'a str;
}
// Trait-Object mit HRTB
fn use_parser(p: &dyn Parser) {
let s = String::from("data");
let _result = p.parse(&s);
}
// p.parse erzeugt implizit HRTB: for<'a> fn(&'a str) -> &'a strBei Methoden mit eigenen Lifetime-Parametern in Trait-Objekten ist die HRTB-Form implizit. Du merkst das selten — nur wenn du die Trait-Methoden auf Trait-Objekten generisch behandelst.
HRTB vs. konkrete Lifetime
Der Unterschied wird klar im Vergleich:
// Variante A: konkrete Lifetime
fn variante_a<'a, F>(f: F)
where F: Fn(&'a str) -> &'a str
{
// Hier muss 'a beim Funktions-Aufruf bestimmt sein.
// Aber: woher kommt es? Aus den Argumenten? Es gibt keine ref-Argumente.
// → 'a ist eine FREE Lifetime, der Aufrufer muss sie bestimmen.
// In den meisten Fällen unbrauchbar.
let _ = f;
}
// Variante B: HRTB
fn variante_b<F>(f: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
// 'a ist quantifiziert über die Closure. Beim Aufruf von f
// wird 'a passend zur jeweiligen Aufrufstelle gewählt.
let s = String::from("test");
let _ = f(&s); // 'a = scope von s
}Variante A funktioniert nur, wenn 'a aus dem Kontext bekannt ist. Variante B mit HRTB funktioniert immer — die Closure-Lifetime ist intern frei wählbar.
HRTB in der Stdlib
Du bist HRTB schon begegnet, ohne es zu wissen.
// Iterator-Methoden wie .filter() nutzen HRTB implizit
let v = vec![1, 2, 3, 4, 5];
let evens: Vec<&i32> = v.iter().filter(|&&x| x % 2 == 0).collect();
// .filter expects: F: FnMut(&Self::Item) -> bool
// Self::Item ist &i32 → die Closure muss &&i32 nehmen
// Implizit: for<'a> FnMut(&'a &i32) -> boolIterator-Adapter wie filter, map, find haben implizit HRTB-Form für ihre Closure-Argumente. Daher funktionieren sie mit beliebigen Item-Lifetimes.
Praxis: HRTB in echtem Code
Higher-Order-Funktion mit Lifetime-erhaltender Closure
pub fn process_strings<F>(strings: Vec<String>, extractor: F)
where F: for<'a> Fn(&'a str) -> &'a str
{
for s in &strings {
let extracted = extractor(s);
println!("{} -> {}", s, extracted);
}
}
fn main() {
let words = vec![
String::from("hello world"),
String::from("rust language"),
String::from("learn fast"),
];
process_strings(words, |s| s.split_whitespace().next().unwrap_or(""));
}Klassisches HRTB-Pattern: Higher-Order-Funktion mit Closure, die für beliebige Eingabe-Lifetimes funktionieren muss.
Trait mit Lifetime-Methode
pub trait Tokenizer {
fn tokens<'a>(&self, input: &'a str) -> Vec<&'a str>;
}
struct WhitespaceTokenizer;
impl Tokenizer for WhitespaceTokenizer {
fn tokens<'a>(&self, input: &'a str) -> Vec<&'a str> {
input.split_whitespace().collect()
}
}
// Verwender mit Trait-Objekt → HRTB implizit
pub fn count_tokens(tokenizer: &dyn Tokenizer, samples: &[String]) -> usize {
samples.iter().map(|s| tokenizer.tokens(s).len()).sum()
}
fn main() {
let t: &dyn Tokenizer = &WhitespaceTokenizer;
let samples = vec![String::from("a b c"), String::from("d e")];
println!("Total: {}", count_tokens(t, &samples)); // 5
}Trait mit Lifetime-Methode. Beim Trait-Objekt-Gebrauch ist die HRTB-Form implizit.
Closure als gespeichertes Member
pub struct StringProcessor {
transform: Box<dyn for<'a> Fn(&'a str) -> String>,
}
impl StringProcessor {
pub fn neu<F>(f: F) -> Self
where F: for<'a> Fn(&'a str) -> String + 'static
{
StringProcessor { transform: Box::new(f) }
}
pub fn apply(&self, input: &str) -> String {
(self.transform)(input)
}
}
fn main() {
let upper = StringProcessor::neu(|s| s.to_uppercase());
println!("{}", upper.apply("hello"));
}Closure als Struct-Member mit HRTB-Bound. Die Closure muss mit beliebigen Eingabe-Lifetimes funktionieren, daher HRTB.
Iterator-Pipeline mit HRTB
pub fn filter_strings<F>(items: &[String], pred: F) -> Vec<&str>
where F: for<'a> Fn(&'a str) -> bool
{
items.iter().filter(|s| pred(s)).map(|s| s.as_str()).collect()
}
fn main() {
let words = vec![
String::from("apple"), String::from("banana"),
String::from("cherry"), String::from("date"),
];
let longs = filter_strings(&words, |s| s.len() > 4);
println!("{longs:?}"); // ["apple", "banana", "cherry"]
}Filter-Funktion mit HRTB. Die Predicate-Closure wird mit jeder String-Lifetime aufgerufen.
Mehrere HRTBs in einer Signatur
pub fn pipeline<P, T>(input: String, predicate: P, transform: T) -> Option<String>
where
P: for<'a> Fn(&'a str) -> bool,
T: for<'a> Fn(&'a str) -> String,
{
if predicate(&input) {
Some(transform(&input))
} else {
None
}
}
fn main() {
let result = pipeline(
String::from("hello"),
|s| s.len() > 3,
|s| s.to_uppercase(),
);
println!("{result:?}"); // Some("HELLO")
}Zwei HRTBs in einer Signatur. Jede Closure ist unabhängig lifetime-polymorph.
Custom-Trait mit HRTB-Bound
pub trait StreamProcessor {
fn handle<'a>(&self, event: &'a str) -> &'a str;
}
pub fn run_stream<P: StreamProcessor>(processor: &P, events: &[String]) {
for e in events {
let response = processor.handle(e);
println!("{e} -> {response}");
}
}
struct EchoProcessor;
impl StreamProcessor for EchoProcessor {
fn handle<'a>(&self, event: &'a str) -> &'a str {
event
}
}
fn main() {
let p = EchoProcessor;
let events = vec![String::from("a"), String::from("b")];
run_stream(&p, &events);
}Trait mit Lifetime-Methode. Beim generischen Verwender wird die HRTB-Form implizit angenommen.
Interessantes
HRTB = for<'a>-Syntax für Lifetime-quantifizierte Bounds.
„Für alle Lifetimes 'a, dieses Trait-Bound gilt". Unterscheidet sich fundamental von einer konkreten Lifetime, die aus dem Kontext stammt.
Klassisch bei Closures mit Lifetime-verbindendem Output.
for<'a> Fn(&'a str) -> &'a str — die Closure verbindet Input- und Output-Lifetime. Geht ohne HRTB nicht.
Oft implizit — Elision macht HRTB für dich.
Fn(&str) in einer Generic-Signatur wird vom Compiler als for<'a> Fn(&'a str) interpretiert. Du merkst HRTB selten, weil es automatisch greift.
Explizit nötig bei komplexen Signaturen.
Bei Closure-Bounds, die Output-Lifetimes mit Input-Lifetimes verbinden. Wenn der Compiler die Elision nicht eindeutig auflösen kann.
HRTB ≠ konkrete Lifetime in <'a>.
fn foo<'a, F: Fn(&'a str)> hat eine spezifische Lifetime, die beim Aufruf festgelegt wird. for<'a> quantifiziert über alle Lifetimes — flexibler und meist das Gewünschte.
Trait-Methoden mit Lifetime-Parameter erzeugen implizit HRTB.
Bei dyn Trait-Usage wird die Trait-Methode als HRTB-Form behandelt. Notwendig für Lifetime-Polymorphie über Trait-Objekt.
Iterator-Adapter nutzen HRTB im Hintergrund.
filter, map, find etc. haben implizit HRTB-Bounds. Daher funktionieren sie mit beliebigen Item-Lifetimes — du merkst es selten.
Wenn der Compiler „may not live long enough“ sagt, prüfe HRTB.
Eine der häufigsten Lösungen für komplexe Closure-Bounds: HRTB-Form explizit ausschreiben. Damit wird die Bedeutung „funktioniert für alle Lifetimes" sichtbar.
Weiterführende Ressourcen
Externe Quellen
- The Rustonomicon – Higher-Rank Trait Bounds
- Rust Reference – Higher-Ranked Trait Bounds
- Rust Blog – Inside Rust: HRTBs