0x03 Common Programming Concepts*
学习变量、基本类型、函数、注释和控制流等基础知识。
Variables and Mutability*
第二章提到,变量默认是不可变的(immutable),下面探讨 Rust 为何及如何鼓励使用不可变性,以及何时选择使用可变变量。
变量不可变时,一旦值被绑定到一个名称上,就不能改变这个值。新建项目 variables。
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
注意 x = 6 不能加 let,不然就变成了提到过的 shadowing。
编译程序,就会报错。
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: make this binding mutable: `mut x`
3 | println!("x = {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
报错原因时不能对不可变变量二次赋值,并给出了修改帮助 mut x。
Rust 编译器保证,如果声明一个值不会变,它就真的不会变。
如果需要使用可变性,就在变量前添加 mut 使其可变。mut 允许把绑定到 x 上的值从 5 改成 6。
除了安全性,还有很多地方需要权衡。使用大型数据结构,可以适当地使用可变变量,会比复制和返回新分配的实例更快。对于较小的数据结构,总是创建新实例,采用偏向函数式的编程风格,可能会使代码更易理解。
变量和常量的区别*
变量默认的不可变性会让人联想到其他语言中的常量(constant)。常量是绑定到一个名称的不允许改变的值,但是常量与变量还是有一些区别。
首先,不允许对常量使用 mut,常量是永远不可变的。
声明常量使用 const,且 必须 注明值的类型。
常量可以在任何作用域中声明,包括全局作用域。
常量只能被设置为常量表达式,而不能是函数调用的结果,或任何其他只能在运行时计算出的值。
const MAX_SIZE: u32 = 100_000;
和 C 一样,Rust 的常量命名规范是使用下划线分割的大写字母。数字中插入下划线能提升可读性。
在声明它的作用域之中,常量在整个程序生命周期中都有效。
隐藏(Shadowing)*
前面也提到,可以复用变量名来隐藏之前的变量。
隐藏与使用 mut 是有区别的,使用 let 可以用这个值进行计算,但计算完后的变量仍是不可变的。
另一个区别是,使用 let 隐藏实际上创建了新变量,可以改变值类型。而对 mut 变量的修改不允许修改类型。
let spaces = " ";
let spaces = spaces.len(); // √
let mut spaces = " ";
spaces = spaces.len(); // ×
Data Types*
Rust 中,每一个值都属于某一个数据类型,据此明确数据处理方式。Rust 有两类数据类型子集:标量(scalar)和复合(compound)。
时刻记住,Rust 是静态类型语言,编译时必须知道所有变量的类型。根据值和使用方式,编译器通常可以推断出想要的类型。在有多种可能类型时,像第二章使用 parse 解析字符串时,就需要添加类型注解 : u32。不添加类型注解,Rust 就会报错。
标量类型*
标量(scalar)类型代表一个单独的值。Rust 有四种基本的标量类型:整型、浮点型、布尔类型和字符类型。
整型*
整数,分无符号和有符号,有符号数以补码形式(two's complement representation)存储。
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
Rust 中的整型字面值有 Decimal, Hex/0x, Octal/0o, Binary/0b, Byte/b'A'(仅限 u8),除了 byte 以外的字面值都允许使用类型后缀,如 57u8,同时允许使用 _ 作为分隔符方便读数,如 1_000。
Rust 默认使用 i32,通常是最快的。isize 和 usize 通常作为某些集合的索引。
对于常见的整型溢出问题,在 debug 模式下构建,Rust 会让这类问题 panic。而在 release 构建中,Rust 不检测溢出,而是直接使用二进制补码包装(two's complement wrapping)操作,即对于 u8,256 变成 0,257 变成 1。标准库中有一个 Wrapping 类型显示提供此功能。
在新版的官方教程中 book/ch03-02-data-types.md at main · rust-lang/book (github.com) 提到可以使用标准库提供给数字类型的一些方法:
- 对溢出进行 wrapping 操作的
wrapping_*方法,如wrapping_add;- 溢出时返回
None的check_*方法;- 返回是否溢出的布尔值的
overflowing_*方法;- 限制到最大值或最小值的
saturating_*方法。
浮点型*
Rust 有两个原生的浮点数,f32 和 f64,默认是 f64,速度几乎一样,但精度更高。使用 IEEE-754 标准表示。
数字运算*
Rust 所有数据类型都支持基本数学运算,+-*/%。
布尔型*
和 C++ 一样,bool: [true/false]。
字符类型*
Rust 的 char 类型用单引号指定,字符串用双引号。
Rust 里的 char 类型大小为四个字节,代表一个 Unicode 值。而字符并不是 Unicode 中的一个真正的概念,因此对字符的直觉可能和 Rust 中的 char 不相符。在第八章会讨论使用字符串存储 UTF-8 编码的文本。
复合类型*
由多个值组合成一个类型。Rust 有两个原生的复合类型:元组(tuple)和数组(array)。
元组类型*
元组是一个将多个其他类型的值组合进一个复合类型的主要方式。元组长度固定:一旦声明,长度就不会变化。
使用圆括号中逗号分隔的值列表来创建元组,可以为这些值添加类型注解。
可以使用模式匹配解构元组值来获取单个值。
还可以使用 . 后跟值的索引直接访问它们。
fn main() {
let tup = (1, 2, 3);
let (x, y, z) = tup;
println!("x = {}, y = {}, z = {}", x, y, z);
println!("tup .0 = {}, .1 = {}, .2 = {}", tup.0, tup.1, tup.2);
}
// x = 1, y = 2, z = 3
// tup .0 = 1, .1 = 2, .2 = 3
数组类型*
和 C 一样,数组中的每个元素类型必须相同。Rust 中的数组是固定长度的:一旦声明,它们的长度不能增长或缩小。
Rust 中,数组的值位于中括号内的逗号分隔列表中。
当想要在栈而不是堆上为数据分配空间,或想要确保总是有固定数量的元素时,数组很有用。但数组并不如 vector 类型灵活。vector 类型是标准库提供的一个允许增长和缩小长度的类似数组的集合类型。第八章会详细讨论 vector。
可以用 : [i32; 5] 的形式给数组添加类型注解,表示数组有 5 个 i32 类型数据。
如果想把数组值都用相同值初始化,可以使用 [0; 5] 的形式,表示创建 5 个 0 组成的数组。
和 C 一样,使用方括号索引的方式访问数组元素。
越界访问数组在编译时不会产生任何错误,但在运行时会 panic。Rust 会在通过索引访问元素时做越界检查。第九章会详细讨论 Rust 的错误处理。
How Funcitons Work*
前面已经介绍了 main 函数,fn 关键词用于声明新函数。
Rust 中的函数和变量名使用 snake case 规范,所有字母小写并使用下划线分隔单词。
Rust 不关心函数定义在何处(是指在同一文件中相对调用它位置之前或之后)。
函数参数*
函数可以带参数,参数是一些变量,作为函数签名的一部分。当函数定义中有形参(parameters)时,可以为这些参数提供具体值,即实参。实参一般称为 arguments,不过日常并不区分 parameters 和 arguments。
函数签名中,必须声明每个参数的类型,即必须在函数定义中提供类型注解。多个参数用逗号分隔。
函数体:语句和表达式*
函数体由一系列语句和一个可选的结尾表达式构成。
Rust 是基于表达式的语言,这是相对于其他语言的重要区别。
语句(Statement)是执行一些操作但不返回值的指令。表达式(Expressions)计算并产生一个值。
使用 let 创建变量并绑定值是一个语句,函数定义也是一个语句。
语句不返回值,即不能把 let 语句赋值给另一个变量。
而在 C 语言中,赋值语句会返回所赋得值,也就是可以写成 x = y = 6,而 Rust 中不能这样写。
表达式会计算出一些值,大多数 Rust 代码是由表达式组成的。函数调用是一个表达式,宏调用也是一个表达式,用来创建新作用域得大括号也是一个表达式。
fn main() {
let x = 5;
let y = {
let x = 3;
x + 1
};
println!("y = {}", y);
}
// y = 4
上面的代码块是一个表达式,值为 4,注意 x + 1 的结尾没有分号。如果表达式的结尾加上分号,就变成了语句,也就没有了返回值。
具有返回值的函数*
函数可以向调用它的代码返回值,函数不对返回值命名,但要用箭头声明它的类型。
在 Rust 中,函数的返回值等于函数体最后一个表达式的值。使用 return 关键字可以在函数中提前返回。但大部分函数都隐式地返回最后的表达式。
如果不小心在返回在结尾表达式后面加了分号,会报错类型不匹配(mismatch types)。使用空元组 () 表示不返回值。
Comments*
Rust 常用的注释就是 //,行内注释。
似乎也支持 /**/ 多行注释。
Control Flow*
Rust 最常见的用来控制执行流的结构是 if 表达式和循环。
if 表达式*
if 表达式允许根据条件执行不同的代码分支。
if 表达式都以 if 关键字开头,后跟一个条件。与条件关联的代码块也被叫做分支(arms),与第二章提到的 match 语句的分支一样。条件为真就执行紧跟着条件之后的分支,也可以用可选的 else 表达式提供一个条件为假时执行的代码块,如果不提供 else 则忽略 if 代码块。
需要注意的是,条件必须是 bool 值,否则会报错。与 C 不一样,Rust 不能支持自动类型转换。
可以使用 else if 表达式处理多重条件,不过太多的条件还是使用 match 语句比较方便。
if 是表达式,可以在 let 语句右侧使用它,Rust 中似乎没有 ?: 这种三目运算符。在这种使用场景下,if 的每个分支的可能的返回值必须是相同类型,否则编译会报错(报错是类型不相同,不过测试了一下应该是类型不同就不行)。这还是因为 Rust 必须在编译时确切知道变量的类型。
使用循环重复执行*
Rust 提供了 loop、while 和 for 三种循环。
loop 是无限循环,可以使用 break 关键字停止。break 表达式可以返回值(测试了一下 break 表达式后是否加分号都可)。
while 是条件循环,当条件为真时执行循环,为假时会调用 break 停止循环(似乎不能在停止循环的同时返回值)。
for 用于遍历集合,安全且简洁。
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a.iter() {
println!("the value is: {}", element);
}
}