Rust Quiz

notice

可以注意到中间漏了些题目,比如 7, 这是因为这个题目已经过时了,已经被移除出 Rust Quiz

quick intro

Rust Quiz is a collection of questions designed to test your knowledge of Rust. It covers various topics, including ownership, lifetimes, and concurrency.

Ref

Note

Q1

值得一提的是,这里介绍了一种 Rust 编译器处理语法的细节。 虽然 Rust 中一切皆表达式,但 Rust 遇到 {} 的时候,会把块处理成 stmt,不会当成后面的二元运算符的左表达式。 比如 {} && true,Rust 会理解成 {}&&true 两个 stmt,而不是 {} && true 一个 expr 类型的 stmt. 也就是这里 && 并没有被当成一个二元运算符,而是当成了连续使用的两个 & 一元运算符。

如果想要把一个 block 当作二元运算符的一个算子表达式,可以用括号括起来,比如 ({} && true),这样 Rust 就会把 {} 当成一个 expr,把 ({} && true) 整体当成一个 expr 类型的 stmt.

Q2

该题目涉及的知识点和 Q1 类似,Rust 编译器在处理语法的时候会把 {} 当成一个 stmt,而不是一个 expr。所以 {} & S(4) 会被 Rust 编译器理解成两个 stmt: {}&S(4),其中 & 没有被理解成 BitAnd 运算符,而是理解成了一元的取引用运算符。

Q3

Rust 中用 const 定义的全局常量,在局部作用域中取&mut 的时候,实际是创建了一个具有该常量值的临时变量,并且&mut 指向该临时变量。 在局部作用域中对 const 定义的全局常量的字段进行修改的时候,会创建一个具有该常量值的临时变量,并不会实际修改该全局常量的值,也就是后面再访问该全局常量时,仍然是原来的值。

Q4

Rust 中使用 b"xx" 语法创建 byte string,也就是类型为 &'static [u8;<n>] 的类型表示,这里面 <n> 代指对应的字符串中字符数量,每个字符看作 ascii 字符。 Rust 中 .. 可以用作表示匹配剩余内容,也可以用于表示一个 RangeFull 类型的 range 所以 b"062"[..][1] 指向的是 [b'0', b'6', b'2'] 中的第一个元素 b'6',对应 ascll 值为 54.

Q6

Rust 中 assiggnment 语句本身的值是 (),也就是 unit 类型。 所以 let x = y = 1; 等价于 let x = (y = 1);,也就是先执行 y = 1,然后该语句的值是 (),再将 () 赋值给 x。 也就是 let x = y = 1; 等价于 y = 1; let x = ();

Q8

Rust 中声明宏匹配标点符号的时候要考虑空格,有时缺乏空格的情况会按照优先左结合组成标点符号的规律匹配,所以有时有没有空格的匹配结构一致。比如声明宏中 ==>== > 是等价的,都被匹配 ==> 两个标点符号。

Rust 中过程宏的 api 更加灵活,能够准确地处理空格的使用。比如能够区别 ==>== >

Q9

Rust 的声明宏的匹配机制中,不同类型的宏的片段指示符(fragment specifiers)可以分为两种,一种会让被捕获的成员不透明,另一种不会。 透明的片段指示符包括:

  • ident
  • lifetime
  • tt

Q10

考察 Rust 中的 method lookup. Rust 中类型的自有方法和类型实现的 trait 方法同名的情况下:

  1. 方法的接收器不同时, &self&mut self 优先
  2. 方法的接收器相同时, 类型的自有方法比 trait 方法优先

本题目为特殊情况,对于 dyn Trait 类型,存在 dyn Trait 的自有方法与该 Trait 提供的方法同名的情况下,当前没有办法调用 到该 dyn Trait 类型的自有方法。

Q11

考察 Rust 中泛型参数的 early bound 和 late bound

对于生命周期泛型参数, 存在 early bound 和 late bound 两种情况。 如果是 late bound 的生命周期泛型参数,则不能够显式地指定生命周期泛型参数。 其中 fn f<'a>()'a 就是 late bound 的生命周期泛型参数; 其中 fn g<'a:'a>()'a 就是 early bound 的生命周期泛型参数

