跳转到主内容生命周期:引用不能比数据活得更久 | 物联网民工

生命周期:引用不能比数据活得更久

C 开发者第一次看到 lifetime,很容易以为它是在给对象标注“活多久”。

这个理解不准确。

Rust 的 lifetime 不是运行时计时器,也不是对象真实存活时间的手写说明。它更像编译器用来检查引用关系的一套静态约束:一个引用不能比它指向的数据活得更久。

这件事在 C 里也一直存在,只是靠人脑判断。

C 里的悬垂指针

最典型的例子是返回局部变量地址:

const char *bad_name(void)
{
    char buf[32] = "sensor";
    return buf;
}

buf 在函数返回时已经不存在。返回出去的指针指向一段已经失效的栈空间。

类似问题还包括:

  • 返回局部数组地址
  • 保存调用方临时 buffer 的指针
  • free 后继续访问旧指针
  • realloc 后继续使用旧地址
  • 把指向短生命周期对象的指针放进长生命周期结构体

C 里这些问题不一定马上崩。它可能在另一次函数调用、一次栈覆盖、一次堆分配之后才表现出来。

Rust 的 lifetime 要阻止的,就是这类引用关系错误。

Rust 会拒绝返回局部引用

Rust 里类似代码无法通过编译:

fn bad_name() -> &str {
    let name = String::from("sensor");
    &name
}

问题不是 String 本身,也不是 &str 本身,而是这个引用指向 name 的内部数据。函数结束时,name 会释放,返回的引用就会悬空。

编译器看见的是这条关系:

返回值引用 -> 指向局部变量 name
name -> 函数结束时释放
返回值 -> 函数外继续使用

这条关系不成立,所以不能编译。

这就是 lifetime 的第一层意义:检查引用指向的数据是否还活着。

大多数 lifetime 不需要手写

很多函数不需要写 lifetime 标注:

fn first_byte(data: &[u8]) -> Option<u8> {
    data.first().copied()
}

这里返回的是 u8,不是引用。函数内部借用了 data,但没有把引用返回出去。

再看这个:

fn trim_name(name: &str) -> &str {
    name.trim()
}

它返回的 &str 来自输入 name。编译器能推断:返回值的有效范围不能超过输入引用。

所以不需要写成:

fn trim_name<'a>(name: &'a str) -> &'a str {
    name.trim()
}

这两个签名表达的是同一件事。显式写 lifetime 只是把编译器能推断的关系写出来。

标注 lifetime 是在说明引用关系

什么时候需要写 lifetime?通常是函数签名里有多个引用,编译器无法只靠规则判断返回值和哪个输入有关。

例如:

fn longer<'a>(a: &'a str, b: &'a str) -> &'a str {
    if a.len() >= b.len() {
        a
    } else {
        b
    }
}

'a 不是说 a 和 b 真实活得一样久。它表达的是一种约束:

返回的引用,要在 a 和 b 都仍然有效的范围内使用

换句话说,返回值可能来自 a,也可能来自 b,所以调用方拿到的引用不能比其中较短的那个输入活得更久。

这和 C 里的文档说明很像:

/* returns a pointer into either a or b; caller must keep both alive */
const char *longer(const char *a, const char *b);

Rust 把这条文档约定写进了类型。

结构体里保存引用也要说明关系

C 里结构体保存外部指针很常见:

struct View {
    const uint8_t *data;
    size_t len;
};

这个结构体不拥有数据,只是一个 view。问题是:data 指向的 buffer 必须比 View 活得更久。

Rust 里如果结构体保存引用,需要 lifetime 参数:

struct View<'a> {
    data: &'a [u8],
}

'a 表达的是:

View 不能比它借用的 data 活得更久

它不是让 View 拥有数据。如果你希望结构体拥有数据,就应该放 Vec<u8>:

struct OwnedBuffer {
    data: Vec<u8>,
}

这里的 API 设计边界很重要:

保存借用:struct View<'a> { data: &'a [u8] }
拥有数据:struct OwnedBuffer { data: Vec<u8> }

如果不想让调用方处理 lifetime,很多时候可以让结构体拥有数据,而不是保存引用。

‘static 不是“永远不释放”的万能标记

'static 容易被误解。

字符串字面量有 'static 生命周期:

let name: &'static str = "sensor";

因为这段字符串数据放在程序的静态区域,可以活到程序结束。

但不要把 'static 当成“修复 lifetime 错误”的万能标记。函数不能把局部变量引用强行标成 'static:

fn bad() -> &'static str {
    let s = String::from("sensor");
    &s
}

这仍然不成立。局部 s 函数结束就释放,引用不可能变成 'static。

看到 lifetime 错误时,正确问题通常不是“怎么加标注”,而是:

这个引用指向的数据,到底由谁拥有?
返回值或结构体要不要拥有数据?
这个引用是否被保存得太久?

这一篇先记住什么

C 和 Rust 的第一层对应关系是:

返回局部变量地址             -> Rust 拒绝返回悬垂引用
指针指向调用方 buffer        -> 引用不能超过调用方数据
结构体保存外部指针           -> struct View<'a>
结构体拥有自己的 buffer      -> struct Owned { data: Vec<u8> }
文档写“caller keeps alive”   -> lifetime 写进类型关系

lifetime 不是对象真实存活时间,也不是给变量手写寿命。

它表达的是引用关系:一个引用能在多大范围内被使用,取决于它指向的数据能活多久。

下一篇可以进入 enum 和模式匹配。那会从 C 里的整数状态码、宏常量和 switch,走到 Rust 用类型表达状态和错误分支。