Rust 之旅 - 核心概念


美好的夜晚!

Rust 之旅 - 核心概念

  • 经典的新手编程问题:猜猜看游戏。它是这么工作的:程序将会随机生成一个 1 到 100 之间的随机整数。接着它会请玩家猜一个数并输入,然后提示猜测是大了还是小了。如果猜对了,它会打印祝贺信息并退出。
  • 涉及概念:变量、基本类型、函数、注释和控制流

关键字(keyword)

目前正在使用的关键字

如下关键字目前有对应其描述的功能。

  • as - 强制类型转换,消除特定包含项的 trait 的歧义,或者对 useextern crate 语句中的项重命名
  • break - 立刻退出循环
  • const - 定义常量或不变裸指针(constant raw pointer)
  • continue - 继续进入下一次循环迭代
  • crate - 链接(link)一个外部 crate 或一个代表宏定义的 crate 的宏变量
  • dyn - 动态分发 trait 对象
  • else - 作为 ifif let 控制流结构的 fallback
  • enum - 定义一个枚举
  • extern - 链接一个外部 crate 、函数或变量
  • false - 布尔字面值 false
  • fn - 定义一个函数或 函数指针类型 (function pointer type)
  • for - 遍历一个迭代器或实现一个 trait 或者指定一个更高级的生命周期
  • if - 基于条件表达式的结果分支
  • impl - 实现自有或 trait 功能
  • in - for 循环语法的一部分
  • let - 绑定一个变量
  • loop - 无条件循环
  • match - 模式匹配
  • mod - 定义一个模块
  • move - 使闭包获取其所捕获项的所有权
  • mut - 表示引用、裸指针或模式绑定的可变性
  • pub - 表示结构体字段、impl 块或模块的公有可见性
  • ref - 通过引用绑定
  • return - 从函数中返回
  • Self - 实现 trait 的类型的类型别名
  • self - 表示方法本身或当前模块
  • static - 表示全局变量或在整个程序执行期间保持其生命周期
  • struct - 定义一个结构体
  • super - 表示当前模块的父模块
  • trait - 定义一个 trait
  • true - 布尔字面值 true
  • type - 定义一个类型别名或关联类型
  • unsafe - 表示不安全的代码、函数、trait 或实现
  • use - 引入外部空间的符号
  • where - 表示一个约束类型的从句
  • while - 基于一个表达式的结果判断是否进行循环

保留做将来使用的关键字

如下关键字没有任何功能,不过由 Rust 保留以备将来的应用。

  • abstract
  • async
  • await
  • become
  • box
  • do
  • final
  • macro
  • override
  • priv
  • try
  • typeof
  • unsized
  • virtual
  • yield

原始标识符

原始标识符(Raw identifiers)允许你使用通常不能使用的关键字,其带有 r# 前缀。

例如,match 是关键字。如果尝试编译如下使用 match 作为名字的函数:

fn match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

会得到这个错误:

error: expected identifier, found keyword `match`
 --> src/main.rs:4:4
  |
4 | fn match(needle: &str, haystack: &str) -> bool {
  |    ^^^^^ expected identifier, found keyword

该错误表示你不能将关键字 match 用作函数标识符。你可以使用原始标识符将 match 作为函数名称使用:

文件名: src/main.rs

fn r#match(needle: &str, haystack: &str) -> bool {
    haystack.contains(needle)
}

