Rust 学习笔记 2

2020/03/30

阅读 《 Rust 程序设计》的一些笔记。

第 3 章 - 通用编程概念

变量和可变性

Rust 中的变量默认为不可变的。

只要变量定义未加上 mut ,它在程序中就不用担心它被改变。

对于大数据结构,加上 mut ,可以直接操作实例,比复制并返回新实例肯定要快。

但是对于小数据结构,创建新的实例或者更多函数式编程风格更容易被理解,所以略低的性能在提高程序可读性时是值得的。

变量(variables)和常量(constants)的区别

常量是绑定到名称且不允许更改的值。但是常量和不可变量还是有一些区别。

首先,不允许将 mut 和常量一起使用,默认情况下,常量不仅不可变,它们始终不可变。

使用 const 关键字定义常量而不是 let 关键字,并且必须注释值得类型。

常量可以在任何范围(scope)中声明,包括全局范围。

最后一个不同,常量只能设置未常量表达式,不能设置未函数调用结果或者只能在运行时计算的任何其他值。

例如:

const MAX_POINTS: u32 = 100_000;

在数字文本中插入下划线,可以提高可读性。

隐藏(Shadowing)

我们可以通过使用相同变量的名称并重复使用 let 关键字来隐藏变量。

隐藏和将变量标记为 mut 不同,因为一旦忘记使用 let 重新分配变量,就会在编译时报错。通过使用 let ,我们可以对一个值执行一些转换,但是在转换完成后,变量是不可变的。

mut 和 shadowing 的另一个不同是,使用 let 关键字实际是创建了一个新的变量,但是可以重用名称。

数据类型

标量类型(scalar types)

标量类型表示单个值, Rust 中有 4 种主要标量类型:整数、浮点数、布尔值 和 字符。

整数类型

整数类型有: i8, i16, i32, i64, i128, isize, u8, u16, u32, u64, u128, usize。

Rust 的默认整数类型是 i32 ,即使在 64 位系统上,这种类型通常也是最快的。

使用 isize 或 usize 的主要用在对某种集合进行索引的场景。

整数溢出,在 debug 模式编译的程序遇到整数溢出问题会 panic, release 模式编译则不 panic ,而是进行两个补码换行,例如 u8 类型保存 256 会变成 0, 保存 257 会变成 1 ,以此类推。依赖整数移除的 wrapping 行为被视为错误,如果要显式 wrap ,则可以使用标准库类型 Wrapping 。

浮点类型

浮点类型有: f32, f64。

默认类型为 f64 ,因为在现代 CPU 上,它的速度与 f32 大致相同,但精度更高。

数字运算

Rust 支持四则运算和求余运算。

附录 B 中列出了更多运算符。

布尔类型

布尔值只要用于条件表达式,比如 if 表达式。

字符类型

char 类型是 Rust 中最基本的字母类型,使用单引号指定。(相对的字符串文字,使用双引号。)

Rust 中 char 类型,大小为 4 字节,代表 Unicode 标量。Unicode 标量值范围从 U+0000 至 U+D7FF 和 U+E000 至 U+10FFFF 。但是 “字符” 并不是 Unicode 中真正的概念,因此你对 “字符” 的直觉可能与 Rust 中不一致。我们在第 8 章中讨论。

复合类型

复合类型可以组合多个值到一个类型中, Rust 有两个基本复合类型: 元组(tuples)和数组(arrays)

元组类型

元组是一种将多种类型的值组合为一个复合类型的一般方法。元组的长度是固定的:声明之后它们的长度就无法增长或缩小。

我们在括号内编写逗号分隔的值列表来创建元组。元组中每个位置都有一个类型,并且元组中不通知的类型不必相同,此例中,我们添加了可选的类型注释:

let tup: (i32, f64, u8) = (1, 1.1, 1);

元组可以通过和 let 的一种模式解构(destructuring)。

fn main() {
    let tup = (1, 2, 3.1);
    let (_, y, _) = tup;
    println!("The value of y is {}", y);
}

除了通过模式匹配进行解构,我们还是可以通过点号(.)加索引的方式访问元组元素。例如:

fn main() {
    let x = (1, 2.1, 3);
    println!("{} {} {}", x.0, x.1, x.2);
}

数组类型

数组中的元素类型必须相同, Rust 中的数组具有固定长度。

放入数组的值被写成用方括号括起来的都好分隔的列表。

当你想把数据分配在栈上而不是堆上时,数组很有用。(第 4 章中讨论堆栈)

