生命周期

本页目录

生命周期参数描述借用之间必须满足的有效性关系,不是运行时计时器。标注不会延长数据的生命,也不会让局部变量在函数返回后继续存活;实现与调用都必须满足签名给出的约束。

同名参数表达关系,而不是相同实际寿命

外层 String内层 String结果可使用的共同区域两个输入共用生命周期约束,不会延长较短输入choose 的签名允许返回任意输入;调用方不能仅凭这次选择猜测更长有效期。
fn choose<'a>(left: &'a str, right: &'a str, use_left: bool) -> &'a str {
    if use_left { left } else { right }
}
fn main() {
    let outer = String::from("outer");
    {
        let inner = String::from("inner");
        let selected = choose(&outer, &inner, true);
        assert_eq!(selected, "outer");
    }
}

两个输入不必原本具有完全相同的存活范围;编译器可以在允许的关系下选择共同满足调用与结果使用的区域。由于签名允许返回任一个输入,调用方不能把结果带到 inner 失效之后,仅凭 use_left 这次为 true 来扩大契约。

如果函数永远返回 left,应把输出只关联到 left 的生命周期,避免无谓地约束 right。生命周期设计也是 API 精度设计。

只依赖左输入时,就让右输入独立

fn left_with_check<'a>(left: &'a str, right: &str) -> &'a str {
    assert!(!right.is_empty());
    left
}
fn main() {
    let outer = String::from("outer");
    let result;
    {
        let temporary = String::from("validation only");
        result = left_with_check(&outer, &temporary);
    }
    assert_eq!(result, "outer");
}

right 只在调用期间参与检查,不是返回引用的数据来源,因此可以更早失效。如果把 right 也写成 &'a str,调用者就会受到不必要的共同约束;编译器按公开签名检查调用,不会针对每次调用窥探实现来放宽承诺。

返回值究竟依赖谁?

这里把输出的依赖关系画成可调整区间。先保持使用位置不变,缩短 right;再把签名改成输出只依赖 left。变化来自签名消除了无关依赖,而不是标注延长了任何对象的生命。

正在呈现知识画面
返回值究竟依赖谁?

输出关联两个输入时受共同区域约束;只关联 left 时,right 可以先结束。

省略规则不靠猜测函数体

函数参数中省略的引用生命周期分别获得独立参数。只有一个输入生命周期时,它用于省略的输出生命周期;方法有 &self 或 &mut self 接收者时,输出默认与接收者借用关联。无法按规则确定就需要显式表达。省略规则

struct Excerpt<'s> { part: &'s str }
impl<'s> Excerpt<'s> {
    fn tied_to_self(&self) -> &str { self.part }
    fn tied_to_source(&self) -> &'s str { self.part }
}
fn main() {
    let source = String::from("chapter");
    let result;
    {
        let excerpt = Excerpt { part: &source };
        assert_eq!(excerpt.tied_to_self(), "chapter");
        result = excerpt.tied_to_source();
    }
    assert_eq!(result, "chapter");
}

两种方法返回同一个字段,但签名承诺不同。第二种输出依赖 source 的有效期,可以在 Excerpt 包装销毁后继续使用;第一种默认还受本次 &self 借用约束。结构持有引用时,生命周期约束保证其有效使用不超过数据允许的范围,不是在引用上加一个运行时到期字段。

source:String拥有整段 "a,b,c"Fields<'input>保存剩余输入的切片返回 &'input str字段内容借用 sourcenext_field 只短暂独占解析器,可同时保留之前返回的字段解析器状态的借用与输入文本的借用是两条不同关系成立的原因是只推进切片视图、不改写输入;可覆盖缓冲的读取器需要另一个契约。

一个可以同时保留多个结果的借用解析器

struct Fields<'input> { rest: Option<&'input str> }
impl<'input> Fields<'input> {
    fn new(input: &'input str) -> Self { Self { rest: Some(input) } }
    fn next_field(&mut self) -> Option<&'input str> {
        let remaining = self.rest.take()?;
        match remaining.split_once(',') {
            Some((first, rest)) => {
                self.rest = Some(rest);
                Some(first)
            }
            None => Some(remaining),
        }
    }
}
fn main() {
    let source = String::from("a,,c,");
    let first;
    {
        let mut fields = Fields::new(&source);
        first = fields.next_field().unwrap();
        let second = fields.next_field().unwrap();
        assert_eq!((first, second), ("a", ""));
        assert_eq!(fields.next_field(), Some("c"));
        assert_eq!(fields.next_field(), Some(""));
        assert_eq!(fields.next_field(), None);
    }
    assert_eq!(first, "a");
}

这只是逗号分隔字段,不是带引号和转义的完整 CSV。Option 区分“还有最后一个空字段”和“已经结束”,因此尾逗号会产生 Some("") 后再产生 None。next_field 返回 &'input str,使输出关联输入,而非本次 &mut self;解析器本身销毁后,first 仍可在 source 有效时使用。

如果把返回类型省略成 Option<&str>,方法规则会把输出与本次 self 借用关联,保留旧结果时就可能不能再次调用 next_field。这不是给编译器增加一个随意的标记,而是准确声明结果所来自的数据。