fn main() {
    assert!(r#match("foo", "foobar"));
}

此代码编译没有任何错误。注意 r# 前缀需同时用于函数名定义和 main 函数中的调用。

原始标识符允许使用你选择的任何单词作为标识符,即使该单词恰好是保留关键字。

变量和可变性

  • 变量默认是不可改变的(immutable)
  • 这是推动你以充分利用 Rust 提供的安全性和简单并发性来编写代码的众多方式之一。不过,你仍然可以使用可变变量。让我们探讨一下 Rust 拥抱不可变性的原因及方法,以及何时你不想使用不可变性。
  • 当变量不可变时,一旦值被绑定一个名称上,你就不能改变这个值。为了对此进行说明,使用 cargo new variables 命令在 projects 目录生成一个叫做 variables 的新项目。
  • 接着,在新建的 variables 目录,打开 src/main.rs 并将代码替换为如下代码,这些代码还不能编译:
fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}
  • 保存并使用 cargo run 运行程序。应该会看到一条错误信息,如下输出所示:

  • 这个例子展示了编译器如何帮助你找出程序中的错误。虽然编译错误令人沮丧,但那只是表示程序不能安全的完成你想让它完成的工作;并 不能 说明你不是一个好程序员!经验丰富的 Rustacean 们一样会遇到编译错误。

错误信息指出错误的原因是 不能对不可变变量 x 二次赋值cannot assign twice to immutable variable x),因为你尝试对不可变变量 x 赋第二个值。

  • 在尝试改变预设为不可变的值时,产生编译时错误是很重要的,因为这种情况可能导致 bug。如果一部分代码假设一个值永远也不会改变,而另一部分代码改变了这个值,第一部分代码就有可能以不可预料的方式运行。不得不承认这种 bug 的起因难以跟踪,尤其是第二部分代码只是 有时 会改变值。
  • Rust 编译器保证,如果声明一个值不会变,它就真的不会变。这意味着当阅读和编写代码时,不需要追踪一个值如何和在哪可能会被改变,从而使得代码易于推导。
  • 不过可变性也是非常有用的。变量只是默认不可变;正如在第二章所做的那样,你可以在变量名之前加 mut 来使其可变。除了允许改变值之外,mut 向读者表明了其他代码将会改变这个变量值的意图。

通过 mut进行修改

  • 通过 mut,允许把绑定到 x 的值从 5 改成 6。在一些情况下,你会想用可变变量,因为与只用不可变变量相比,它会让代码更容易编写。
  • 除了防止出现 bug 外,还有很多地方需要权衡取舍。
  • 例如,使用大型数据结构时,适当地使用可变变量,可能比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用更偏向函数式的编程风格,可能会使代码更易理解,为可读性而牺牲性能或许是值得的。
fn main() {
    //通过 `mut`,允许把绑定到 `x` 的值从 `5` 改成 `6`
    let mut x = 5;
    println!("The value of mut x is: {}", x);
    x = 6;
    println!("The value of x is: {}", x);
}

变量和常量的区别

不允许改变值的变量:常量constants),类似于不可变变量,常量是绑定到一个名称的不允许改变的值,不过常量与变量还是有一些区别。

首先,不允许对常量使用 mut。常量不光默认不能变,它总是不能变。

声明常量使用 const 关键字而不是 let,并且 必须 注明值的类型。

常量可以在任何作用域中声明,包括全局作用域,这在一个值需要被很多部分的代码用到时很有用。

最后一个区别是,常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。

声明常量的例子

  • 它的名称是 MAX_POINTS,值是 100000。
  • Rust 常量的命名规范是使用下划线分隔的大写字母单词,并且可以在数字字面值中插入下划线来提升可读性。
const MAX_POINTS: u32 = 100_000;
  • 在声明它的作用域之中,常量在整个程序生命周期中都有效,这使得常量可以作为多处代码使用的全局范围的值,例如一个游戏中所有玩家可以获取的最高分或者光速。
  • 将遍布于应用程序中的硬编码值声明为常量,能帮助后来的代码维护人员了解值的意图。
  • 如果将来需要修改硬编码值,也只需修改汇聚于一处的硬编码值。

隐藏(Shadowing)

  • 在猜猜看游戏中,我们可以定义一个与之前变量同名的新变量,而新变量会 隐藏 之前的变量。
  • Rustacean 们称之为第一个变量被第二个 隐藏 了,这意味着使用这个变量时会看到第二个值。
  • 可以用相同变量名称来隐藏一个变量,以及重复使用 let 关键字来多次隐藏。