Q12

考察 Drop 的顺序

Q13

考察 Rust 中 0 大小类型(ZST)的数组中每个元素的地址都是一样的。 ZST 在 Rust 中是一种特殊的抽象,属于编译时才存在的概念。 编译后程序不会为 ZST 的值开辟空间。

Q14

Rust 中的 impl 语句实现的位置不重要,整个程序的 scope 中都可以访问。

Q15

考察 Rust 中整数类型推导的规则。 对于 Rust 中的整数类型的变量,如果它调用了 trait 方法。 首先查询该整数类型本身是否实现了该 trait,比如查询 i32,该题目中查询到没有,然后查询其他整数类型,查询到 u32 满足该 trait,所以推导该变量为 u32 类型。 如果所有整数类型 T 都没有实现该 trait,则会查询整数类型的 &T 类型是否是实现了该 trait,优先从 &i32 开始。

ps, 这个材料中推荐的 stack overflow 的回答看起来很晦涩,笔者直接放弃。笔者觉得自己这个理解更直接。

经过实验,发现:

  1. 如果直接整数类型 T 对 trait 的实现存在多个,但是不包含 i32 的实现的话,则编译器会推导整数类型为 i32,然后报错 trait 未实现
  2. 如果有多个直接整数类型 T 对 trait 的实现,且包含 i32 的实现的话,则正常编译,并且编译器推导该变量为 i32 类型。
  3. 如果只是存在一个整数类型对该 trait 的实现,则编译器推导该变量为该整数类型。
  4. 用 T 代指整数类型,如果没有直接整数类型实现该 trait, 但是有且仅有一个 &T 整数类型的引用类型实现该 trait, 则编译器推导该变量为 T 类型
  5. 如果存在多个&T 对该 trait 的实现,且不包含 &i32 的实现,则编译器推导该变量为 i32 类型,然后报错 trait 未实现

Q16

Rust 中没有 --++ 这样的一元自增/自减运算符。 所以 --x 其实相当于 -(-x),也就是先对 x 取负,然后再对结果取负。

Q17

考察 Rust 中的运算符,Rust 中没有自增/自减运算符。 所以若干个 - 或者若干个 + 会相当于用右结合的方式运算。

Q18

Rust 语法中的成员搜索规则是先搜索成员方法,找不到成员方法再搜索成员变量。

Q19

Rust 中 let _ = s; 这个语句不会 move 变量 s, 也就是 s 如果有 Drop::drop 实现的话,该函数不会在这一行后立刻触发。

Q22

Rust 中 1. 整体表示一个浮点数。 Rust 中宏指示符会把合法数字表示当成一个 token,对应 tt 片段指示符。

Q23

考察 Rust 中 method lookup 的规则。 自由方法和实现的 trait 方法同名的情况下, 如果接收器相同,优先使用接收器为 &self 的方法 如果接收器不同, 优先使用自有方法。

Q24

可以把宏的卫生性理解成一种对局部变量的着色。 对于宏中直接使用的外部局部变量的名称,指向的是宏定义处有定义的局部变量。 至于常量,Rust 中把局部常量当作 items 而不是局部变量,宏使用的外部局部常量的名称指向调用宏时外部的局部常量。

Q25

考察 Rust 的 desconstructing 语法,以及 drop 的时机。 drop 的时机:

  1. 当一个值被构造出来后没有持有者的时候,会直接 drop;
  2. 当值的持有者超出作用域的时候,drop.

let S = f()S 不是一个变量名, 而是一个 destructuring 语法中用到的模式,指向类型 struct S, 这里表示一个不会失败的模式解构,并且绑定新的变量。

Q26

考察迭代器的延迟计算

Q27

&dyn Trait 类型作为参数类型或者参数类型为 T,约束 T:Trait 时参数的行为都是一样的,只能够访问到 Trait 中定义的方法 Rust 中 super trait 不等于 c++ 中的继承,而只是说明一个 trait 要实现必须先实现了某些 trait.

