Rust学习(四)-- 通用编程概念

Rust学习(四)-- 通用编程概念

  • 变量与可变性
  • 数据类型
    • 标量类型
    • 复合类型
  • 函数
  • 注释
  • 控制流

变量与可变性

  • 声明变量使用let关键字
  • 默认情况下,变量是不可变的(Immutable)
  • 声明变量时,在变量前面加mut,就可以使变量可变。

变量和常量

  • 常量(constant),常量在绑定值以后也是不可变的,但是它与不可变的变量有很多区别:

    • 不可以使用mut,常量永远都是不可变的
    • 声明常量使用 const 关键字,它的类型必须被标注
    • 常量可以在任何作用域内进行声明,包括全局作用域
    • 常量只可以绑定到常量表达式,无法绑定到函数的调用结果或只能在运行时才能计算出的值
  • 在程序运行期间,常量在其声明的作用域内一直有效

  • 命名规范:Rust锂常量使用全大写字母,每个单词之间使用下划线分类例如:MAX_POINTS

Shadowing

  • 可以使用相同的名字声明新的变量,新的变量就会shadow之前声明的同名变量
    • 在后续的代码中这个变量名代表的就是新的变量
  • shadow 和把变量标记为 mut 是不一样的:
    • 如果不使用 let 关键字,那么重新给非mut的变量赋值会导致编译时错误
    • 而使用let关键字声明的同名新变量,也是不可变的
    • 使用let声明的同名新变量,它的类型可以与之前不同
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main(){
  let x = 5;
  let x = x+1;
  {
    let x =  x * 2;
    println!("The value of x in the inner scope is: {}", x);
  }

  println("The value of x is {}", x);
}

这个程序首先将 x 绑定到 5 上,接着通过 let x = 创建了一个新变量 x,获取初始值并加1,这样x的值就变成了6了,然后,在花括号创建的内部作用域内,第三个 let 语句也隐藏了 x,并创建了一个新的变量,将之前的值乘以 2,x得到的值是12,当该作用域结束时,内部 shadowing的作用域也结束了,x又回到了 6,运行这段程序,它会有如下输出:

1
2
3
4
5
6
❯ cargo run
   Compiling project-1 v0.1.0 (D:\www\demos\rust\project-1)
    Finished dev [unoptimized + debuginfo] target(s) in 0.46s
     Running `target\debug\project-1.exe`
The value of x in the inner scope is 12
The value of x is 6

隐藏与将变量标记为 mut 是有区别的,当不小心尝试对变量重新赋值时,如果没有使用let关键字,就会导致编译时错误,通过使用let,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不可变的

mut 与隐藏的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,并且复用这个名字,例如,假设程序请求用户输入空格字符串来说明希望在文本之间显示多个空格,接下来我们 将输入存储成数字(多少个空格):

1
2
let spaces = "   ";
let spaces = spaces.len();

第一个 spaces 变量是字符串类型,第二个 spaces 变量是数字类型。隐藏使我们不必使用不同的名字,如 spaces_strspaces_num;相反,我们可以复用 spaces 这个更简单的名字。然而,如果尝试使用 mut,将会得到一个编译时错误


数据类型