fn main() {
    let x = 5;
    let x = x + 1;
    let x = x * 2;

    println!("The value of x is: {}", x);
    //The value of x is: 12
}
  • 这个程序首先将 x 绑定到值 5 上。
  • 接着通过 let x = 隐藏 x,获取初始值并加 1,这样 x 的值就变成 6 了。
  • 第三个 let 语句也隐藏了 x,将之前的值乘以 2x 最终的值是 12

隐藏与将变量标记为 mut 是有区别的

隐藏与将变量标记为 mut 是有区别的。

当不小心尝试对变量重新赋值时,如果没有使用 let 关键字,就会导致编译时错误。

通过使用 let,我们可以用这个值进行一些计算,不过计算完之后变量仍然是不变的。

mut 与隐藏的另一个区别是,当再次使用 let 时,实际上创建了一个新变量,我们可以改变值的类型,但复用这个名字。

示例

  • 例如,假设程序请求用户输入空格字符来说明希望在文本之间显示多少个空格,然而我们真正需要的是将输入存储成数字(多少个空格)
  • 这里允许第一个 spaces 变量是字符串类型,而第二个 spaces 变量,它是一个恰巧与第一个变量同名的崭新变量,是数字类型。
  • 隐藏使我们不必使用不同的名字,如 spaces_strspaces_num
  • 相反,我们可以复用 spaces 这个更简单的名字。
fn main(){
    let spaces = "   ";
    let spaces = spaces.len();
    println!("The value of spaces is: {}", spaces);
    //The value of spaces is: 3
}
  • 然而,如果尝试使用 mut,将会得到一个编译时错误。
  • 这个错误说明,我们不能改变变量的类型。
//错误
fn main(){
    let mut spaces = "   ";
    spaces = spaces.len();
    println!("The value of spaces is: {}", spaces);
}

数据类型

  • 在 Rust 中,每一个值都属于某一个 数据类型data type),这告诉 Rust 它被指定为何种数据,以便明确数据处理方式。
  • 我们将看到两类数据类型子集:标量(scalar)复合(compound)
  • 记住,Rust 是 静态类型statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
  • 根据值及其使用方式,编译器通常可以推断出我们想要用的类型。
  • 当多种类型均有可能时,比如猜数字游戏使用 parseString 转换为数字时,必须增加类型注解。
  • let guess: u32 = "42".parse().expect("Not a number!");

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

error[E0282]: type annotations needed
 --> src/main.rs:2:9
  |
2 |     let guess = "42".parse().expect("Not a number!");
  |         ^^^^^
  |         |
  |         cannot infer type for `_`
  |         consider giving `guess` a type

标量类型

标量scalar)类型代表一个单独的值。

Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型

整型

整数 是一个没有小数部分的数字。

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

表格展示了 Rust 内建的整数类型。

在有符号列和无符号列中的每一个变体(例如,i16)都可以用来声明整数值的类型。

长度有符号无符号
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

每一个变体都可以是有符号或无符号的,并有一个明确的大小。

有符号无符号 代表数字能否为负值,换句话说,数字是否需要有一个符号(有符号数),或者永远为正而不需要符号(无符号数)。

每一个有符号的变体可以储存包含从 -(2n - 1) 到 2n - 1 - 1 在内的数字,这里 n 是变体使用的位数。

所以 i8 可以储存从 -(27) 到 27 - 1 在内的数字,也就是从 -128 到 127。

无符号的变体可以储存从 0 到 2n - 1 的数字,所以 u8 可以储存从 0 到 28 - 1 的数字,也就是从 0 到 255。

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

注意除 byte 以外的所有数字字面值允许使用类型后缀,例如 57u8,同时也允许使用 _ 做为分隔符以方便读数,例如1_000

Rust 中的整型字面值

数字字面值例子
Decimal98_222
Hex0xff
Octal0o77
Binary0b1111_0000
Byte (u8 only)b'A'

