Rust 八股
设问
先有 Rust 还是先有 Rust 编译器?
第一个 Rust 编译器一定是用非 Rust 实现的,假设它叫引导编译器,巨佬用引导编译器来编译用 Rust 编写的 Rust 编译器源代码得到 Rust 实现的 Rust 编译器,从此以后的迭代开发无需引导编译器,而是基于 Rust 实现的 Rust 编译器。
第一次看到 rust-lang/rust 就有这样的疑问,后来才想起来这种解决方案被称为自举。
语句与表达式的区别?
语句(statement)是执行某些动作的指令,无返回值;表达式(expression)被估算(evaluate)输出返回值。
当在语句行尾处遇到 ;
时,其左邻的片段将被估算出返回值,该返回值又被 =
赋予某个变量,它们通常成对出现,若该片段不含分号,则它被看作是表达式。
内存分配在哪里?
基于堆的内存分配(heap-based memory allocation)或基于栈的内存分配(stack-based memory allocation)。赋值语句、函数调用等场景离不开给变量(的值)分配内存,很喜欢书中的餐厅隐喻,在栈上分配内存就像叠碟子,而堆上分配内存就像进入餐厅找空位。
标量类型(scalar type)数值仅在堆上分配内存,比如整数、浮点数、布尔、字符,它们的长度固定的含义是在运行时比特序列长度不可变,换言之,在编译时就已经知道它们的大小。复合类型(Compound Types)中的元组和数组的长度固定,因此在栈上分配内存;相反,在编译时未知大小的数据或可能改变大小的数据必须被存储在堆上,而找到堆上数据的方法是通过栈上的指针的值——虚拟地址,地址为标量。总而言之,要么仅在栈上分配内存,要么既在栈上,又在堆上分配内存。
- 每个小矩形内:栈在左,堆在右。
scalar
标量。ptr
指针。- 箭头:地址指向的位置。
len
长度。cap
容量。- 蓝色:len。
- 绿色:cap - len。
赋值语句或函数调用传参本质上是纯栈的数据拷贝?
如果在堆上的数据很大,那么堆的数据拷贝在运行时开销非常昂贵,一般情况下都是隐式 Copy,除非显式 Clone。
什么是所有权?
所有权(ownership)使 Rust 能够无需垃圾收集器的情况下保证内存安全。默认情况下,违反所有权规则会导致编译错误,所有权规则如下所述:
- 每个值(value)都有一个所有者(owner)。
- 一个所有者是一个变量。
- 隐式只有一个所有者,显式支持多个所有者。
- 当所有者超出作用域(variable scope)时,该值将被销毁(drop)。
- 范围开始于左大括号
{
, 结束于右大括号}
。 - 通过实现 Drop 来销毁值。
- 范围开始于左大括号
在规则之上,所有权可以在变量之间转移(move),用官方的例子来说,被其它变量夺走所有权的变量已无效。
let s1 = String::from("hello");
let s2 = s1;
// This code does not compile!
println!("{}, world!", s1);
传递函数参数或接收返回值时转移所有权很繁琐,不要恐慌,Rust 有使用一个值而不夺取所有权的特性:引用(reference),比如上文 1.4 插图第 3 列第 2 行。
引用与指针的区别?
指针指向一个内存地址,可以用来访问存储在该内存地址上的值,引用也类似,与指针不同的是,引用保证在引用的有效期内指向一个特定类型的有效值(编译时检查),引用的规则如下所述:
- 一个变量只能有一个可变(mutable)引用,或多个不可变(immutable)引用。
- 一个引用是一个变量。
- 声明变量时,它的值默认是不可变的,即只读,除非使用
mut
修饰才可写。
- 引用必须始终有效(valid),参考生命周期。
什么是借用?
借用(borrow)是指在不拥有所有权的情况下,从一个变量中借用它的值。这意味着,在某个时刻,只能有一个变量对它进行借用。这样做可以避免数据竞争,同时也提供了灵活性,因为多个变量可以对同一个值进行借用。
画图太麻烦了,直接借用 Graphical depiction of ownership and borrowing in Rust。
什么是数据竞争?
数据竞争(data race)是指在多个线程同时访问同一个变量时,由于线程之间的执行顺序无法预知,可能会导致结果不可预测或错误的情况,发生数据竞争有三个条件:
- 两个或多个进程/线程/协程并发访问相同的变量。
- 至少有一个访问是写入。
- 没有同步访问机制。
什么是生命周期?
变量在作用域中从初始化到销毁的整个过程称之为生命周期(lifetime)。生命周期主要目的是防止“悬挂引用”和“悬空指针”错误,比如当多个指针指向一个地址时,通过一个指针销毁了该地址的数据,另一个指针就产生了悬挂引用,我们可以使用生命周期注解来确保引用在我们需要时有效。
String 与 &str 的区别?
String
和 &str
是 Rust 中的两种不同的字符串类型:
- String 类型表示一个可变的、UTF-8 编码的字符串。它是一个类型安全的、动态分配内存的字符串类型,可以随时扩展或收缩字符串的长度。String 类型可以随时被修改,并且它的所有权在整个程序的生命周期内都被保持。
- &str 类型表示一个不可变的、UTF-8 编码的字符串,它是一个指向字符串数据的指针。由于 &str 类型是不可变的,因此它不能被修改,也不能扩展或收缩字符串的长度。&str 类型可以被当作一个只读的字符串值来使用,但是它不能被修改。
什么是 Monomorphization?
Monomorphization 是一种编译器优化技术,它的目的是将多态代码(即具有多种可能类型的代码)转换为单态代码(即只有一种可能类型的代码)。这样做的好处是可以提高代码的执行效率,因为单态代码可以被编译器更好地优化。
什么是 Blanket implementations?
Blanket implementations 是指在 Rust 编程语言中,为一类类型实现一个 trait(即接口)的方法,使得这一类类型都可以使用这个 trait 的方法。这种方法可以让开发者更方便地定义一组类型所共有的行为,并且可以让这些类型在运行时具有更高的性能。
迭代器比循环快?
在某些情况下,使用迭代器(iterator)可能比使用循环(loop)执行得更快。这是因为迭代器可以让编译器进行更多的优化,例如预先计算迭代次数并对循环进行展开(unrolling),从而减少了运行时的开销。此外,迭代器还可以更高效地处理大型数据集,因为它只会在需要时才加载和处理数据。
有哪些零成本抽象?
零成本抽象是指在编译时执行的抽象操作,不会带来额外的运行时开销。Rust 中有多种零成本抽象的实现方式,包括但不限于:
- Traits:traits 是 Rust 中的接口类型,可以用来定义一组类型所共有的行为。使用 traits 可以在编译时将多态代码转换为单态代码,从而提高代码的执行效率。
- Generics:generics 是 Rust 中的泛型类型,可以用来定义支持多种类型的代码。使用 generics 可以在编译时为每种类型生成不同的代码版本,从而避免在运行时进行类型检查。
- Closures:closures 是 Rust 中的匿名函数,可以用来定义简单的函数逻辑。使用 closures 可以在编译时进行类型推断和代码优化,从而避免在运行时进行类型检查和内存分配。
引用与智能指针的区别?
引用(&T
)和智能指针(如 Box<T>
和 Rc<T>
)都是用来处理内存管理的类型。它们的主要区别在于,引用是一个不可变的指向某个值的指针,而智能指针是一种带有额外特性的指针,它不仅能保证数据的安全,还能自动管理内存的分配和释放。比如,Box<T>
类型的智能指针用于在堆上分配内存,并自动在指针离开作用域时释放内存;Rc<T>
类型的智能指针则用于维护引用计数,并在引用计数为 0 时释放内存。
Deref 是什么?
Deref coercion converts a reference to a type that implements the
Deref
trait into a reference to another type. For example, deref coercion can convert&String
to&str
becauseString
implements theDeref
trait such that it returns&str
.
- Rust calls
deref
to turn the&String
into&str
- &str 来自对 String 切片(用
&
与[
..]
) - &String 是类型为 String 的数据的引用
- 不存在类型:str
如何选择智能指针?
- The
Box<T>
type has a known size and points to data allocated on the heap. - The
Rc<T>
type keeps track of the number of references to data on the heap so that data can have multiple owners. - The
RefCell<T>
type with its interior mutability gives us a type that we can use when we need an immutable type but need to change an inner value of that type; it also enforces the borrowing rules at runtime instead of at compile time.
引用循环会泄漏内存?
Reference variable 可以 drop 且 reference count 可以递减,但它指向的实例或值在堆上却不能被 drop,由于引用循环,实例的 reference count 不为零……
强引用与弱引用的区别?
在我看来 strong reference 与 weak reference:
- drop 或者 clean up 时弱引用忽略不计
- Weak 引用的值可能已被删除
- clone、downgrade、upgrade
- Rc 是强(被)引用类型,Weak 是弱(被)引用类型
如何构造树?
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
// 从结点到父结点是弱引用(防止因引用循环而泄漏内存
// 更改其父节点
parent: RefCell<Weak<Node>>,
// 从结点到子结点是强引用( 当结点被 drop 时,它的子结点也应该被 drop
// 结点能够被其它变量引用(允许多个 onwers
// 更改哪些结点是另一个结点的字结点
children: RefCell<Vec<Rc<Node>>>,
}
如何构造线程池?
本文首发于 https://h2cone.github.io/