Q28

同 Q25 一样,考察 Drop 的时机

Q29

type M = (i32); 等价于 type M = i32; Rust 中 一个元素的 tuple 需要结尾加上 , 来表示, 比如 (T,),以与 (T) 区分开来。

Rust 中对于无后缀的整数数字,往往优先认为是 i32 类型的整数。

Q30

考察 Rust 中的方法查找规则。 以及考察 Rust 标准库中的 Rc 实现了 Clone trait 这个知识点。

Q31

考察 Rust 中变量的方法查找顺序。

trait Or {
    fn f(self);
}

struct T;

impl Or for &T {
    fn f(self) {
        print!("1");
    }
}

impl Or for &&&&T {
    fn f(self) {
        print!("2");
    }
}

fn main() {
    let t = T;
    let wt = &T; // &T
    let wwt = &&T; // &&T, &&&T, &mut &&T , &T
    let wwwt = &&&T; // &&&T, &&&&T
    let wwwwt = &&&&T; // &&&&T
    let wwwwwt = &&&&&T; // &&&&&T,&&&&&&T, &mut &&&&&T, &&&&T
    t.f(); // 1
    wt.f(); // 1
    wwt.f(); // 1
    wwwt.f(); // 2
    wwwwt.f(); // 2
    wwwwwt.f(); // 2
}

Q32

考察 Rust 的 match 语法 xx|yy if bb => {} {..} 这种模式, 相当于

xx if bb => {..},
yy if bb => {..}

Q33 (Notice!!)

Rust 中 || 优先和后面的表达式组成闭包 所以 || .. .method() 用括号和空格标上顺序应该为 (|| ..) . method() 如果想要表达调用为 .. 实现的 method 方法,应该给 .. 加上括号如下: || ((..).method())

Q34

Rust 中函数指针的大小是固定的,是 usize 大小,需要用来存储函数的地址。 但是 Rust 中函数名本身作为值使用时,它本身的类型不是对应函数指针类型,而是与其函数定义本身绑定的一个类型,这个类型特定指向该函数的实现,所以不需要存储函数的地址,所以大小为 0.

举例说明

fn main(){
    fn f() {}
    let a : fn() = f; // a的类型是函数指针类型, 大小为usize的大小
    let b = f; // b的类型是函数f的类型(fn f()), 大小为0
    println!(
        "{:?} {:?}",
        std::mem::size_of_val(&a),
        std::mem::size_of_val(&b)
    );
    // 输出: 8 0
}

Q35

考察 Rust 中声明宏的卫生性规则。 为了避免声明宏中新定义的变量与外部同作用域下变量命名冲突,即使是同名变量,宏中定义的变量会被当成不同名字的变量处理。

不过如果变量命名是通过宏参数传入的,那么宏中定义的变量会与外部同作用域下变量命名相同。

#![allow(unused)]
fn main() {
macro_rules! x {
    ($n:expr) => {
        let a = X($n);
    };
    ($id:ident,$n:expr) => {
        let $id = X($n);
    };
}

struct X(u64);

impl Drop for X {
    fn drop(&mut self) {
        print!("{}", self.0);
    }
}

/// 输出: 121
#[test]
fn t1() {
    let a = X(1);
    // x!(2);
    x!(a, 2);
    print!("{}", a.0);
}

/// 输出: 221
#[test]
fn t2() {
    let a = X(1);
    x!(a, 2);
    print!("{}", a.0);
}
}

Q36

考察 Rust 中闭包的本质。 闭包相当于建立了一个具有闭包对应函数签名的自有方法的结构体。 该闭包捕获的外部变量,相当于结构体的字段。 如果闭包捕获的外部变量的类型都是实现了 Copy trait,则该闭包相当的结构体也相当于 实现了 Copy trait

Q37

考察 Rust 中的临时生命周期延长。 当一个临时变量的引用被 let/const/static 语句中使用的时候,这个临时变量的生命周期会延长到该语句所在的作用域结束。