3. 借用与生命周期面试题精讲
在Rust面试中,借用与生命周期是区分候选人是否真正理解所有权模型的关键分水岭。面试官通常不会只问“什么是借用”,而是给出一个编译失败的代码片段,要求你解释错误原因、推导生命周期约束,并给出修复方案。本章围绕四个高频考点展开:借用规则、可变与不可变引用冲突、生命周期标注与省略规则、结构体中的生命周期参数。
一、借用规则:面试中的第一道筛子
Rust的借用规则可以浓缩为两条:同一作用域内,要么存在多个不可变引用(&T),要么存在一个可变引用(&mut T),二者不能同时存在;引用必须始终有效。面试中常见的问法是:“下面这段代码为什么编译不过?”
fn main() {
let mut data = vec![1, 2, 3];
let first = &data[0];
data.push(4);
println!("{}", first);
}
编译器会报错:cannot borrow `data` as mutable because it is also borrowed as immutable。原因在于first持有对data内部元素的不可变引用,而push可能触发扩容,导致原有内存被释放或移动,first将变成悬垂引用。面试中你需要明确指出:这不是“编译器太严格”,而是借用检查器在阻止真实的内存安全问题。修复方式有两种:将first的使用提前到push之前,或者用data[0]的值拷贝替代引用。
二、可变与不可变引用冲突的推导
面试官常给出更隐蔽的冲突场景,考察你是否理解NLL(非词法生命周期)。例如:
fn main() {
let mut s = String::from("hello");
let r1 = &s;
let r2 = &s;
println!("{} {}", r1, r2);
let r3 = &mut s;
r3.push_str(" world");
println!("{}", r3);
}
这段代码在Rust 2018之后可以编译通过。关键判断依据是:r1和r2的最后一次使用在println!处,之后不再被使用,因此它们的生命周期在r3创建前已经结束。面试中回答这类问题,要强调“引用的生命周期从创建开始,到最后一次使用结束”,而不是到作用域结束。如果面试官追问“如果r1在r3之后还被使用呢”,你应立刻指出那会触发冲突,并说明编译器会精确报告冲突位置。
三、生命周期标注与省略规则
生命周期标注的核心目的是告诉编译器多个引用之间的存活关系。面试中常考的函数签名是:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
这里'a表示返回值的生命周期与两个参数中较短的那个一致。面试官可能问:“如果两个参数生命周期不同会怎样?”答案是:调用时编译器会取两者交集,即较短的那个。你需要能推导出:传入一个长生命周期字符串和一个短生命周期字符串,返回值只能在短生命周期内使用。
省略规则是面试中的加分项。三条规则必须能脱口而出:第一,每个引用参数获得独立生命周期;第二,如果只有一个输入生命周期,它被赋给所有输出生命周期;第三,如果有&self或&mut self,其生命周期被赋给所有输出生命周期。面试中常给一个方法签名让你判断省略后的完整形式,例如:
fn get_name(&self) -> &str
根据第三条规则,等价于fn get_name<'a>(&'a self) -> &'a str。如果方法有多个参数但包含&self,输出仍绑定到self,其他参数的生命周期不参与输出推导。
四、结构体中的生命周期参数
当结构体持有引用时,必须标注生命周期。面试中常见题目是:“为什么结构体不能直接存&str?”因为编译器需要知道结构体实例不能比它持有的引用活得更久。正确写法:
struct Excerpt<'a> {
part: &'a str,
}
impl<'a> Excerpt<'a> {
fn announce(&self, announcement: &str) -> &str {
println!("Attention: {}", announcement);
self.part
}
}
面试中会追问:announce的返回值为什么是&str而不是&'a str?根据省略规则,&self的生命周期被赋给返回值,因此返回的self.part实际绑定到&self的借用周期,而非结构体内部的'a。这是一个容易答错的细节,能准确区分候选人的理解深度。
五、面试推导训练:从错误信息反推约束
面试官常给出一段编译错误,要求你反向推导生命周期约束。例如:
fn main() {
let result;
{
let temp = String::from("temp");
result = &temp;
}
println!("{}", result);
}
错误信息是`temp` does not live long enough。你需要解释:result的生命周期跨越了temp的作用域,但temp在内部块结束时被释放,导致result成为悬垂引用。修复方式是将temp移到外层,或让result在内部块内使用完毕。面试中回答这类问题,建议按“引用创建位置→引用使用位置→被引用值释放位置”三步推导,并明确指出编译器在哪个点判定生命周期不足。
六、可直接使用的面试自检清单
以下清单可用于面试前快速自检,也可作为面试中回答借用与生命周期问题的推导框架:
| 检查项 | 判断依据 | 常见错误 |
|---|---|---|
| 是否存在可变与不可变引用共存 | 可变引用的创建点是否在不可变引用最后一次使用之后 | 误以为作用域结束才释放引用 |
| 返回值生命周期绑定到哪个参数 | 省略规则第三条:有&self时绑定到self |
错误绑定到其他参数或结构体泛型生命周期 |
| 结构体实例是否比引用活得久 | 结构体泛型生命周期是否覆盖实例使用范围 | 遗漏<'a>标注或impl块未声明 |
| 闭包捕获是否导致借用冲突 | 闭包捕获方式(&、&mut、move)与外部引用是否重叠 |
忽略闭包默认按引用捕获 |
面试中遇到生命周期问题时,先画出每个引用的“创建—使用—释放”时间线,再对照借用规则判断冲突点。能清晰说出“编译器在哪个位置、因为哪条规则、拒绝了哪两个引用的共存”,就已经超过了大多数候选人。结构体生命周期参数的关键在于:泛型生命周期不是装饰,而是结构体实例与外部引用之间的契约,impl块必须与结构体声明保持一致,方法返回值则优先遵循省略规则绑定到&self。