那么该使用哪种类型的数字呢?

  • 如果拿不定主意,Rust 的默认类型通常就很好。
  • 数字类型默认是 i32:它通常是最快的,甚至在 64 位系统上也是。
  • isizeusize 主要作为某些集合的索引。
整型溢出

比方说有一个 u8 ,它可以存放从零到 255 的值。

那么当你将其修改为 256 时会发生什么呢?这被称为 “整型溢出”(“integer overflow” )。

关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 检查这类问题并使程序 panic,这个术语被 Rust 用来表明程序因错误而退出。

在 release 构建中,Rust 不检测溢出,相反会进行一种被称为二进制补码包装(two’s complement wrapping)的操作。

简而言之,256 变成 0257 变成 1,依此类推。

依赖整型溢出被认为是一种错误,即便可能出现这种行为。

如果你确实需要这种行为,标准库中有一个类型显式提供此功能,Wrapping

浮点型

Rust 也有两个原生的 浮点数floating-point numbers)类型,它们是带小数点的数字。

Rust 的浮点数类型是 f32f64,分别占 32 位和 64 位。

默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。

展示浮点数的实例

  • 浮点数采用 IEEE-754 标准表示。
  • f32 是单精度浮点数,f64 是双精度浮点数。
//展示浮点数的实例
fn main() {
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32
}

数值运算

Rust 中的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取余

下面的代码展示了如何在 let 语句中使用它们:

这些语句中的每个表达式使用了一个数学运算符并计算出了一个值,然后绑定给一个变量。

//数值运算
fn main() {
    // 加法
    let sum = 5 + 10;
    println!("The value of sum is: {}", sum);

    // 减法
    let difference = 95.5 - 4.3;
    println!("The value of difference is: {}", difference);

    // 乘法
    let product = 4 * 30;
    println!("The value of product is: {}", product);

    // 除法
    let quotient = 56.7 / 32.2;
    println!("The value of quotient is: {}", quotient);

    // 取余
    let remainder = 43 % 5;
    println!("The value of remainder is: {}", remainder);

}

Rust 提供的所有运算符的列表

布尔型

Rust 中的布尔类型有两个可能的值:truefalse

使用布尔值的主要场景是条件表达式,例如 if 表达式。

Rust 中的布尔类型使用 bool 表示。例如:

//布尔型
fn main() {
    let t = true;
    let f: bool = false; // 显式指定类型注解
}

字符类型

Rust 也支持字母。

Rust 的 char 类型是语言中最原生的字母类型

注意 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)

元组类型

元组是一个将多个其他类型的值组合进一个复合类型的主要方式。

元组长度固定:一旦声明,其长度不会增大或缩小

我们使用包含在圆括号中的逗号分隔的值列表来创建一个元组。

元组中的每一个位置都有一个类型,而且这些不同值的类型也不必是相同的。

可选的类型注解

//复合类型 - 元组类型 - 可选的类型注解
fn main(){
    let tup:(i32,f64,u8) = (500,6.4,1);
}

tup 变量绑定到整个元组上,因为元组是一个单独的复合元素。

使用模式匹配(pattern matching)来解构(destructure)元组值

为了从元组中获取单个值,可以使用模式匹配(pattern matching)来解构(destructure)元组值。

程序首先创建了一个元组并绑定到 tup 变量上。

接着使用了 let 和一个模式将 tup 分成了三个不同的变量,xyz

这叫做 解构destructuring),因为它将一个元组拆成了三个部分。

最后,程序打印出了 y 的值,也就是 6.4

//为了从元组中获取单个值
//,可以使用模式匹配(pattern matching)来解构(destructure)元组值。
fn main() {
    let tup = (500, 6.4, 1);

    let (x, y, z) = tup;

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

使用点号(.)后跟值的索引来直接访问

  • 这个程序创建了一个元组,x,并接着使用索引为每个元素创建新变量。
  • 跟大多数编程语言一样,元组的第一个索引值是 0
//使用点号(.)后跟值的索引来直接访问
fm main(){
    let x: (i32, f64, u8) = (500, 6.4, 1);
    let fh = x.0;
    let spf = x.1;
    let one = x.2;
}

数组类型

另一个包含多个值的方式是 数组array)。