&'static T 与 T: 'static

&'static T 表示借用允许覆盖整个程序执行期,例如字符串字面量。T: 'static 表示 T 不包含不满足 'static 约束的借用;拥有数据的 String 可以满足它,即使这个 String 在下一行就被 drop。

fn accepts_static<T: 'static>(value: T) { drop(value); }
fn main() {
    accepts_static(String::from("short lived owner"));
    let literal: &'static str = "literal";
    accepts_static(literal);
}

move 一个短引用不会让它变成 'static。Box::leak 可以在 T 满足相应约束时产生长借用,但泄漏不应成为普通生命周期错误的默认修复手段;先考虑返回拥有值、缩短借用或调整资源归属。

用 Cow 表达“可能借用,也可能生成新内容”

use std::borrow::Cow;
fn lowercase_ascii(text: &str) -> Cow<'_, str> {
    if text.bytes().any(|b| b.is_ascii_uppercase()) {
        Cow::Owned(text.to_ascii_lowercase())
    } else {
        Cow::Borrowed(text)
    }
}
fn main() {
    assert!(matches!(lowercase_ascii("ready"), Cow::Borrowed(_)));
    assert!(matches!(lowercase_ascii("READY"), Cow::Owned(_)));
    let saved;
    {
        let input = String::from("READY");
        saved = lowercase_ascii(&input).into_owned();
    }
    assert_eq!(saved, "ready");
}

返回类型保留可能借用输入的关系,调用者不能仅凭“这次字符串有大写”就把整个 Cow 当成 'static。into_owned 明确取得 String:借用分支需要复制,拥有分支直接转交已有 String。这个函数只处理 ASCII 大小写,不是 Unicode 大小写折叠或身份规范化算法。Cow

协变要明确是哪个参数

'a: 'b 表示 'a outlives 'b,即 'a 满足至少覆盖 'b 的要求。引用能否缩短还取决于类型参数的位置:

类型对外层 'a对内部 T
&'a T协变协变
&'a mut T协变不变
UnsafeCell不适用不变

可变引用并非对自身生命周期不变;它对被引用的 T 不变。若 T 又是 &'static str,就不能把 &mut T 当作能写入任意短借用的位置。协变规则

fn shorter<'long: 'short, 'short>(value: &'long mut i32) -> &'short mut i32 {
    value
}
fn main() {
    let mut number = 1;
    *shorter(&mut number) += 1;
    assert_eq!(number, 2);
}

反方向的风险是写入:如果一个 &mut &'static str 能接受短命 String 的引用,外层位置之后仍被当作 'static 使用,就会悬垂。这解释了内部 T 必须保持不变,和临时缩短独占借用不是一回事。

允许写入短引用,会留下什么?

要理解不变性,先看写入通道,而不是只看当前读取是否安全。原位置保存 &'static str;如果把内部类型放宽,短借用视图就可能把 local 写进去,视图消失后原位置却仍承诺 static。缩短外层借用不会改变可写入类型,因此是另一个问题。

正在呈现知识画面
允许写入短引用,会留下什么?

&mut T 对外层生命周期协变,对内部 T 不变;后者防止把短引用写进要求 static 的位置。

HRTB:回调必须适用于每次调用的借用

fn apply<F>(function: F)
where F: for<'a> Fn(&'a str) -> &'a str,
{
    let local = String::from("  local  ");
    assert_eq!(function(&local), "local");
    assert_eq!(function(" literal "), "literal");
}
fn trim(text: &str) -> &str { text.trim() }
fn main() { apply(trim); }

for<'a> 要求回调对任意适用的 'a 都满足接口,允许 apply 用内部临时借用调用它。它不同于由外层调用者选定一个特定 'a。Fn(&str) -> &str 的省略形式也可表达这里的高阶生命周期关系。

PhantomData 不会自动验证裸指针

PhantomData<&'a T> 可以让一个没有实际引用字段的结构,在类型检查中表达逻辑借用关系,影响生命周期、协变与 auto trait 推导。但它不验证地址,也不替你保证构造时指针来自该借用。

封装裸指针的迭代器仍需从有效切片等入口建立范围与生命期约束,维护初始化、对齐和访问规则。只是往结构里加一个标记,再随意返回 &'a T,仍可能不 sound。相关证明见 unsafe 契约。

为生命周期错误选择对应的接口修正

错误来自哪里应调整的关系
返回局部创建值的引用返回拥有值,或让调用者提供存储
输出被无关输入限制拆分输入生命周期,只关联实际来源
解析器旧结果阻止下一次调用确认结果依赖外部输入还是可覆盖的内部缓冲
异步任务要求 'static转移拥有数据,或使用允许短借用的作用域机制
回调想保存内部临时引用保存拥有副本,或改为仅在回调期间消费
共享与可变位置的生命周期转换失败检查型变,尤其是 &mut T 内部的 T

for<'a> 回调能够接收调用方内部临时数据,恰好意味着它不能任意把这次短借用保存到更长寿命容器中。若要保留数据,可以在回调中 to_owned;若只需统计长度或解析为拥有数字,返回这些结果即可。生命周期签名应与存储策略一致,而不是靠加更多 'a 或 'static 碰运气。