23道通向初级架构师的Rust面试题

6 阅读6分钟

一、Rust 中级题

1. 为什么 usize 传参不会丢失所有权,而 String 会?

核心结论
usize 实现了 Copy,传参时发生拷贝;String 未实现 Copy,传参时发生 move。

关键概念

  • Copy vs Move
  • 栈类型 vs 堆分配类型

代码示例

fn takes_usize(x: usize) {
    println!("{}", x);
}

fn takes_string(s: String) {
    println!("{}", s);
}

fn main() {
    let a = 10;
    takes_usize(a);
    println!("{}", a); // OK

    let s = String::from("hello");
    takes_string(s);
    // println!("{}", s); // 编译错误:value moved
}

2. 什么是 Copy trait?

核心结论
实现 Copy 的类型在赋值或传参时按位复制,而非转移所有权。

关键概念

  • 隐式拷贝
  • 无析构逻辑(no Drop)

代码示例

#[derive(Copy, Clone)]
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p1 = Point { x: 1, y: 2 };
    let p2 = p1; // copy
    println!("{} {}", p1.x, p2.x);
}

3. &str 引用的数据存储在哪里?

核心结论
&str 只是一个切片,数据位置取决于其来源。

关键概念

  • 字符串字面量
  • 切片

代码示例

fn main() {
    let a: &str = "hello"; // 二进制 data 段

    let s = String::from("world");
    let b: &str = &s; // 指向堆内存
}

4. 什么是生命周期(lifetime)?

核心结论
生命周期用于描述引用在多长时间内是合法的

关键概念

  • 借用检查器
  • 防止悬垂引用

代码示例

fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}

5. 什么是 'static 引用?

核心结论
'static 引用在整个程序生命周期内都有效。

关键概念

  • 程序级生命周期

代码示例

fn get_static() -> &'static str {
    "hello world"
}

6. 为什么 &'static str 可以赋值给 &str

核心结论
Rust 允许生命周期缩短(lifetime coercion)。

关键概念

  • 'static outlives 所有生命周期

代码示例

fn main() {
    let s: &'static str = "hello";
    let t: &str = s; // OK
}

7. 如何把值从栈移动到堆?

核心结论
使用 Box<T> 或其他堆分配智能指针。

关键概念

  • 堆分配

代码示例

fn main() {
    let x = 5;
    let boxed = Box::new(x);
}

8. 为什么 &Box<T> 可以当作 &T 使用?

核心结论
因为 Box<T> 实现了 Deref<Target = T>

关键概念

  • Deref coercion

代码示例

fn takes_ref(t: &i32) {
    println!("{}", t);
}

fn main() {
    let b = Box::new(10);
    takes_ref(&b); // 自动解引用
}

9. 如何在不传 &mut 的情况下实现可变性?

核心结论
使用内部可变性(interior mutability)。

关键概念

  • RefCell

代码示例

use std::cell::RefCell;

fn main() {
    let x = RefCell::new(5);
    *x.borrow_mut() += 1;
    println!("{}", x.borrow());
}

二、Rust 高级题

10. 单线程 / 多线程的共享所有权如何实现?

核心结论

  • 单线程:Rc
  • 多线程:Arc

关键概念

  • 引用计数
  • 线程安全

代码示例

use std::rc::Rc;
use std::sync::Arc;

fn main() {
    let a = Rc::new(5);
    let b = a.clone();

    let x = Arc::new(10);
    let y = x.clone();
}

11. 为什么 Rc<T> / Arc<T> 默认不可变?

核心结论
共享所有权默认只读,避免数据竞争。

解决方式
结合内部可变性。

代码示例

use std::rc::Rc;
use std::cell::RefCell;

fn main() {
    let v = Rc::new(RefCell::new(1));
    *v.borrow_mut() += 1;
}

12. 生命周期中的子类型关系如何理解?

核心结论
生命周期越长,类型越“具体”,是较短生命周期的子类型。

关键概念

  • 协变(variance)

代码示例

fn takes_short<'a>(x: &'a str) {}

fn main() {
    let s: &'static str = "hello";
    takes_short(s); // OK
}

13. 泛型约束 vs trait object 的取舍?

核心结论

  • 泛型:静态分发,性能好
  • trait object:动态分发,灵活