与元组不同,数组中的每个元素的类型必须相同。

Rust 中的数组与一些其他语言中的数组不同,因为 Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小

Rust 中,数组中的值位于中括号内的逗号分隔的列表中:

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

当你想要在栈(stack)而不是在堆(heap)上为数据分配空间,或者是想要确保总是有固定数量的元素时,数组非常有用。

但是数组并不如 vector 类型灵活。

vector 类型是标准库提供的一个 允许 增长和缩小长度的类似数组的集合类型。

当不确定是应该使用数组还是 vector 的时候,你可能应该使用 vector。

想要使用数组而不是 vector 的例子

  • 当程序需要知道一年中月份的名字时,程序不大可能会去增加或减少月份。
  • 这时你可以使用数组,因为我们知道它总是包含 12 个元素
fn main(){
    let months = ["January", "February", "March", "April",
     "May", "June", "July","August",
      "September", "October", "November", "December"];
}

另一种写法

  • 在方括号中包含每个元素的类型,后跟分号,再后跟数组元素的数量。
  • i32 是每个元素的类型。
  • 分号之后,数字 5 表明该数组包含五个元素。
fn main(){
    let a: [i32;5] = [1, 2, 3, 4, 5];
}

另一个初始化数组的语法

如果你希望创建一个每个元素都相同的数组,可以在中括号内指定其初始值,后跟分号,再后跟数组的长度。

let a = [3; 5];

访问数组元素

数组是一整块分配在栈上的内存。可以使用索引来访问数组的元素。

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

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

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

无效的数组元素访问

访问数组结尾之后的元素,编译不过,在运行时会因错误而退出。

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

    let element = a[index];

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

使用 cargo run 运行代码后会产生如下结果:

  • 编译并没有产生任何错误,不过程序会出现一个 运行时runtime)错误并且不会成功退出。
  • 当尝试用索引访问一个元素时,Rust 会检查指定的索引是否小于数组的长度。如果索引超出了数组长度,Rust 会 panic,这是 Rust 术语,它用于程序因为错误而退出的情况。

这是第一个在实战中遇到的 Rust 安全原则的例子。

在很多底层语言中,并没有进行这类检查,这样当提供了一个不正确的索引时,就会访问无效的内存。

通过立即退出而不是允许内存访问并继续执行,Rust 让你避开此类错误。

函数

  • 函数遍布于 Rust 代码中。
  • 你已经见过语言中最重要的函数之一:main 函数,它是很多程序的入口点。
  • 你也见过 fn 关键字,它用来声明新函数。
  • Rust 代码中的函数和变量名使用 snake case 规范风格。
  • 在 snake case 中,所有字母都是小写并使用下划线分隔单词

示例

  • Rust 中的函数定义以 fn 开始并在函数名后跟一对圆括号。
  • 大括号告诉编译器哪里是函数体的开始和结尾。
  • 可以使用函数名后跟圆括号来调用我们定义过的任意函数。
  • 因为程序中已定义 another_function 函数,所以可以在 main 函数中调用它。
  • 注意,源码中 another_function 定义在 main 函数 之后;也可以定义在之前。
  • Rust 不关心函数定义于何处,只要定义了就行。
fn main() {
    println!("Hello, world!");

    another_function();
}

fn another_function(){
    println!("another");
}

  • main 函数中的代码会按顺序执行。
  • 首先,打印 “Hello, world!” 信息,然后调用 another_function 函数并打印它的信息。

函数参数

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

示例

//函数参数
fn main() {
    another_function(5);
}