Rust 中,每一个值都属于某一个 数据类型(data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。我们将看到两类数据类型子集:标量(scalar)和复合(compound)。

记住,Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。根据值及其使用方式,编译器通常可以推断出我们想要用的类型。当多种类型均有可能时, “比较猜测的数字和秘密数字” 使用 parse 将 String 转换为数字时,必须增加类型注解,像这样:

1
let guess: u32 = "42".parse().expect("Not a number");

如果不像上面的代码这样添加类型注解 : u32,Rust 会显示如下错误,这说明编译器需要我们提供更多信息,来了解我们想要的类型

标量类型

标量(scalar)类型代表一个单独的值。Rust有四种基本的标量类型:整形、浮点型、布尔类型和字符串类型,你可能在其他语言中建国它们。让我们深入了解它们在Rust中是如何工作的。

整型

整数是一个没有小数部分的数字,该类型声明表明,它关联的值应该是一个占据32比特位的无符号整数(有符号整数类型以 i 开头而不是 u)。

Rust中的整形

长度 有符号 无符号
8-bit i8 u8
16-bit i16 u16
32-bit i32 u32
64-bit i64 u64
128-bit i128 u128
arch isize usize

每一个有符号的变体可以储存包含从 -(2^(n - 1)) 到 2^(n - 1) - 1 在内的数字,这里 n 是变体使用的位数。所以 i8 可以储存从 -(2^7) 到 2^7 - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 2^n - 1 的数字,所以 u8 可以储存从 0 到 2^8 - 1 的数字,也就是从 0 到 255。

另外,isizeusize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的,32 位架构上它们是 32 位的。

Rust 中的整型字面量

数字字面量 例子
Decimal(十进制) 98_222
Hex(十六进制) 0xff
Octal(八进制) 0o77
Binary(二进制) 0b1111_0000
Byte(字节) b’A'

【注意】:整型溢出 比如说有一个 u8 类型的变量 num,它可以存储从0-255的值,那么当你将其修改为256时会发生什么呢?这被称为整型溢出integer overflow),这会导致以下两种行为之一的发生,当在debug模式时,Rust检查这类问题并使程序panic,这个术语被Rust用来表明程序因错误而退出 使用 –release flag 在 release 模式中构建时,Rust不会检测会导致panic的整型溢出。相反发生整型溢出时,Rust会进行一种被称为二进制补码wrapping(two’s complement wrapping)的操作。简而言之,比此类型能容纳的最大值还大的值会绕到最小值,值256会变成0,值257会变成1,以此类推。程序不会 panic 为了显式的处理溢出的可能性,可以使用者几类标准库提供的原始数字类型方法:

  • 所有模式下都可以使用 wrapping_*方法进行 wrapping,如 wrapping_add
  • 如果 checked_* 方法出现溢出,则返回 None
  • overflowing_* 方法返回值和一个布尔值,布尔值表示是否溢出
  • saturating_* 方法在值的最小值或最大值处进行饱和处理

浮点型

Rust 也有两个原生的 浮点数(floating-point numbers)类型,它们是带小数点的数字。Rust 的浮点数类型是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。所有的浮点型都是有符号的。

1
2
3
4
5
fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

浮点数采用 IEEE-754 标准表示。f32 是单精度浮点数,f64 是双精度浮点数。

数值运算

Rust 中所有数字类型都支持数字运算:加法、减法、乘法、除法和取余。整数除法会向零舍入到最接近的的整数。下面的代码展示了如何在 let 语句中使用它们:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
fn main() {
    // addition
    let sum = 5 + 10;

    // subtraction
    let diffrence = 95.5 - 4.3;

    // multiplication
    let product = 4 * 30;

    // division
    let quotient = 56.7 / 32.2;
    let truncated = -5 / 3; // 结果为 -1

    // remainder
    let remainder = 43 % 5;

    println!(
        "sum: {}, diffrence: {}, product: {}, quotient: {}, truncated: {}, remainder: {}",
        sum, diffrence, product, quotient, truncated, remainder
    )

    // 输出结果: 
    // sum: 15, diffrence: 91.2, product: 120, quotient: 1.7608695652173911, truncated: -1, remainder: 3
}

布尔型

正如其他大部分编程语言一样,Rust中的布尔类型有两个可能得值:truefalse。Rust中的布尔类型使用 bool 表示。例如:

1
2
3
4
fn main(){
  let t = true;
  let f:bool
}

字符类型

Rust的 char类型是语言中最原生的字母类型,下面是一些声明 char 值的例子:

1
2
3
4
5
fn main(){
  let c = 'z';
  let z:char = 'Z';
  let heart_eyed_cat = '😻';
}

注意,我们用单引号声明 char 字面量,而与之相反的是,使用双引号声明字符串字面量。Rust的char类型的大小为四个字节(four bytes),并代表了一个 Unicode标量值(Unicode scalar value),这意味着它可以比ASCII表示更多的内容。在Rust中,带变音符号的字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的char值。Unicode标量值包含 U+0000U+D7FFU+E000U+10FFFF 之间的所有值。不过,“字符”并不是一个Unicode中的概念,所以人直觉上的”字符“可能与Rust中的char并不符合。

复合类型