代码示例

trait Foo { fn foo(&self); }

fn generic<T: Foo>(t: T) { t.foo(); }
fn dynamic(t: &dyn Foo) { t.foo(); }

14. 'static 作为 trait bound 通常用在什么场景?

核心结论
保证类型在异步或跨线程场景下不会引用短生命周期数据。

关键概念

  • async / Future

代码示例

use std::thread;

fn spawn<T: Send + 'static>(v: T) {
    thread::spawn(move || {
        drop(v);
    });
}

三、Rust 架构题

这一部分面向 高级工程师 / 架构设计 / Rust 核心机制理解,重点考察对并发、异步、内存模型和 unsafe 边界的理解。


15. SendSync 的区别是什么?

核心结论

  • Send:类型的所有权可以在线程间移动
  • Sync:类型的共享引用 &T 可以在线程间共享

关键概念

  • 自动 trait(auto trait)
  • 数据竞争的编译期预防

代码示例

use std::rc::Rc;
use std::sync::Arc;
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn main() {
    // Rc 既不是 Send 也不是 Sync
    // assert_send::<Rc<i32>>();
    // Arc 同时是 Send + Sync
    assert_send::<Arc<i32>>();
    assert_sync::<Arc<i32>>();
}

16. 为什么 RefCell<T> 不是 Sync

核心结论
RefCell 通过运行期借用检查实现可变性,无法保证线程安全。

关键概念

  • 运行期借用检查
  • 非原子性状态

代码示例

use std::cell::RefCell;
fn assert_sync<T: Sync>() {}
fn main() {
    // 编译失败:RefCell<T> 不是 Sync
    // assert_sync::<RefCell<i32>>();
}

17. Mutex<T>RwLock<T> 的核心区别是什么?

核心结论

  • Mutex:同时只允许一个线程访问
  • RwLock:允许多个读者或一个写者

关键概念

  • 读多写少场景
  • 锁竞争

代码示例

use std::sync::{Mutex, RwLock};
fn main() {
    letm = Mutex::new(5);
    *m.lock().unwrap() += 1;
    letr = RwLock::new(10);
    *r.write().unwrap() += 1;
}

18. 什么是 Pin?它解决了什么问题?

核心结论
Pin 用于保证某个值在内存中的地址不会再发生移动。

关键概念

  • 自引用结构体
  • async / Future 状态机

代码示例

use std::pin::Pin;
struct SelfRef {
    data: String,
    ptr: *constString,
}
// Pin 防止 SelfRef 被移动,避免 ptr 悬空

19. Unpin 是什么?

核心结论
Unpin 表示类型在被 Pin 之后仍然可以安全移动。

关键概念

  • 大多数普通类型都是 Unpin
  • async 生成的 Future 通常不是 Unpin

代码示例

fn assert_unpin<T: Unpin>() {}
fn main() {
    assert_unpin::<i32>();
}

20. async/await 在 Rust 中是如何实现的?

核心结论
async fn 会被编译器转换为一个实现了 Future 的状态机。

关键概念

  • 零成本抽象
  • 状态机展开

代码示例

use std::future::Future;
async fn foo() -> i32 {
    42
}
fn assert_future<F: Future<Output = i32>>(_: F) {}
    fnmain() {
    assert_future(foo());
}

21. 为什么 Future 经常需要 'static

核心结论
为了确保 Future 在跨线程或长期运行时不会引用已经失效的数据。

关键概念

  • 任务调度器
  • 生命周期逃逸

代码示例

use std::thread;
fn spawn<F>(f: F)
    where
    F: FnOnce() + Send + 'static,
    {
    thread::spawn(f);
}

22. 什么是 unsafe?Rust 的安全边界在哪里?

核心结论
unsafe 不是关闭安全,而是由程序员手动保证 Rust 的安全不变量。

关键概念

  • 安全抽象包裹 unsafe
  • 最小 unsafe 原则

代码示例

unsafe fn dangerous(ptr: *consti32) -> i32 {
    *ptr
}

23. 什么情况下你会在架构层面选择 unsafe

核心结论

  • 性能关键路径
  • 与底层系统 / FFI 交互
  • 实现安全抽象(如 Vec、Arc)

关键概念

  • 不变量设计
  • 封装与审计