fn another_function(x: i32){

    //x的值为:5
    println!("x的值为:{}",x);
}
  • another_function 的声明中有一个命名为 x 的参数。
  • x 的类型被指定为 i32
  • 当将 5 传给 another_function 时,println! 宏将 5 放入格式化字符串中大括号的位置。
  • 在函数签名中,必须 声明每个参数的类型。
  • 这是 Rust 设计中一个经过慎重考虑的决定:要求在函数定义中提供类型注解,意味着编译器不需要你在代码的其他地方注明类型来指出你的意图。

当一个函数有多个参数时,使用逗号分隔

  • 创建有两个参数的函数,都是 i32 类型。
  • 函数打印出了这两个参数的值。
  • 注意函数的参数类型并不一定相同,这个例子中只是碰巧相同罢了。
//当一个函数有多个参数时,使用逗号分隔
fn main() {
    another_function(5,6);
}

fn another_function(x: i32,y: i32){

    //x的值为:5
    // y的值为:6
    println!("x的值为:{}",x);
    println!("y的值为:{}",y);

}

包含语句和表达式的函数体

  • 函数体由一系列的语句和一个可选的结尾表达式构成。
  • Rust 是一门基于表达式(expression-based)的语言,这是一个需要理解的(不同于其他语言)重要区别。
  • 语句Statements)是执行一些操作但不返回值的指令。
  • 表达式(Expressions)计算并产生一个值。

示例:使用 let 关键字创建变量并绑定一个值是一个语句

  • let y = 6; 是一个语句。
//包含语句和表达式的函数体
//使用 let 关键字创建变量并绑定一个值是一个语句
fn main() {
    let y = 6;
}

示例:包含一个语句的 main 函数定义

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

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

表达式(大括号(代码块))

  • 表达式会计算出一些值,并且你将编写的大部分 Rust 代码是由表达式组成的。
  • 考虑一个简单的数学运算,比如 5 + 6,这是一个表达式并计算出值 11
  • 表达式可以是语句的一部分,语句 let y = 6; 中的 6 是一个表达式,它计算出的值是 6
  • 函数调用是一个表达式。
  • 宏调用是一个表达式。
  • 我们用来创建新作用域的大括号(代码块),{},也是一个表达式