数组不如向量类型(vector type)灵活。虽然,向量是标准库提供的类似集合类型,允许大小缩放。如果不确定使用数组还是向量,则可能就该使用向量。第 8 章细谈向量。

一个使用数组而不是向量的例子:程序中需要知道每个月份的名称,这样的程序不太可能增加或删除月份,这样你可以使用数组,因为你知道它始终包含 12 个元素。

初始化给数组赋值的方式:

// [类型; 长度] 
let a: [i32; 5] = [1, 2, 3, 4, 5];

// [初始值; 长度]
let a = [3; 5]; 
// 等同于
let a = [3, 3, 3, 3, 3];
访问数组元素

数组是栈上分配的单一内存块。你可以通过索引访问数组元素,如下:

fn main() {
    let a = [1, 2, 3, 4];
    let first = a[0];
    let second = a[1];
}
无效的数组元素访问

如果你尝试访问超出数组末尾的数组元素会如何?下面的程序可以通过编译,但是运行会报 panic :

fn main() {
    let a = [1, 2, 4];
    let index = 5;
    let element = a[index];
}

第 9 章讨论错误处理。

函数

Rust 使用 snake case 作为函数和变量名的常规样式,即所有字母小写并用下划线分隔单词。

Rust 中函数可以写在源码中的任何位置,不用关注声明顺序。

函数参数

定义函数时可以拥有形参(parameters),这些特殊变量是函数签名的一部分。 传递给函数的具体值称为实参(arguments)。在日常交谈中,人们倾向于将形参和实参交替用于函数定义中的变量和调用函数时传递的具体值。

函数签名中,必须声明每个参数的类型。这是 Rust 的刻意设计:函数定义中指明类型就意味着编译器不需要在其他地方推断你的意图。

如果函数需要多个参数,定义时用逗号分隔。

函数体包含语句(Statements)和表达式(Expressions)

函数体由一系列可选的表达式结尾的语句组成。至此,我们仅介绍了没有结尾表达式的函数,但是你已经将表达式视为语句的一部分了。 因为 Rust 时基于表达式的语言,所以这里要理解它们重要的区别。其他语言中没有类似的区别,下面我们看看语句和表达式的差异如何影响函数体。

我们实际上已经使用过语句和表达式。语句是执行某些操作但没有返回值的指令。表达式的计算结果是结果值。举个例子。

创建一个变量并使用 let 关键字为其赋值就是一条语句。

fn main() {
    let y = 6; // 这是一条语句
}

函数定义也是语句;前面整个实例就是一个语句。

语句没有返回值。所以,你无法用 let 语句为另一个变量赋值。下面的例子是错误的:

fn main() {
    let x = (let y = 6); // 这是错误的
}

let y = 6 语句不会有返回值,因此 x 不会绑定任何内容。不想其他语言,例如 C 和 Ruby ,赋值会在赋值的位置返回,其他语言中你可以写 x = y = 6 来为 x 和 y 共同赋值。

表达式求值,将出现在你之后编写的大部分 Rust 代码中。 表达式可以是语句的一部分:下个例子中,6 就是语句 let y = 6 中的表达式。调用函数是一个表达式。调用宏一个表达式。创建新作用域的代码块也是一个表达式,例如:

fn main() {
    let x = 5;
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {}", y);
}

注意 x + 1 这一行没有分号。表达式就不包含结尾的分号。如果你在结尾加了分号,就把它变成了语句,即不会有返回值。 在接下来探索返回值和表达式时,请记住这一点。

函数返回值

函数可以把值返回给代码调用方。我们没有命名返回值,但是要在 -> 后声明它们的类型。 Rust 中,函数的返回值与函数体中的最终表达式的值同义。你可以用 return 关键字提前返回,但是大多数函数都隐式返回最后一个表达式。这是一个带返回值的函数的实例:

fn five() -> i32 {
    5
}

fn main() {
    let x = five();
    println!("The value of x is :{}", x);
}

这个 five 函数在 Rust 中是合法的。

另一个例子:

fn main() {
    let x = plus_one(5);
    println!("The value of x is: {}", x);
}

// 这是个错误的函数
fn plus_one(x: i32) -> i32 {
    x + 1; 
}

编译错误如下:

 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |    --------            ^^^ expected `i32`, found `()`
  |    |
  |    implicitly returns `()` as its body has no tail or `return` expression
8 |     x + 1; 
  |          - help: consider removing this semicolon

