Solution Patterns
Pattern 1: HRTB (Higher-Ranked Trait Bounds)
// Problem: Concrete lifetime vs generic lifetime
// ❌ This doesn't work with dyn
type ClosureFn<T> = dyn for<'a> FnOnce(&'a mut Connection) -> BoxFuture<'a, T>;
// ✅ Solution: Keep HRTB in generic bounds
async fn with_connection<F, T, Fut>(f: F) -> Result<T, Error>
where
F: for<'c> FnOnce(&'c mut Connection) -> Fut,
Fut: Future<Output = Result<T, Error>>,
{
let mut conn = get_connection().await?;
f(&mut conn).await
}
// Usage
with_connection(|conn| async move {
query("SELECT * FROM users").fetch_all(conn).await
}).await?;
Pattern 2: GAT + Object Safety
// Problem: GAT makes trait non-object-safe
// ❌ Can't use dyn with GAT
trait Repository {
type Row<'a>: RowView<'a>; // GAT
fn query<'a>(&'a self) -> Vec<Self::Row<'a>>;
}
// let repo: Box<dyn Repository> = ...; // Error!
// ✅ Solution: Layered architecture
trait InternalRepo {
type Row<'a>: RowView<'a>; // GAT for internal use
fn query_borrowed<'a>(&'a self) -> Vec<Self::Row<'a>>;
}
trait PublicRepo: Send + Sync {
fn query(&self) -> Vec<RowDto>; // Owned data
}
// Adapter converts borrowed -> owned
impl<T: InternalRepo> PublicRepo for T {
fn query(&self) -> Vec<RowDto> {
self.query_borrowed()
.into_iter()
.map(|row| row.to_dto())
.collect()
}
}
Pattern 3: Static Bound Conflicts
// Problem: 'static requirement conflicts with borrowing
// ❌ Can't borrow when 'static required
async fn bad_resolver(&self, ctx: &Context) -> Result<&Data> {
// Error: lifetime 'a not 'static
}
// ✅ Solution: Return owned data
async fn good_resolver(&self, ctx: &Context) -> Result<DataDto> {
let borrowed = self.repo.query().await?;
Ok(borrowed.to_owned_dto())
}
// Alternative: Use Arc for shared ownership
async fn shared_resolver(&self) -> Result<Arc<Data>> {
Ok(Arc::clone(&self.cached_data))
}
Pattern 4: Lifetime Elision Edge Cases
// Problem: Compiler can't infer lifetime
// ❌ Ambiguous lifetime
struct Parser<'a> {
input: &'a str,
}
impl<'a> Parser<'a> {
fn parse(&self) -> Result<&str, Error> {
// Which lifetime? 'a or 'self?
// Compiler can't tell
}
}
// ✅ Explicit lifetime annotation
impl<'a> Parser<'a> {
fn parse<'b>(&'b self) -> Result<&'a str, Error> {
// Returns data from input, not self
Ok(&self.input[..10])
}
}
Pattern 5: Async + Lifetime Conflicts
// Problem: Holding references across await
// ❌ Can't hold borrow across await
async fn bad_async() {
let data = get_data();
let borrowed = &data.field;
some_async_call().await; // Error: data might move
use_borrowed(borrowed);
}
// ✅ Solution 1: Clone before await
async fn good_async_clone() {
let data = get_data();
let owned = data.field.clone();
some_async_call().await; // OK
use_owned(&owned);
}
// ✅ Solution 2: Drop borrow before await
async fn good_async_scope() {
let data = get_data();
let value = {
let borrowed = &data.field;
extract_value(borrowed)
}; // borrow dropped
some_async_call().await; // OK
}
Common Conflict Patterns
| Pattern | Cause | Solution | |---------|-------|----------| | HRTB → dyn | Concrete vs universal lifetime | Use generic functions | | GAT → dyn | Variable-sized associated types | Layered design with owned DTOs | | 'static + borrow | Lifetime contradiction | Return owned data | | Async + borrow | Future holds state across await | Clone or drop before await | | Closure capture + Send | Lifetime issues | Use 'static or Arc |
When to Give Up Borrowing
Performance vs Maintainability
// Decision factors:
fn should_use_owned() -> bool {
// ✅ Use owned if:
// - Complex lifetime interactions
// - API boundaries
// - Async contexts
// - Multi-threaded sharing
// ✅ Keep borrowing if:
// - Large data structures
// - Hot path performance
// - Simple lifetime relationships
// - Internal implementation only
true
}
Rule of Thumb
- API layer: Default to owned data
- Internal impl: Borrow when beneficial
- Performance hotspot: Profile first, then optimize
- High complexity: Fall back to owned
Workflow
Step 1: Diagnose Error
Common errors:
"one type is more general" → HRTB + dyn conflict
"lifetime may not live long enough" → Borrow exceeds scope
"cannot be made into object" → GAT or HRTB in trait
"does not live long enough" → Early drop
Step 2: Choose Strategy
Options:
→ Simplify: Remove abstraction
→ Split: Separate borrowed/owned layers
→ Clone: Accept allocation cost
→ Arc: Shared ownership
→ Redesign: Change data flow
Step 3: Validate Solution
Check:
→ Compiles without hacky workarounds
→ Reasonable complexity
→ Performance acceptable
→ Maintainable long-term
Debugging Techniques
Minimize
// Reduce to minimal reproduction
// Remove generics, traits, async one by one
// Find the core conflict
Explicit Lifetimes
// Write out all lifetime parameters
// Makes relationships visible
fn explicit<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}
Accept Reality
// Not all designs can compile
// Sometimes owned data is the answer
// Complexity has limits
Review Checklist
When dealing with complex lifetimes:
- [ ] HRTB not used with dyn trait objects
- [ ] GAT traits have owned alternative for object safety
- [ ] 'static bounds justified and documented
- [ ] Async functions don't hold borrows across await
- [ ] Lifetime elision not hiding ambiguity
- [ ] Complex lifetimes have explicit annotations
- [ ] Considered owned data alternative
- [ ] Design simplification explored first
Verification Commands
# Check for lifetime errors
cargo check
# Expand to see generated code
cargo expand
# Verify no borrow checker issues
cargo clippy
Related Skills
- rust-ownership - Basic lifetime fundamentals
- rust-async - Async lifetime patterns
- rust-type-driven - Type-level design
- rust-trait - Trait object constraints
- rust-performance - When to optimize with borrowing
Localized Reference
- Chinese version: SKILL_ZH.md - 完整中文版本,包含所有内容