//表达式(大括号(代码块))
fn main() {
    let x = 5;
    
     let y = {
        let x = 3;
        x + 1
     };

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

具有返回值的函数

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

示例:有返回值的函数

  • five 函数中没有函数调用、宏、甚至没有 let 语句——只有数字 5
  • 这在 Rust 中是一个完全有效的函数。
  • 注意,也指定了函数返回值的类型,就是 -> i32
//具有返回值的函数
fn main(){
    let x = five();

    //x的值为:5
    println!("x的值为:{}",x);
}

fn five() -> i32 {
    5
}
  • five 函数的返回值是 5,所以返回值类型是 i32
  • 这段代码有两个重要的部分:首先,let x = five(); 这一行表明我们使用函数的返回值初始化一个变量。
  • 因为 five 函数返回 5,这一行与如下代码相同:let x = 5;
  • 其次,five 函数没有参数并定义了返回值类型;
  • 不过函数体只有单单一个 5 也没有分号,因为这是一个表达式,我们想要返回它的值。

“mismatched types”(类型不匹配)

fn main(){
    let x = plus_five(5);

    //x的值为:6
    println!("x的值为:{}",x);
}

fn plus_five(x: i32) -> i32 {
    x + 1
    //x + 1;
}
  • 如果在包含 x + 1 的行尾加上一个分号,把它从表达式变成语句,我们将看到一个错误。

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

注释

  • 简单的注释
// hello, world
  • 在 Rust 中,注释必须以两道斜杠开始,并持续到本行的结尾。
  • 对于超过一行的注释,需要在每一行前都加上 //,像这样:
// 对于超过一行的注释,需要在每一行前都加上 `//`,像这样
// 在 Rust 中,注释必须以两道斜杠开始,并持续到本行的结尾。
// 注释也可以在放在包含代码的行的末尾.
  • 注释也可以在放在包含代码的行的末尾:
fn main() {
    let lucky_number = 7; // 注释也可以在放在包含代码的行的末尾
}
  • 不过你更经常看到的是以这种格式使用它们,也就是位于它所解释的代码行的上面一行。
fn main() {
    // 位于它所解释的代码行的上面一行
    let lucky_number = 7;
}

控制流

  • 根据条件是否为真来决定是否执行某些代码,以及根据条件是否为真来重复运行一段代码是大部分编程语言的基本组成部分。
  • Rust 代码中最常见的用来控制执行流的结构是 if 表达式循环

if 表达式

  • if 表达式允许根据条件执行不同的代码分支。
  • 你提供一个条件并表示 “如果条件满足,运行这段代码;如果条件不满足,不运行这段代码。”
//if 表达式
fn main() {
    let number = 3;

    //yes
    if number < 5 {
        println!("yes");
    } else {
        println!("no");
    }
}
  • 所有的 if 表达式都以 if 关键字开头,其后跟一个条件。
  • 在这个例子中,条件检查变量 number 的值是否小于 5。
  • 在条件为真时希望执行的代码块位于紧跟条件之后的大括号中。
  • if 表达式中与条件关联的代码块有时被叫做 arms
  • 也可以包含一个可选的 else 表达式来提供一个在条件为假时应当执行的代码块。
  • 如果不提供 else 表达式并且条件为假时,程序会直接忽略 if 代码块并继续执行下面的代码。
  • 另外值得注意的是代码中的条件 必须bool 值。

代码中的条件必须bool

  • 如果条件不是 bool 值,我们将得到一个错误。
  • 这个错误表明 Rust 期望一个 bool 却得到了一个整数。
  • 不像 Ruby 或 JavaScript 这样的语言,Rust 并不会尝试自动地将非布尔值转换为布尔值。
  • 必须总是显式地使用布尔值作为 if 的条件
fn main() {
    let number = 3;

    if number {
        println!("3?");
    }
}

修改if表达式:如果想要 if 代码块只在一个数字不等于 0 时执行

fn main() {
    let number = 3;

    if number != 0 {

        //不等于 `0` 时执行
        println!("不等于 `0` 时执行");
    }
}

使用 else if 处理多重条件

  • 可以将 else if 表达式与 ifelse 组合来实现多重条件。
  • 当执行这个程序时,它按顺序检查每个 if 表达式并执行第一个条件为真的代码块。
  • 注意即使 6 可以被 2 整除,也不会输出 2,更不会输出 else 块中的 4, 3, or 2
  • 原因是 Rust 只会执行第一个条件为真的代码块,并且一旦它找到一个以后,甚至都不会检查剩下的条件了。
//使用 else if 处理多重条件
fn main() {
    let number = 6;

    //3
    if number % 4 == 0 {
        println!("4");
    } else if number % 3 == 0 {
        println!("3");
    } else if number % 2 == 0 {
        println!("2");
    } else {
        println!("4, 3, or 2");
    }
}
  • 使用过多的 else if 表达式会使代码显得杂乱无章,所以如果有多于一个 else if 表达式,最好重构代码。
  • Rust 还有一个强大的分支结构(branching construct),叫做 match

在 let 语句中使用 if

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

示例:将 if 表达式的返回值赋给一个变量

  • number 变量将会绑定到表示 if 表达式结果的值上。
fn main() {
    let condition = true;
    let number = if condition {
        5
    } else {
        6
    };

    //number值: 5
    println!("number值: {}", number);
}
  • 记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式。
  • 在这个例子中,整个 if 表达式的值取决于哪个代码块被执行。
  • 这意味着 if 的每个分支的可能的返回值都必须是相同类型;
  • 在示例中,if 分支和 else 分支的结果都是 i32 整型。

如果它们的类型不匹配,则会出现一个错误

  • if 代码块中的表达式返回一个整数,而 else 代码块中的表达式返回一个字符串。
  • 这不可行,因为变量必须只有一个类型。
  • Rust 需要在编译时就确切的知道 number 变量的类型,这样它就可以在编译时验证在每处使用的 number 变量的类型是有效的。
  • Rust 并不能够在 number 的类型只能在运行时确定的情况下工作;
  • 这样会使编译器变得更复杂而且只能为代码提供更少的保障,因为它不得不记录所有变量的多种可能的类型。
//如果它们的类型不匹配,则会出现一个错误
fn main() {
    let condition = true;

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

    println!("number值: {}", number);
}

使用循环重复执行

  • 多次执行同一段代码是很常用的,Rust 为此提供了多种 循环loops)。
  • 一个循环执行循环体中的代码直到结尾并紧接着回到开头继续执行
  • Rust 有三种循环:loopwhilefor