主要报错信息 “类型不匹配” 揭示了这段代码的核心问题。由于 x + 1; 是语句不会有返回值,则末尾表达式就变成了 (), 一个空的元组。什么都不返回,这与函数的定义相矛盾导致错误。并且在此输出中, Rust 提供了一条建议,可能纠正这个错误:删除分号,可解决错误。

注释

Rust 中的注释必须以双斜线开头,对于超过一行的注释,你需要在每一行开头都加上 // 。

Rust 拥有另一种注释 —— 文档注释,我们在第 14 章关于如何发布 crate 到 crate.io 中讲述。

控制流

在大多数编程语言中,根据条件是否为真来决定是否运行某块代码,而在条件为真时来决定重复运行一些代码是基本的构建块。让你控制 Rust 代码执行流程的最常见构造是 if 表达式和循环。

if 表达式

与 if 表达式中的条件相关联的代码块有时称为手臂(arms),就像之前猜数字游戏中讨论的 match 表达式中的 arm 一样。

可选的,我们可以使用 else 表达式给程序在条件为假时执行一个替代的代码块。

值得注意的时,条件必须是 bool 类型。不像 Ruby 或者 JavaScript, Rust 不会自动将非布尔类型转换为布尔类型。

使用 else if 处理多条件

过多的 else if 表达式使代码混乱,因此如果超过一个,就要考虑重构你的代码。第六章描述 Rust 中强大的分支选择结构,称为 match 。

let 语句中使用 if

例子:

fn mail() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    println!("The value of number is: {}", number);
}

变量 number 将会被 if 表达式的输出值绑定。

请记住,代码块的计算结果为它们中的最后一个表达式,数字本身也是表达式。所以在这个例子中,整个 if 表达式的值取决于要执行的代码块。这意味着来自 if 的每个分支的值必须使同一类型。如果类型不匹配,就会报错:

fn main() {
    let condition = true;
    let number = if condition { 
        5
    } else {
        "six" // 类型不匹配
    };

    println!("The value of number is: {}", number);
}

编译时就会报错。如果仅在运行时确定数字类型, Rust 将无法做到这一点;如果编译器必须跟踪任何变量的多个假设类型,那么编译器会过于复杂且对代码缺少保证。

循环重复

Rust 中有三种循环: loop, while 和 for。

用 loop 重复代码

fn main() {
    loop {
        println!("again!");
    }
}

运行这段代码会重复输出 again! 直到通过 ctrl+c 中断程序。

使用 break 关键字可以跳出 loop 循环。

loop 循环中返回值

loop 的用途之一是重试你知道可能会失败的操作,例如检查线程是否完成了工作。然而,你也许需要把结果传递给其余代码,要做到这一点,你可以在 break 表达式之后协商你要返回的值。

fn main() {
    let mut counter = 0;
    let result = loop {
        counter += 1;
        if counter == 10 {
            break counter * 2;
        }
    };

    println!("The result is: {}", result);
}

while 条件循环

fn main() {
    let mut number = 3;

    while number != 0 {
        println!("{}!", number);

        number -= 1;
    }

    println!("LIFTOFF!!!");
}

使用 while 循环,可以消除许多 loop if else break 结构产生的嵌套,使代码更清晰。

用 for 循环遍历一个集合

用 while 循环遍历集合是这样的:

fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

    while index < 5 {
        println!("the value is: {}", a[index]);

        index += 1;
    }
}

使用 for 循环,就更加简洁:

fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
}

使用 while 循环遍历集合时,如果 a 集合少了元素,而 while 循环的条件没有修改,代码会 panic ,当你使用 for 循环时,不需要担心这个问题。

for 循环的安全性和简洁性使其称为 Rust 中最常用的循环构造。即使在你想要多次运行某段代码的情况下,多数 Rust 程序员也选择用 for 循环。为了做到这一点要使用 Range ,它是标准库提供的一种类型,产生一个数字序列。

这是一个使用 for 循环的倒计数程序,它还包含我们未提到过得方法,用于反转范围(range):

fn main() {
    for number in (1..4).rev() {
        println!("{}!", number);
    }
    println!("LIFTOFF!!!");
}

这段代码更好些,对吧?

总结

本章你学到了:变量、标量以及符合类型,函数,注释,if表达式,还有循环。通过以下习题巩固本章讨论的概念:

当你准备好继续前进时,我们将讨论 Rust 中其他编程语言不常见的概念:所有权(ownership)。