面试题答案
一键面试1. 生命周期、所有权和借用规则基础
- 所有权:Rust 中的每个值都有一个所有者,且在同一时间只有一个所有者。当所有者离开作用域时,值会被销毁。例如:
{
let s = String::from("hello"); // s 是 "hello" 字符串的所有者
} // s 离开作用域,字符串被销毁
- 借用:允许在不转移所有权的情况下使用值。有两种借用:不可变借用(
&T
)和可变借用(&mut T
)。借用规则规定:- 同一时间,要么只能有一个可变借用,要么可以有多个不可变借用。
- 借用的生命周期必须小于等于被借用值的生命周期。
let s = String::from("hello");
let r1 = &s; // 不可变借用
let r2 = &s; // 多个不可变借用是允许的
// let r3 = &mut s; // 错误:存在不可变借用时,不能有可变借用
- 生命周期:是指值在程序中存在的时间段。Rust 编译器使用生命周期标注来确保借用关系的有效性。例如:
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
这里的 'a
是生命周期参数,它表明函数 longest
的返回值的生命周期与参数 x
和 y
中较短的那个相同。
2. 保证并发安全
- 编译器检查:通过上述所有权、借用和生命周期规则,Rust 编译器在编译时就能检测到大多数数据竞争情况。在并发场景下,这些规则同样适用。例如,当使用
std::thread::spawn
创建新线程时:
use std::thread;
fn main() {
let mut data = String::from("hello");
// 错误:data 是可变的,且下面尝试将其不可变借用移动到新线程中
// thread::spawn(move || {
// println!("{}", &data);
// }).join().unwrap();
// 正确做法:先创建不可变借用,再将其移动到新线程
let data_ref = &data;
thread::spawn(move || {
println!("{}", data_ref);
}).join().unwrap();
}
- 跨函数借用:在函数间传递借用时,编译器会检查借用的生命周期是否正确。例如:
fn first<'a>(s: &'a str) -> &'a str {
s
}
fn second<'a>(s: &'a str) -> &'a str {
let sub = &s[0..3];
sub
}
fn main() {
let s = String::from("hello");
let result1 = first(&s);
let result2 = second(result1);
println!("{}", result2);
}
这里 first
和 second
函数的生命周期参数确保了借用的有效性。
3. 跨线程复杂借用场景
Send
和Sync
标记 trait:Send
:如果一个类型实现了Send
,意味着该类型的值可以安全地转移到其他线程。大多数 Rust 类型都实现了Send
,但像Rc<T>
这种引用计数类型没有实现,因为多个线程可能同时修改引用计数导致数据竞争。Sync
:如果一个类型实现了Sync
,意味着该类型的值可以在多个线程间安全共享。例如&T
实现了Sync
,而&mut T
没有实现,因为可变借用会破坏共享的安全性。
- 示例:使用
Mutex<T>
来安全地在多线程间共享可变数据。
use std::sync::{Arc, Mutex};
use std::thread;
fn main() {
let data = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let data_clone = Arc::clone(&data);
let handle = thread::spawn(move || {
let mut num = data_clone.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Final value: {}", *data.lock().unwrap());
}
这里 Mutex<T>
实现了 Sync
和 Send
,允许在多线程间安全共享可变数据。Arc<T>
用于在多个线程间共享 Mutex<T>
的所有权,而 lock
方法获取锁以确保同一时间只有一个线程可以访问数据,从而避免数据竞争。