使用 loop 重复执行代码

  • loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
  • 当运行这个程序时,我们会看到连续的反复打印 loop!,直到我们手动停止程序。
  • 大部分终端都支持一个快捷键,ctrl-c,来终止一个陷入无限循环的程序。
//使用 `loop` 重复执行代码
fn main() {
    loop {
        println!("loop!");
    }
}

从循环返回

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

    let result = loop {
        counter += 1;

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

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

while条件循环

  • 当条件为真,执行循环。当条件不再为真,调用 break 停止循环。
  • 这个循环类型可以通过组合 loopifelsebreak 来实现。
  • Rust 为此内置了一个语言结构,它被称为 while 循环。

示例:while条件循环

  • 程序循环三次,每次数字都减一。
  • 接着,在循环结束后,打印出另一个信息并退出。
//示例:while条件循环
fn main() {
    let mut number = 3;

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

        number = number - 1;
    }

    //3!
    // 2!
    // 1!
    // LIFTOFF!!!
    println!("LIFTOFF!!!");
}
  • 当条件为真时,使用 while 循环运行代码。
  • 这种结构消除了很多使用 loopifelsebreak 时所必须的嵌套,这样更加清晰。
  • 当条件为真就执行,否则退出循环。

使用 for 遍历集合

如果使用 while 结构来遍历集合中的元素,比如数组,会有什么缺点?

  • 代码对数组中的元素进行计数。
  • 它从索引 0 开始,并接着循环直到遇到数组的最后一个索引(这时,index < 5 不再为真)。
  • 数组中的所有五个元素都如期被打印出来。
  • 尽管 index 在某一时刻会到达值 5,不过循环在其尝试从数组获取第六个值(会越界)之前就停止了。
  • 但这个过程很容易出错,如果索引长度不正确会导致程序 panic。
  • 这也使程序更慢,因为编译器增加了运行时代码来对每次循环的每个元素进行条件检查。
fn main() {
    let a = [10, 20, 30, 40, 50];
    let mut index = 0;

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

        index = index + 1;
    }

    //the value is: 10
// the value is: 20
// the value is: 30
// the value is: 40
// the value is: 50
}

更简洁的替代方案,使用 for 循环来对一个集合执行每个元素

  • 重要的是,我们增强了代码安全性,并消除了可能由于超出数组的结尾或遍历长度不够而缺少一些元素而导致的 bug。
  • 例如,在上例中如果从数组 a 中移除一个元素但忘记将条件更新为 while index < 4,代码将会 panic。
  • 使用 for 循环的话,就不需要惦记着在改变数组元素个数时修改其他的代码了。
  • for 循环的安全性和简洁性使得它成为 Rust 中使用最多的循环结构。
fn main() {
    let a = [10, 20, 30, 40, 50];

    for element in a.iter() {
        println!("the value is: {}", element);
    }
    
//     the value is: 10
// the value is: 20
// the value is: 30
// the value is: 40
// the value is: 50
}

示例:使用 for 循环来倒计时,rev,用来反转 range

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

//     3!
// 2!
// 1!
// LIFTOFF!!!
}

声明:三二一的一的二|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Rust 之旅 - 核心概念


三二一的一的二