'static 生命周期约束总给人一种不好拿捏的感觉,因为它出现在不同位置上时含义也会有所不同,需要仔细甄别。此外,它还总会和其他一些机制关联着一起出现,比如闭包、Fn trait、生命周期约束以及多线程场景,这会增加理解的困难度,所以,对 'static 感到困惑是正常的。本文会带领大家全面了解一下 'static 生命周期约束。本文原文链接:https://laurence.blog.csdn.net/article/details/165619915 禁止转载。
1. 从 static 变量谈起
static X: i32 = 42;
const Y: i32 = 42;
跟其他语言一样,static 变量的根本特征是:它的数据会被写进程序的数据段(.rodata / .data / .bss),进程启动时会装载到内存中,然后一直持续到程序结束,内存位置都不会发生变动。
所以,通常来说,static 变量是全局性的,但这不是绝对的。应该这样说:static 变量具备成为全局变量的条件,但是否是全局的取决于声明它的位置,因为,除了可以在 module 级别声明外,我们也可以在一个函数的局部作用域中声明一个 static 变量,它同样是放在数据段,运行时加载进内存的,只不过,它声明的作用域限制了它的“可见性”。
经常与 static 变量一起比较的是 const 常量,static 变量有自己确切的内存地址,可以对它:取引用、取地址,而 const 常量更像是它代表的那个值的“别名”,它没有自己的内存地址,在编译时,遇到使用到它的地方,编译器会就地物化一份这个值。
2. 'static 生命周期约束
'static 表示其标记对象的生命周期是程序的整个运行期,也就是说这个对象在程序运行期间会一直存活,结合它所处的“上下文”(所处的位置),'static 可以出现在四种位置上,以及一种容易被认为是可能的位置,但实际上是不允许的,以下是具体描述和示例:
| 修饰引用 | 声明引用可以活到程序结束 | let s: &'static str = “hello” | ☑ |
| 作为类型约束 | 约束类型不得含有外部引用 | T: 'static | ☑ |
| supertrait 的位置上(依然是类型约束) | 约束实现者不得含有外部引用 | trait Foo: 'static {…} | ☑ |
| 结构体类型的尖括号中 | 作为一个具体的生命周期类型 | b: Bar<'static> | ☑ |
| 结构体定义的尖括号中 | 不合法 | struct Bar<'static> {…} // error! | ☒ |
2.1 修饰引用
使用 'static 修饰一个引用时意思是“这个引用能一直活到程序结束,也就是“你想持有多久都行”。显然,不是随便什么引用加上个 'static 修饰就能活到程序结束,引用的“底气”来自于它的值,是因为引用的值能“贯穿程序的整个生命周期”,所以引用才有机会冠以 'static 修饰,那都有哪些值具备这种能力呢?
1. static 变量的引用
这是最常见的 'static 引用的来源:
static X: i32 = 42;
let a: &'static i32 = &X;
2. 字面量的引用
let b: &'static str = "hello";
const N: i32 = 7;
let c: &'static i32 = &N;
3. Box::leak 泄漏的值
let d: &'static String = Box::leak(Box::new(String::from("hello")));
Box::leak() 函数会消耗掉 Box 实例,然后把它存放在堆上的数据泄漏出来,这时候我们可以使用一个 'static 引用来接住这个泄漏出来的数据,就像上面示例代码做的那样。这种处理方法主要用于在程序的剩余生命期内保留数据。
示例中的 d 反而是对 'static 引用的一种更深刻的表达,作为一个 'static 引用,它指向的值不在数据段,没有任何 static 声明,但它依然成立!它所指向的数据(内存中的位置)会在程序结束前一直有效。
2.2 作为类型约束
2.2.1 'static 作为类型约束的含义
当 'static 作为类型约束时,它表达的含义是:这个类型内部不得含有任何外部引用('static 引用除外),说得再具体一些的话就是:结构体不得含有任何引用类型的字段('static 引用除外),枚举、元组等同理。这里顺带解释一个偶尔会有人使用的概念:短期借用、短生命周期引用,它们的意思就是:除 'static 引用之外的所有引用,因为每次都说:不得含有任何引用类型('static 引用除外)会比较麻烦,所以有人把排除掉 'static 引用后的引用类型叫:短期借用或短生命周期引用,所以刚才的表述也可说成是:结构体不得含有任何短生命周期引用,这个“短”也不见得真有多短,只是相对于 'static 而言的。
这里我们需要强化两个知识点:首先是“类型约束”:类型约束1主要包括:trait bound(特质约束)、lifetime bound(生命周期约束)两种,它们都是用来修饰泛型的“类型参数”的,记住:没有泛型就没有所谓的“约束”;然后是“类型”有哪些?也就是“类型约束”都是作用在什么上面?最常见的当然是结构体!对于结构体来说,如果它有 'static 约束,就是指不允许它含有任何引用类型的字段('static 引用除外),如果是枚举和元组,同样是看它的组成元素里有没有任何引用类型('static 引用除外),注意:'static 约束的检查机制是递归检查类型中的所有字段,包括字段中的字段,只要发现一个引用类型('static 引用除外),就不满足,一个都没有才满足。另外,记住:trait 从来不是一种类型,类型约束从来不是作用到一个 trait 上,而是作用到它的实现者上,即使见到 trait Foo: 'static {…} 这样写法,也不要错误的认为 'static 可以约束 trait,它实际上约束的还是 Foo 的实现者,是要求它的实现者内部不得含有任何引用类型的字段。下面,我们看一个示例,既然是要展示类型约束,那必定要有泛型,所以,还是要引入一个泛型函数作为 driver:
struct Owned {
name: String,
}
// 定义结构体时只要出现引用,如果不是 'static 引用,必须要引入生命周期标注!
struct Borrowed<'a> {
name: &'a str,
}
fn check<T: 'static>(_: T) {}
fn main() {
let s = String::from("hi");
// Owned 不含引用,满足:T: 'static 约束
check(Owned { name: s.clone() }); // ☑
// Borrowed 含有引用,不满足:T: 'static 约束
check(Borrowed { name: &s }); // ☒
}
有些人会对 'static 作为类型约束时的含义感到困惑,觉得没法把它本来的语义和对类型的约束效果联系起来,所以直接“死记硬背”了它作为类型约束时的含义:不能含有外部引用。实际上, 'static 作为类型约束时表达出的语义还是很自然的: 'static 修饰的是一个类型,套用它的原始语义的话,意思是说:这个类型可以一直活到程序运行结束,类型又不是一个具体值,本来不就是一直存在吗?这个想法就被“惯性思维”带跑了,别忘了:类型里可以包含引用字段,有了引用,类型的生命周期就会对外部的值产生的依赖,这时候,类型就必须添加生命周期标注了,除非依赖的是 'static 引用,也就是说:从生命周期这个维度观察,类型可以分成两种:一种是生命周期依赖不受外界影响的,另一种是生命周期会依赖外部值的生命周期的,具体到代码上的表现就是:前者只含有值或 'static 引用;后者则在定义中引入了外部引用(如果出现就必须进行生命周期标注),只是 Rust 没有给这两种类型起过正式的称谓。但是,'static 约束刚好能精准地将这两种类型区分开了,满足 'static 约束的就是第一种类型,不满足的就是第二种类型,所以,我们在日常表述时,说:“一个类型是 'static 的”就是说它不包含引用类型('static 引用除外);说:“一个类型不是 'static 的”就表示它内部一定有短生命周期引用。
2.2.2 满足 'static 约束的类型种类
接下来,我们再看满足约束的实现者这一侧,我们说:一个类型是否满足 'static 就是看它有没有外部引用,这个描述虽然正确,但不够细致,一个类型是否含有外部引用并不是单单看它的字段有没有引用类型这么简单(当然,这是其中一种情形),有两方面的因素会影响最终的结果:
① 一个类型,比如一个结构体,如果引入了泛型,那它就不是一个单一结构体了,而是一个结构体“模板”,此时,它是否满足 'static 约束就不能单单看它自身有没有引用字段了,还要看具体的类型参数,如果在实例化时传入了引用类型,那这个具体的实例同样也是不满足 'static 约束的(参考下面示例中的 struct C);
② 一个类型,比如一个结构体,确实依赖到了引用字段,此时必须在结构体声明上添加生命周期标注,但是,如果在实例化时传入的是 'static 引用,反而可以满足 'static 约束(参考下面示例中的 struct D)。
以下列举了四种典型的类型(结构体)定义,有的可以直接确定是否满足 'static 约束,有的则是因为引入了泛型要再递进一层分析泛型类型是否满足 'static 约束,还有的是因为引入了生命周期参数,如果传入是 'static 引用,就满足,否则就不满足:
| 1 | struct A { x: i32 } | 总是 |
| 2 | struct B { s: &'static str } | 总是 |
| 3 | struct C { x: T } | 取决于 T,如果 T 满足 'static 约束,则 C 满足,反之亦然 |
| 4 | struct D<'a> { s: &'a str } | 取决于具体的生命周期,如果是 'static,则 D 满足,否则不满足 |
2.2.3 特别场景:作为闭包的类型约束
我觉得需要特别把 'static 作为闭包的类型约束单独拿出来介绍一下。首先,闭包也是有类型的(Fn、FnMut、FnOnce),所以,理论上,'static 作为闭包类型约束和 'static 作为普通类型(比如结构体)的约束应该是没有分别的,但是闭包毕竟不是结构体,'static 能约束到闭包的什么呢?这是一个很好的问题,如果找到关键联系,理解起来就会非常轻松。
我们知道,闭包都会被编译器自动编译成匿名的结构体,它所捕获的外部值或引用都是这个结构体中的字段。这就和 'static 作为结构体的类型约束能对齐了!当 'static 作为闭包的类型约束时(也就是出现在 T: Fn()/FnMut()/FnOnce() + 'static 这种位置上时),它实际上约束的是:闭包不得捕获外部的引用('static 引用除外)!这种情形有一个最常见的例子,就是 thread::spawn() 需要的闭包,它的类型约束就是:F: FnOnce() -> T + Send + 'static,如果你传给 thread::spawn() 的闭包依赖到了一个外部的引用,编译会报错,一般的解决方法就是使用 move 关键字把引用的值直接 move 进闭包。
2.4 出现在 supertrait 的位置上
'static 还可以出现在 supertrait 的位置上,但这个描述本身就有一定欺骗性,永远不要把 'static 当成 trait!出现在 supertrait 位置上的 'static 并不是真得 supertrait,其实还是类型约束,约束是的 self,也就是 trait 的实现者。看下面这个例子:
trait Plugin: 'static {
fn run(&self);
}
struct A { name: String }
impl Plugin for A { // A 不含引用字段,可编译
fn run(&self) { println!("{}", self.name); }
}
struct B<'a> { src: &'a str }
impl<'a> Plugin for B<'a> { // B 含有引用字段,无法实现 Plugin,编译错误
fn run(&self) { println!("{}", self.src); }
}
2.5 作为生命周期泛型参数的一个“常量”
'static 最后一种用法就是作为生命周期参数的一个“具体类型”替换在泛型中声明的一个生命周期。'static 是唯一有名字的具体生命周期,可以视作 'a 这种生命周期参数的一个“常量”,实际上,'static 的这种用法我们在 2.2.2 节已经介绍到了,就是针对 struct D<'a> { s: &'a str } 这种 带生命周期标注的结构体,当我们创建一个 D 的实例时,传入的引用会把生命周期约束关系带入到这个实例中。这里有一种特殊场景:我们可以传入 'static 得到一个不依赖外部引用或者只有 'static 引用的具体类型。下面,列举了三种常见的出现形式:
struct D<'a> { s: &'a str }
fn f(d: D<'static>) { … } // ☑ 作为参数
type StaticD = D<'static>; // ☑ 类型别名
let d: D<'static> = D { s: "hi" }; // ☑ 作为变量
这里,我们要澄清两个概念:生命周期泛型参数与生命周期标注。对于 struct D<'a> { s: &'a str } 而言,它确实有“泛型”的一面,因为它使用 <> 声明了泛型参数,而里面的这个** 'a 叫生命周期泛型参数2,简称:生命周期参数,但它同时也是生命周期标注,两种含义并不冲突,并且也很难剥离开**。
通常,我是将 'a 看作一种生命周期标注,很少会将其看作一个生命周期泛型参数,因为:当 'a 作为生命周期参数时,它的“具体值”是在实例化结构体时,由编译器根据传入的引用自动推断出来的,它确实是一个具体的生命周期的“值”,这是 'a 具有“泛型”意味的原因,但是,这个“值”你是写不出来的(也不需要写出来),提供这个值的方式是在要关联的引用字段上添加标注,然后让编译器去推断。
所以,在绝大多数情况下,我们是体会不到 'a 作为生命周期泛型参数的“意义”,根本原因就是,我们几乎写不出一种具体的生命周期的“值”,全都是靠它作为生命周期标注由编译器自动推断的,'a 作为泛型参数的那一面完全被遮挡了。如果我们能明确地写出一种具体的生命周期,就可以看到它作为“生命周期泛型参数”的一面,有能写出来的具体的生命周期吗?有且只有一个,就是 'static!这就是我们能看到的 <'a> 最像一个泛型参数而不是生命周期约束的示例:
struct D<'a> { s: &'a str }
let d: D<'static> = D { s: "hi" };
其实是有三种,还有一种叫 outlives bound,没有合适的中文译名,outlives 意为:比…活得久。典型的 outlives bound 写作 'a: 'b,读作 “a outlives 'b”,它的意思是: 'a: 'b 'a 至少活得跟 'b 一样久。更多信息参考:https://nomicon.purewhite.io/dropck.html ↩︎
Generic Parameter(泛型参数)包含三类:Type Parameter(T)、Lifetime Parameter('a)、Const Parameter(const N: usize)。生命周期参数是泛型参数的一种 ↩︎
网硕互联帮助中心


评论前必须登录!
注册