复合类型(compound types)可以将多个值组合成一个类型。Rust有两个原生的符合类型:元组(tuple)和数组(array)。

元组

元组是一个将多个其他类型的值组合进一个符合类型的主要方式。元组长度固定:一旦声明,其长度不会增大或缩小。 我们使用包含在圆括号中的逗号分割的值列表来创建一个元组。元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的,:

1
2
3
fn main(){
  let tup:(f32, f64, u8) = (500, 6.4, 1);
}

tup变量绑定到整个元组上,因为元组是一个单独的复合元素。为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(distructure)元组值 like this:

1
2
3
4
5
fn main(){
  let tup: (u32, &str, f64) = (3, "hello", 3.14);
  let (x, y, z) = tup;
  println!("The x is {}, y is {}, z is {}", x, y, z);
}

程序首先创建了一个元组并绑定到tup变量上。接着使用了let 和一个模式将tup分成了三个不同的变量,xyz。这叫做解构,因为它将一个元组拆成了三个部分。 我们也可以使用符号(.)后跟值的索引来直接访问它们。 例如:

1
2
3
4
5
6
7
8
9
fn main(){
  let tup:(i32, f64, u8) = (500, 6.4, 1);

  let five_hundred = x.0;

  let six_point_four = x.1;

  let one = x.2;
}

不带任何值的元组有个特殊的名称,叫做**单元(uint)**元组。这种值以及对应的类型都写作(),表示空值或空的返回类型。如果表达式不返回任何其他值,则会隐式返回单元值。

数组类型

另一个包含多个值的方式是数组(array)。与元组不同,数组中的每个元素的类型必须相同。Rust 中的数组与一些其他语言中的数组不同,Rust 中的数组长度是固定的。 我们将数组的值写成在方括号内,用逗号分隔:

1
2
3
fn main(){
  let arr = [1,2,3,4,5];
}

当你想要在栈(stack)而不是在堆(heap)上为数据分配空间,或者是想要确保总是有固定数量的元素时,数组非常有用。但是数组并不如 vector 类型灵活。vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。当不确定是应该使用数组还是 vector 的时候,那么很可能应该使用 vector。

然而,当你确定元素个数不会改变时,数组会更有用。例如,当你在一个程序中使用月份名字时,你更应趋向于使用数组而不是 vector,因为你确定只会有 12 个元素。

1
2
3
4
fn main(){
  let months = ["January", "February", "March", "April", "May", "June", "July",
              "August", "September", "October", "November", "December"];
}

可以像这样编写数组的类型:在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。

1
2
3
fn main() {
  let a:[i32; 5] = [1,2,3,4,5];
}

这里,i32是每个元素的类型。分号之后,数字5表名改数组包含五个元素。 还可以通过在方括号内指定初始值加分号再加元素个数来创建一个每个元素都为相同值的数组:

1
2
3
fn main(){
  let a = [3; 5];
}

变量名为 a 的数组将包含 5 个元素,这些元素的值最初都将被设置为 3。这种写法与 let a = [3, 3, 3, 3, 3]; 效果相同,但更简洁。

  • 访问数组元素 数组是可以在栈(stack)上分配的一直固定大小的单个内存块。可以使用索引来访问数组的元素,像这样:
1
2
3
4
5
6
fn main(){
  let a = [1,2,3,4,5];

  let first = a[0];
  let second = a[1];
}

在这个例子中,叫做first的变量的值是 1,因为它是数组索引 [0]的值。变量second 将会是数组索引 [1] 的值 2

  • 无效的数组元素访问

让我们看看如果我们访问数组结尾之后的元素会发生什么呢?比如你执行以下代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
use std::io;

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

    println!("Please enter an array index.");

    let mut index = String::new();

    io::stdin()
        .read_line(&mut index)
        .expect("Failed to read line");

    let index: usize = index
        .trim()
        .parse()
        .expect("Index entered was not a number");

    let element = a[index];

    println!("The value of the element at index {index} is: {element}");
}

此代码编译成功。如果您使用 cargo run 运行此代码并输入 0、1、2、3 或 4,程序将在数组中的索引处打印出相应的值。如果你输入一个超过数组末端的数字,如 10,你会看到这样的输出:

1
2
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

