通用编程概念
本文大部分内容翻译自:The Rust Programming Language
本文涵盖了几乎每种编程语言中都会出现的概念,以及它们在Rust中的工作原理。
许多编程语言的核心有很多共同点,本章中介绍的概念都不是Rust独有的,但我们将在Rust的背景下讨论它们,并解释使用这些概念的惯例。
1. 变量和可变性(Variables and Mutability)
默认情况下,变量是不可变的。
这是Rust可以提供安全、简单的并发性来编写代码的原因之一,但我们仍然可以选择使变量可变。
下面会介绍Rust如何以及为什么鼓励你支持不变性(immutability),以及为什么有时你可能不去使用这个特性。
1.1. 变量的不变性
当变量不可变时,一旦它被绑定了一个值,该值就再也不能被更改。
创建variables
工程:
1 | cargo new variables |
然后将src/main.rs
改为如下内容:
1 | fn main() { |
然后执行:
1 | cargo run |
执行结果:
错误消息表明,导致错误的原因是“cannot assign twice to immutable variable”,即不能为不可变变量x
赋值两次,这是因为程序中试图为不可变的x
变量分配第二个值。
在我们试图更改被指定为不可变的值时,编译时报错(compile-time errors)是很重要的,因为这种情况可能会导致错误。
如果我们代码的一部分假设一个值永远不会改变,而另一部分却会改变该值,那么代码的第一部分可能不会得到它期望的结果。这种错误的原因在事后可能很难追踪,特别是当第二段代码只是偶尔更改这个值的时候。Rust编译器保证,当我们声明一个值不会改变,它就真的不会改变,我们不需要自己去跟踪它,代码也更容易推理。
1.2. 将变量变为可变
但可变性也非常有用。变量在默认情况下是不可变的,但可以通过在变量名前面添加mut
来使其可变。添加mut
向阅读代码的人传达了一个信息:代码的其他部分可能会修改这个变量的值。
将上面代码改为如下内容:
1 | fn main() { |
运行结果:
1.3. 常量(Constants)
与不可变变量一样,常量是绑定到名称且不允许更改的值,但常量和变量之间有一些差异:
- 对常量不能使用
mut
。默认情况下,常量不仅不可变,而且总是不可变的。用const
关键字而不是let
关键字声明常量,并且必须标注类型 - 常量可以在任何作用域中声明,包括全局作用域,这样程序的各个部分都可以使用它们
- 常量只能被赋值为常量表达式,不能被赋值为那些只能在运行时计算出结果的值
常量声明举例:
1 | const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; |
常量的名称是THREE_HOURS_IN_SECONDS
,它的值被设为60 * 60 * 3
。Rust对常量的命名管理是全部使用大写字母,单词之间用下划线_
连接。
编译器能够在编译时评估一组有限的操作,让我们能够选择以更容易理解和验证的方式写出此值,而不是将该常量设置为值10,800
。
常量在程序运行期间,在声明范围内有效。将程序中使用的硬编码值命名为常量,有利于将该值的含义传达给代码的未来维护者。并且,如果将来需要更新硬编码值,代码中只有一个位置需要更改。
1.4. Shadowing
Rust允许声明一个与上一个变量同名的新变量。第一个变量被第二个变量屏蔽(shadow),这意味着编译器在使用变量名称时,看到的是第二个变量。实际上,第二个变量覆盖了第一个变量,任何使用该变量名的操作都会作用于第二个变量,直到它自己也被屏蔽,或者离开作用域。我们可以通过使用同一变量的名称并重复使用let
关键字来屏蔽变量,如下所示:
1 | fn main() { |
这段程序首先将x
绑定到5。然后通过重复let x =
创建一个新的变量x
,取原始值并加1,这样x
的值就是6。然后,在用花括号创建的内部作用域中,第三个let
语句也会屏蔽x
并创建一个新变量,将上一个值乘以2,使x
的值为12。当离开作用域时,内部屏蔽结束,x
返回为6。
运行结果:
Shadowing与mut的不同之处:
屏蔽与将变量标记为
mut
不同,因为如果我们不小心在不使用let
关键字的情况下重新给这个变量赋值,我们将收到编译错误(compile-time error)。通过使用let
,我们可以变量做一些转换,但在这些转换完成后,变量依旧是不可变的(immutable)mut
不能更改变量类型。当我们再次使用let
关键字时,我们正在创建一个新变量,可以更改变量的类型,但重用相同的名称。但mut
不允许更改变量类型。例如:1
2
3
4let spaces = "abc";
println!("{}", spaces);
let spaces = spaces.len(); // 允许
println!("{}", spaces);运行结果:
1
2let mut spaces = " ";
spaces = spaces.len(); // 不允许程序报错:
2. 数据类型(Data Types)
Rust中的每个值都有其所属类型,这样Rust才知道如何处理这些数据。
Rust是一种静态类型语言,这意味着它必须在编译时知道所有变量的类型。编译器通常可以根据变量被赋予的值和变量被使用的方式来推断数据的类型。在类型有多种可能的情况下,必须加上类型注释(type annotation),如:
1 | let guess: u32 = "42".parse().expect("Not a number!"); |
如果不使用: u32
类型注释,Rust就会显示下面的错误,意思是编译器需要更多的信息才能知道我们想要使用哪种类型:
2.1. 标量类型(Scalar Types)
标量类型表示单个值。Rust有四种主要标量类型:整数、浮点数、布尔值和字符。
2.1.1. 整数类型(Integer Types)
整数是没有小数部分的数字。例如u32
类型,表示与之关联的值应该是占用32位空间的无符号整数(有符号整数类型以i
开头,而不是u
)。下表显示了Rust中的内置整数类型,我们可以使用其中任何一个来声明整数值的类型。
Length | Signed | Decimal Range | Unsigned | Decimal Range |
---|---|---|---|---|
8-bit | i8 | $[-2^{7},2^{7}-1]$=[-128,127] | u8 | [0,255] |
16-bit | i16 | $[-2^{15},2^{15}-1]$=[-65536,65535] | u16 | [0,$2^{16}-1$] |
32-bit | i32 | $[-2^{31},2^{31}-1]$ | u32 | [0,$2^{32}-1$] |
64-bit | i64 | [$-2^{63}, 2^{63} - 1$] | u64 | [0,$2^{64}-1$] |
128-bit | i128 | [$-2^{127},2^{127} - 1$] | u128 | [0,$2^{128}-1$] |
arch | isize | usize |
每个成员都可以有符号或无符号,并具有显式大小。有符号和无符号是指数字是否可能是负数。有符号的数字使用二进制补码(two’s complement)来存储。
二进制补码(two’s complement):
正数使用原码,负数用其相反数的二进制取反加1表示。
例如,若要用二进制补码表示-3
:
- 求其相反数 =>
3
- 求上面结果的二进制表示 =>
0011
- 将上面结果取反 =>
1100
- 将上面结果加1 =>
1101
那么1101
就是-3
的二进制补码(two’s complement)
每个有符号变量可以存储从$[-2^{n-1},2^{n-1}-1]$的数字,其中$n$是变量的位数;无符号成员可以存储$[0,2^{n}-1]$的数字。
此外,isize
和usize
类型取决于正在运行本程序的计算机的架构,该架构在表中表示为“arch”:如果是64位架构,则为64位;如果使用的是32位架构,则为32位。
可以使用下表的任何形式表示整数。当数字有可能是多种数字类型时,允许用类型后缀来指定类型,如57u8
。还可以使用_
作为可视化分隔符,使数字更易于阅读,例如1_000
,其值与1000
相同。
Number literals | Example |
---|---|
Decimal | 98_222 |
Hex | 0xff |
Octal | 0o77 |
Binary | 0b1111_0000 |
Byte (u8 only) |
b'A' |
Rust默认使用的整数类型是i32
。使用isize
或usize
的主要用于对集合进行索引。
2.1.2. 浮点类型(Floating-Point Types)
Rust还有两种浮点数的原始类型,即具有小数点的数字。
Rust的浮点类型是f32
(单精度)和f64
(双精度),大小分别为32位和64位。默认类型是f64
,因为在现代CPU上,它的速度与f32
大致相同,但能够提高精度。所有浮点类型都是有符号的。
举例:
1 | fn main() { |
Rust支持所有数字类型的基本数学运算:加法、减法、乘法、除法和余数。
整数除法四舍五入到最近的整数。
举例:
1 | fn main() { |
2.1.3. 布尔类型(Boolean Type)
与大多数其他编程语言一样,Rust中的布尔类型有两个可能的值:true
和false
。
布尔值占一个字节,使用bool
来指定。
举例:
1 | fn main() { |
主要在条件语句中使用布尔值,例如if表达式。
2.1.4. 字符类型(Character Type)
Rust的字符类型是最原始的字母类型。
举例:
1 | fn main() { |
用单引号'
指定字符类型(char
)。Rust的字符类型大小为4个字节,表示Unicode标量值,这意味着它可以表示的不仅仅是ASCII。重音字母、中文、日语和韩语字符、表情符号和零宽度空格都是Rust中有效的字符值。Unicode标量值从U+0000
到U+D7FF
和U+E000
到U+10FFFF
。
2.2. 复合类型(Compound Types)
复合类型可以将多个值组合成一种类型。Rust有两种原始复合类型:元组(Tuple)和数组(Array)。
2.2.1. 元组类型(Tuple Type)
元组是一种将具有各种类型的多个值组合成一种复合类型的通用方法。元组具有固定的长度,一旦声明,它们的大小就无法改变。
创建元组的方法:在括号()
内写一系列以逗号分隔的值。
元组中的每个元素都有一个类型,不同元素的类型不一定相同。
举例:
1 | fn main() { |
tup
变量与整个元组绑定,想要得到元组中元素的值,可以用模式匹配的方法。例如:
1 | fn main() { |
这段程序首先创建一个元组,并将其绑定到变量tup
。然后使用一个let模式
获取tup
,将其转换为三个独立的变量,x
、y
和z
。这种方法被称为解构(destructing),因为它将单个元组分为三部分。最后,程序打印出y
的值,即6.4
。
我们也可以使用句点.
加我们要访问的值的索引来访问元组元素。例如:
1 | fn main() { |
不含任何值的元组有一个特殊名称,unit
。此值及其相应类型都写作()
,表示空值或空返回类型。如果表达式不返回任何其他值,则隐式返回unit值。
2.2.2. 数组类型(Array Type)
获得一个拥有多个值的集合的另一个方法是使用数组(Array)。与元组不同,数组的每个元素都必须具有相同的类型。与其他语言中的数组不同,Rust中的数组具有固定的长度。
2.2.2.1. 声明数组
我们将数组中的值写成方括号[]
内以逗号分隔的列表:
1 | fn main() { |
当希望将数据分配到栈(stack)而不是堆(heap)上时,或者想确保我们始终拥有固定数量的元素时,数组非常有用。然而,数组不像向量类型(vector)那么灵活。向量是标准库提供的类似集合类型,它的大小可以增长或缩小。如果不确定是使用数组还是向量,很可能应该使用向量。
然而,当元素数量不需要更改时,数组会更有用。例如,如果想要在程序中使用月份的名称,可能会使用数组而不是向量,因为我们知道它总是包含12个元素:
1 | let months = ["January", "February", "March", "April", "May", "June", "July", |
数组的类型表示:
用方括号来声明数组的类型,包括每个元素的类型,分号;
,数组中的元素数量:
1 | let a: [i32; 5] = [1, 2, 3, 4, 5]; |
这里,i32
是每个元素的类型。在分号之后,数字5
表示数组包含5个元素。
还可以通过初始值; 数组长度
来初始化数组,这样数组中的每个元素都有相同的值:
1 | let a = [3; 5]; |
名为a
的数组包含5个元素,这些元素初始值被设为3。这与let a = [3, 3, 3, 3, 3]
一样,但更简洁。
2.2.2.2. 访问数组中的元素
数组是已知的、大小固定的、可在栈上分配的单个内存块。可以使用索引来访问数组的元素:
1 | fn main() { |
变量first
将被赋值为1
,因为这是数组中索引[0]
的值。变量second
将被数组的索引[1]
赋值为2
。
2.2.2.3. 越界访问
越界访问(访问的索引值大于等于数组长度)会造成panic:
1 | fn main() { |
cargo build
结果:
3. 函数(Functions)
函数(Functions)在Rust代码中很普遍。Rust中最重要的函数之一:主函数(main
function)。这是许多程序的切入点。
用fn
关键字来声明新函数,后面跟着函数名和一组花括号{}
。花括号是用来告诉编译器,函数的主体开始或结束了。
Rust使用Snake case作为函数和变量名的惯例样式,所有字母都是小写字母,用下划线_
将单词分开:
1 | fn main() { |
another_function()
可以定义在main()
之后,也可以定义在maint()
之前,都可以被main()
调用。
3.1. Parameters
带参数的函数示例:
1 | fn main() { |
another_function
有一个参数x
,类型为i32
。
在函数签名中,必须声明每个参数的类型。
多个参数示例:
1 | fn main() { |
3.2. Statements和Expressions
3.2.1. Statements
Statements没有返回值。例如:
1 | fn main() { |
创建变量并给它赋值是statement,函数定义是statement,上面的整段代码都是statements。
由于statement不返回任何值,所以不能将一个statement赋值给一个变量,如:
1 | fn main() { |
这段代码会有编译错误,因为它将一个let
statement赋值给了变量x
。
3.2.2. Expressions
表达式(Expressions)可以计算出一个值,并且末尾没有分号;
。例如5 + 6
,这是一个计算结果为值11的表达式。
expression可以是statement的一部分,比如let y = 6;
中,6
是expression。函数调用是expression,调用宏(macro)是expression,使用花括号创建的新的代码块是expression。
举例:
1 | fn main() { |
这个expression:
1 | { |
是一个计算结果为4
的代码块,这个结果会被绑定到变量y
,成为let
statement的一部分。
expressions的末尾不能加分号;
,否则就会被转换为statement,就不能返回值了。
3.3. 有返回值的函数
返回值的类型必须声明在->
之后。可以使用return
关键字并指定返回值来从函数返回,但大多数函数都隐式返回最后一个表达式。
例1:
1 | fn five() -> i32 { |
例2:
1 | fn main() { |
如果x + 1
后面加了分号,变成x + 1;
,会出现“mismatched types”错误。因为函数plus_one
的定义说,它将返回一个类型为i32
的值,但statement并不会计算出一个值,而是由单位类型(unit type)()
表示。因此,plus_one
没有返回任何东西,这与它的定义相矛盾,因此导致错误。
4. 注释(Comments)
举例:
1 | // So we’re doing something complicated here, long enough that we need |
5. 控制流(Control Flow)
5.1. if表达式
5.1.1. if
举例:
1 | fn main() { |
if
后跟的条件必须是bool
类型,否则程序会报错。
5.1.2. else if
举例:
1 | fn main() { |
5.1.3. 在let
语句中使用if
举例:
1 | fn main() { |
注意,这里if
和else
后面的值必须是同一种类型,否则会报错。如:
1 | fn main() { |
if
和else
后面的值不兼容,会出现编译错误。
5.2. loop
5.2.1. 无限循环
举例:
1 | fn main() { |
跳出循环的方法:break
关键词。
5.2.2. 返回结果
还可以使用loop来返回结果:
1 | fn main() { |
result
变量用来保存loop
返回的结果,上面例子中,返回的是counter * 2
,即20。
5.2.3. loop labels:跳出label
指定的loop
举例:
1 | fn main() { |
默认情况下,break
和continue
作用于他们所在的loop。loop label以单引号'
开始,使用loop label后,可用于跳出label指定的loop。
5.3. while
举例:
1 | fn main() { |
5.4. for
举例:
1 | fn main() { |
使用Range
:
1 | fn main() { |
rev()
用于reverse the range。
参考资料
[1] Common Programming Concepts:https://doc.rust-lang.org/stable/book/ch03-00-common-programming-concepts.html