Rust 八股

Posted on Dec 4, 2022

设问

先有 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)中的元组和数组的长度固定,因此在栈上分配内存;相反,在编译时未知大小的数据或可能改变大小的数据必须被存储在堆上,而找到堆上数据的方法是通过栈上的指针的值——虚拟地址,地址为标量。总而言之,要么仅在栈上分配内存,要么既在栈上,又在堆上分配内存。

stack(frame)-heap

  • 每个小矩形内:栈在左,堆在右。
  • 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)是指在不拥有所有权的情况下,从一个变量中借用它的值。这意味着,在某个时刻,只能有一个变量对它进行借用。这样做可以避免数据竞争,同时也提供了灵活性,因为多个变量可以对同一个值进行借用。

rust-move-copy-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  because  String  implements the  Deref  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 referenceweak 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>>>,
}

strong_and_weak_reference_counts

如何构造线程池?

SimpleThreadPool(Rust)

本文首发于 https://h2cone.github.io/

其它