程序在索引操作中使用一个无效的值时导致 运行时 错误。程序带着错误信息退出,并且没有执行最后的 println! 语句。当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic,这是 Rust 术语,它用于程序因为错误而退出的情况。这种检查必须在运行时进行,特别是在这种情况下,因为编译器不可能知道用户在以后运行代码时将输入什么值。

这是第一个在实战中遇到的 Rust 安全原则的例子。在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。通过立即退出而不是允许内存访问并继续执行,Rust 让你避开此类错误。


函数

函数在 Rust 代码中非常普遍。你已经见过语言中最重要的函数之一:main 函数,它是很多程序的入口点。你也见过 fn 关键字,它用来声明新函数。

Rust 代码中的函数和变量名使用 snake case 规范风格。在 snake case 中,所有字母都是小写并使用下划线分隔单词。这是一个包含函数定义示例的程序:

1
2
3
4
5
6
7
8
9
fn main(){
  println!("Hello world");

  another_function();
}

fn another_function(){
  println!("Another function.");
}

我们在 Rust 中通过输入 fn 后面跟着函数名和一对圆括号来定义函数。大括号告诉编译器哪里是函数体的开始和结尾。

可以使用函数名后跟圆括号来调用我们定义过的任意函数。因为程序中已定义 another_function 函数,所以可以在 main 函数中调用它。注意,源码中 another_function 定义在 main 函数 之后;也可以定义在之前。Rust 不关心函数定义所在的位置,只要函数被调用时出现在调用之处可见的作用域内就行。

参数

我们可以定义为拥有 参数(parameters)的函数,参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为参数(arguments),但是在日常交流中,人们倾向于不区分使用 parameter 和 argument 来表示函数定义中的变量或调用函数时传入的具体值。

在这版 another_function 中,我们增加了一个参数:

1
2
3
4
5
6
7
fn main() {
    another_function(5);
}

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

尝试运行程序,将会输出如下内容:

1
2
3
4
5
$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/functions`
The value of x is: 5

在函数签名中,必须 声明每个参数的类型。这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器再也不需要你在代码的其他地方注明类型来指出你的意图。而且,在知道函数需要什么类型后,编译器就能够给出更有用的错误消息。

当定义多个参数时,使用逗号分隔,像这样:

1
2
3
4
5
6
7
fn main() {
    print_labeled_measurement(5, 'h');
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

这个例子创建了一个名为 print_labeled_measurement 的函数,它有两个参数。第一个参数名为 value,类型是 i32。第二个参数是 unit_label ,类型是 char。然后,该函数打印包含 valueunit_label 的文本。

语句和表达式

函数体由一系列的语句和一个可选的结尾表达式构成。目前为止,我们提到的函数还不包含结尾表达式,不过你已经见过作为语句一部分的表达式。因为 Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。其他语言并没有这样的区别,所以让我们看看语句与表达式有什么区别以及这些区别是如何影响函数体的。

语句(Statement)是执行一些操作但不返回值的指令。表达式(expressions)计算并产生一个值。让我们看一些例子: 实际上,我们已经使用过语句和表达式了,使用let关键字创建变量并绑定一个值是一个语句。

1
2
3
fn main(){
  let y = 6;
}

函数定义也是语句,上面整个例子本身就是一个语句。 语句不返回值。因此,不能把 let 语句赋值给另一个变量,因为 let 语句本身没有返回值。比如下面的例子尝试做的,会产生一个错误:

1
  let x = (let y = 6);

当运行这个程序时,会得到如下错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
error: expected expression, found `let` statement
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^

error: expected expression, found statement (`let`)
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: variable declaration using `let` is a statement

error[E0658]: `let` expressions in this position are unstable
 --> src/main.rs:2:14
  |
2 |     let x = (let y = 6);
  |              ^^^^^^^^^
  |
  = note: see issue #53667 <https://github.com/rust-lang/rust/issues/53667> for more information

warning: unnecessary parentheses around assigned value
 --> src/main.rs:2:13
  |
2 |     let x = (let y = 6);
  |             ^         ^
  |
  = note: `#[warn(unused_parens)]` on by default
help: remove these parentheses
  |
2 -     let x = (let y = 6);
2 +     let x = let y = 6;
  |

For more information about this error, try `rustc --explain E0658`.
warning: `functions` (bin "functions") generated 1 warning
error: could not compile `functions` due to 3 previous errors; 1 warning emitted

let y = 6 语句并不返回值,所以没有可以绑定到x上的值。这与其他语言不同,例如 C 和 Ruby,他们的赋值语句会返回所赋的值。在这些语言中,可以这么写 x = y = 6,这样 xy 的值都是 6; Rust 中不能这么写。

表达式会计算出一个值,并且你将编写的大部分 Rust 代码是由表达式组成的。考虑一个数学运算,比如 5 + 6,这是一个表达式并计算出值 11。表达式可以是语句的一部分:在示例中,语句 let y = 6; 中的 6 是一个表达式,它计算出的值是 6。函数调用是一个表达式。宏调用是一个表达式。用大括号创建的一个新的块作用域也是一个表达式,例如:

1
2
3
4
5
6
7
8
fn main(){
  let y = {
    let x = 3;
    x + 1 // 【注意】没有 ";"
  }

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

这个表达式:

1
2
3
4
fn main(){
  let x = 3;
  x + 1
}

是一个代码块,它的值是 4。这个值作为 let 语句的一部分被绑定到 y 上。注意 x + 1 这一行在结尾没有分号,与你见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的结尾加上分号,它就变成了语句,而语句不会返回值。在接下来探索具有返回值的函数和表达式时要谨记这一点。

具有返回值的函数

函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->)后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return关键字和指定值,可从函数中提前返回;但大部分函数隐式的返回最后的表达式。这是一个有返回值的函数的例子:

1
2
3
4
5
6
7
8
9
fn five() -> i32 {
    5
}

fn main() {
    let x = five();

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

five 函数中没有函数调用、宏、甚至没有 let 语句 —— 只有数字 5。这在 Rust 中是一个完全有效的函数。注意,也指定了函数返回值的类型,就是 -> i32。尝试运行代码;输出应该看起来像这样:

1
2
3
4
5
$ cargo run
   Compiling functions v0.1.0 (file:///projects/functions)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/functions`
The value of x is: 5

five 函数的返回值是 5,所以返回值类型是 i32。让我们仔细检查一下这段代码。有两个重要的部分:首先,let x = five(); 这一行表明我们使用函数的返回值初始化一个变量。因为 five 函数返回 5,这一行与如下代码相同:

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

其次,five 函数没有参数并定义了返回值类型,不过函数体只有单单一个 5 也没有分号,因为这是一个表达式,我们想要返回它的值。

让我们看看另外一个例子:

1
2
3
4
5
6
7
8
9
fn main() {
    let x = plus_one(5);

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

fn plus_one(x: i32) -> i32 {
    x + 1
}

运行代码会打印出 The value of x is: 6。但如果在包含 x + 1 的行尾加上一个分号,把它从表达式变成语句,我们将看到一个错误。

主要的错误信息,“mismatched types”(类型不匹配),揭示了代码的核心问题。函数 plus_one 的定义说明它要返回一个 i32 类型的值,不过语句并不会返回值,使用单位类型 () 表示不返回值。因为不返回值与函数定义相矛盾,从而出现一个错误。在输出中,Rust 提供了一条信息,可能有助于纠正这个错误:它建议删除分号,这会修复这个错误。

控制流

if表达式

if 表达式允许根据条件执行不同的代码分支。你提供一个条件并表示“如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”

1
2
3
4
5
6
7
8
9
fn main(){
  let number = 3;

  if number < 5 {
    println!("condition was true");
  }else {
    println!("condition was false");
  }
}

另外值得注意的是代码中的条件 必须 是 bool 值。如果条件不是 bool 值,我们将得到一个错误。例如,尝试运行以下代码:

1
2
3
4
5
6
7
fn main(){
  let number = 3;
  // 这里if 条件的值是3
  if number {
    println!("number was three");
  }
}

Rust抛出了一个错误:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: mismatched types
 --> src/main.rs:4:8
  |
4 |     if number {
  |        ^^^^^^ expected `bool`, found integer

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

这个错误表明 Rust 期望一个 bool 却得到了一个整数。不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。必须总是显式地使用布尔值作为 if 的条件。

使用if else处理多重条件

可以将 else if 表达式与 ifelse 组合来实现多重条件。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let number = 6;

    if number % 4 == 0 {
        println!("number is divisible by 4");
    } else if number % 3 == 0 {
        println!("number is divisible by 3");
    } else if number % 2 == 0 {
        println!("number is divisible by 2");
    } else {
        println!("number is not divisible by 4, 3, or 2");
    }
}

这个程序有四个可能的执行路径。运行后应该能看到如下输出:

1
2
3
4
5
$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target/debug/branches`
number is divisible by 3

当执行这个程序时,它按顺序检查每个 if 表达式并执行第一个条件为 true 的代码块。注意即使 6 可以被 2 整除,也不会输出 number is divisible by 2,更不会输出 else 块中的 number is not divisible by 4, 3, or 2。原因是 Rust 只会执行第一个条件为 true 的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。

在let中使用if

因为 if 是一个表达式,我们可以在 let 语句的右侧使用它:

1
2
3
4
5
6
7
fn main(){
  let condition = true;

  let number = if condition {5} else {6};

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

number变量将会绑定到表示if表达式结果的值上。运行这段代码看看会出现什么:

1
2
3
4
5
$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
    Finished dev [unoptimized + debuginfo] target(s) in 0.30s
     Running `target/debug/branches`
The value of number is: 5

记住,代码块的值是最后一个表达式的值,而数字本身就是一个表达式。在这个例子中,整个if表达式的值取决于哪个代码块被执行。这意味着 if 的每个分支的可能的返回值都必须是相同类型;在上例中,if分支和else分支的结果都是i32整型。如果它们的类型不匹配,如下面这个例子,则会出现一个错误:

1
2
3
4
5
6
7
fn main() {
    let condition = true;

    let number = if condition { 5 } else { "six" };

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

当编译这段代码时,会得到一个错误。ifelse 分支的值类型是不相容的,同时 Rust 也准确地指出在程序中的何处发现的这个问题:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
$ cargo run
   Compiling branches v0.1.0 (file:///projects/branches)
error[E0308]: `if` and `else` have incompatible types
 --> src/main.rs:4:44
  |
4 |     let number = if condition { 5 } else { "six" };
  |                                 -          ^^^^^ expected integer, found `&str`
  |                                 |
  |                                 expected because of this

For more information about this error, try `rustc --explain E0308`.
error: could not compile `branches` due to previous error

if 代码块中的表达式返回一个整数,而 else 代码块中的表达式返回一个字符串。这不可行,因为变量必须只有一个类型。Rust 需要在编译时就确切的知道 number 变量的类型,这样它就可以在编译时验证在每处使用的 number 变量的类型是有效的。如果 number 的类型仅在运行时确定,则 Rust 无法做到这一点;且编译器必须跟踪每一个变量的多种假设类型,那么它就会变得更加复杂,对代码的保证也会减少。

使用循环重复执行

Rust有三种循环:loopwhilefor。我们每一个都试试。

使用 loop 重复执行代码

loop关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main(){
  
  let mut num = 0;
  loop {
      println!("The number is {}", num);
      num += 1;
      if num == 10 {
          break;
      }
  }
}

从循环返回值

loop 的一个用例是重试可能会失败的操作,比如检查线程是否完成了任务。然而你可能会需要将操作的结果传递给其它的代码。如果将返回值加入你用来停止循环的 break 表达式,它会被停止的循环返回:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
fn main() {
    let mut counter = 0;

    let result = loop {
        counter += 1;

        if counter == 10 {
            break counter * 2;
        }
    };

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

在循环之前,我们声明了一个名为 counter 的变量并初始化为 0。接着声明了一个名为 result 来存放循环的返回值。在循环的每一次迭代中,我们将 counter 变量加 1,接着检查计数是否等于 10。当相等时,使用 break 关键字返回值 counter * 2。循环之后,我们通过分号结束赋值给 result 的语句。最后打印出 result 的值,也就是 20。

循环标签:在多个循环之间消除歧义

如果存在嵌套循环,breakcontinue 应用于此时最内层的循环。你可以选择在一个循环上指定一个 循环标签(loop label),然后将标签与 breakcontinue 一起使用,使这些关键字应用于已标记的循环而不是最内层的循环。下面是一个包含两个嵌套循环的示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
fn main() {
    let mut count = 0;
    'counting_up: loop {
        println!("count = {count}");
        let mut remaining = 10;

        loop {
            println!("remaining = {remaining}");
            if remaining == 9 {
                break;
            }
            if count == 2 {
                break 'counting_up;
            }
            remaining -= 1;
        }

        count += 1;
    }
    println!("End count = {count}");
}

外层循环有一个标签 counting_up,它将从 0 数到 2。没有标签的内部循环从 10 向下数到 9。第一个没有指定标签的 break 将只退出内层循环。break 'counting_up; 语句将退出外层循环。这个代码打印:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.58s
     Running `target/debug/loops`
count = 0
remaining = 10
remaining = 9
count = 1
remaining = 10
remaining = 9
count = 2
remaining = 10
End count = 2

while 条件循环

在程序中计算循环的条件也很常见。当条件为 true,执行循环。当条件不再为 true,调用 break 停止循环。这个循环类型可以通过组合 loop、if、else 和 break 来实现;如果你喜欢的话,现在就可以在程序中试试。

然而,这个模式太常用了,Rust 为此内置了一个语言结构,它被称为 while 循环。下例中使用了 while:程序循环三次,每次数字都减一。接着,在循环结束后,打印出另一个信息并退出。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
fn main() {
    let mut number = 3;

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

        number -= 1;
    }

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

这种结构消除了很多使用 loop、if、else 和 break 时所必须的嵌套,这样更加清晰。当条件为 true 就执行,否则退出循环。

使用 for 遍历集合

可以使用 while 结构来遍历集合中的元素,比如数组。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

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

        index += 1;
    }
}

这里,代码对数组中的元素进行计数。它从索引 0 开始,并接着循环直到遇到数组的最后一个索引(这时,index < 5 不再为真)。运行这段代码会打印出数组中的每一个元素:

1
2
3
4
5
6
7
8
9
$ cargo run
   Compiling loops v0.1.0 (file:///projects/loops)
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/loops`
the value is: 10
the value is: 20
the value is: 30
the value is: 40
the value is: 50

数组中的所有五个元素都如期被打印出来。尽管 index 在某一时刻会到达值 5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。

但这个过程很容易出错;如果索引长度或测试条件不正确会导致程序 panic。例如,如果将 a 数组的定义改为包含 4 个元素而忘记了更新条件 while index < 4,则代码会 panic。这也使程序更慢,因为编译器增加了运行时代码来对每次循环进行条件检查,以确定在循环的每次迭代中索引是否在数组的边界内。

作为更简洁的替代方案,可以使用 for 循环来对一个集合的每个元素执行一些代码。for 循环看起来如下示例所示:

1
2
3
4
5
6
7
fn main() {
    let a = [10, 20, 30, 40, 50];

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

for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。即使是在想要循环执行代码特定次数时,大部分 Rustacean 也会使用 for 循环。这么做的方式是使用 Range,它是标准库提供的类型,用来生成从一个数字开始到另一个数字之前结束的所有数字的序列。下面是一个使用 for 循环来倒计时的例子,它还使用了一个我们还未讲到的方法,rev,用来反转 range。

注意:以下代码不会踏足到数字 4,仅从一个数字开始到另一个数字之前。

1
2
3
4
5
6
fn main() {
    for number in (1..4).rev() {
        println!("{number}!");
    }
    println!("LIFTOFF!!!");
}

每日一算

  • 题目: 3的幂

  • 来源:leetcode 326

  • 难度:简单

  • 描述: 给定一个整数,写一个函数来判断它是否是3的幂次方。如果是,返回 true,否则,返回 false。 整数 n 是3的幂次方需满足:存在整数 x 使得 n == 3^x

  • 算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
fn main(){
  let n = 9;
  println!("{}", is_three_power(n));
}

fn is_three_power(mut n: i32) -> bool {
  if n > 0 && n % 3 == 0 {
    n /= 3;
  }

  n == 1
}
皖ICP备20014602号
Built with Hugo
Theme Stack designed by Jimmy