通用编程概念
变量和可变性
默认情况下变量是不可变的(immutable),不过你也可以选择让变量是可变的(mutable).
变量的遮蔽
你可以声明和前面变量具有相同名称的新变量,说这个是第一个变量被第二个变量遮蔽(shadow),这意味着当我们使用变量时我们看到的会是第二个变量的值。我们可以通过使用相同的变量名并重复使用 let 关键字来遮蔽变量,如下程序并不会报错:
let x = 5;
let x = x+1;
{
let x = x*3;
}
let x = String::new();
遮蔽和将变量标记为 mut 的方式不同,因为除非我们再次使用 let 关键字,否则若是我们不小心尝试重新赋值给这个变量,我们将得到一个编译错误。通过使用 let,我们可以对一个值进行一些转换,但在这些转换完成后,变量将是不可变的。
mut 和遮蔽之间的另一个区别是,因为我们在再次使用 let 关键字时有效地创建了一个新的变量,所以我们可以改变值的类型,但重复使用相同的名称。
常量
与不可变变量类似,常量(constant)是绑定到一个常量名且不允许更改的值,但是常量和变量之间存在一些差异。
首先,常量不允许使用 mut
。常量不仅仅默认不可变,而且自始至终不可变。常量使用 const
关键字而不是 let
关键字来声明,并且值的类型必须注明。
最后一个不同点是常量只能设置为常量表达式或能在运行时计算得到的值。,而不能是函数调用的结果.
Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词。
将整个程序中用到的硬编码(hardcode)值命名为常量,对于将该值的含义传达给代码的未来维护者很有用。如果将来需要更改硬编码的值,则只需要在代码中改动一处就可以了。
数据类型
Rust 的每个值都有确切的数据类型(data type),该类型告诉 Rust 数据是被指定成哪类数据,从而让 Rust 知道如何使用该数据。
请记住 Rust 是一种静态类型(statically typed)的语言,这意味着它必须在编译期知道所有变量的类型。编译器通常可以根据值和使用方式推导出我们想要使用的类型。
标量类型
标量(scalar)类型表示单个值。Rust 有 4 个基本的标量类型:整型、浮点型、布尔型和字符。
整数
整数(integer)是没有小数部分的数字
举个例子u32 类型。此类型声明表明它关联的值应该是占用 32 位空间的无符号整型(有符号整型以 i 开始,i 是英文单词 integer 的首字母,与之相反的是 u,代表无符号 unsigned 类型)
长度 | 有符号类型 | 无符号类型 |
---|---|---|
8 位 | i8 | u8 |
16 位 | i16 | u16 |
32 位 | i32 | u32 |
64 位 | i64 | u64 |
128 位 | i128 | u128 |
arch | isize | usize |
有符号数字以二进制补码形式存储
此外,isize 和 usize 类型取决于程序运行的计算机体系结构,在表中表示为“arch”:若使用 64 位架构系统则为 64 位,若使用 32 位架构系统则为 32 位。
数字字面量 | 示例 |
---|---|
十进制 | 98_222 |
十六进制 | 0xff |
八进制 | 0o77 |
二进制 | 0b1111_0000 |
字节 (仅限于 u8 ) | b'A' |
整型溢出
比方说有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出(integer overflow),这会导致两种行为的其中一种。当在调试(debug)模式编译时,Rust 会检查整型溢出,若存在这些问题则使程序在编译时 panic.Rust 使用 panic 这个术语来表明程序因错误而退出。
在当使用 --release
参数进行发布(release)模式构建时,Rust 不检测会导致 panic 的整型溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码包裹(two’s complement wrapping)的操作。简而言之,大于该类型最大值的数值会被“包裹”成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。
要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下一系列方法:
- 使用
wrapping_*
方法在所有模式下进行包裹,例如 wrapping_add - 如果使用 checked_* 方法时发生溢出,则返回 None 值
- 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
- 使用 saturating_* 方法使值达到最小值或最大值
浮点型
浮点数(floating-point number)是带有小数点的数字,在 Rust 中浮点类型(简称浮点型)数字也有两种基本类型。Rust 的浮点型是 f32 和 f64,它们的大小分别为 32 位和 64 位。默认浮点类型是 f64,因为在现代的 CPU 中它的速度与 f32 的几乎相同,但精度更高。所有浮点型都是有符号的。f32 类型是单精度浮点型,f64 为双精度浮点型。
数字运算
Rust 的所有数字类型都支持基本数学运算:加法、减法、乘法、除法和取模运算。整数除法会向下取整。
布尔型
和大多数编程语言一样,Rust 中的布尔类型也有两个可能的值:true 和 false。布尔值的大小为 1 个字节.
字符类型
Rust 的 char(字符)类型是该语言最基本的字符类型
注意,我们声明的 char 字面量采用单引号括起来,这与字符串字面量不同,字符串字面量是用双引号括起来。Rust 的字符类型大小为 4 个字节,表示的是一个 Unicode 标量值,这意味着它可以表示的远远不止是 ASCII。标音字母,中文/日文/韩文的文字,emoji,还有零宽空格(zero width space)在 Rust 中都是合法的字符类型。Unicode 值的范围为 U+0000 ~ U+D7FF 和 U+E000~U+10FFFF。不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。
补充一下计算机基础知识
一个字节(byte)由 8 位(bit) 组成。这是计算机科学中标准的定义
位(bit):是计算机中最小的数据单位,它可以表示两种状态之一,通常用 0 或 1 表示
字节(byte):是计算机存储数据的基本单位。一个字节由 8 个位组成,可以表示 2的8次方=256 种不同的值(从 0 到 255)。
1 字节 (Byte) = 8 位 (bit)
1 千字节 (KB) = 1024 字节 (B)
1 兆字节 (MB) = 1024 千字节 (KB)
1 吉字节 (GB) = 1024 兆字节 (MB)
复合类型
复合类型(compound type)可以将多个值组合成一个类型。Rust 有两种基本的复合类型:元组(tuple)和数组(array)。
元组
元组是将多种类型的多个值组合到一个复合类型中的一种基本方式。元组的长度是固定的:声明后,它们就无法增长或缩小。
我们通过在小括号内写入以逗号分隔的值列表来创建一个元组。元组中的每个位置都有一个类型,并且元组中不同值的类型不要求是相同的。
let tup: (i32, f64, u8) = (500, 6.4, 1);
变量 tup 绑定到整个元组,因为元组被认作是单个复合元素。 想从元组中获取个别值,我们可以使用模式匹配来解构(destructure)元组的一个值
let tup = (500, 6.4, 1);
let (x, y, z) = tup;
该程序首先创建一个元组并将其绑定到变量 tup 上。 然后它借助 let 来使用一个模式匹配 tup,并将它分解成三个单独的变量 x、y 和 z。 这过程称为解构(destructuring)
除了通过模式匹配进行解构外,我们还可以使用一个句点(.)连上要访问的值的索引来直接访问元组元素
let x: (i32, f64, u8) = (500, 6.4, 1);
let five_hundred = x.0;
let six_point_four = x.1;
let one = x.2;
该程序创建一个元组 x,然后通过使用它们的索引为每个元素创建新的变量。和大多数编程语言一样,元组中的第一个索引为 0。
没有任何值的元组 () 是一种特殊的类型,只有一个值,也写成 ()。该类型被称为单元类型(unit type),该值被称为单元值(unit value)。如果表达式不返回任何其他值,就隐式地返回单元值。
数组类型
将多个值组合在一起的另一种方式就是使用数组(array)。与元组不同,数组的每个元素必须具有相同的类型。与某些其他语言中的数组不同,Rust 中的数组具有固定长度。
我们在方括号内以逗号分隔的列表形式将值写到数组中:
let a = [1, 2, 3, 4, 5];
// 数组的另一种声明方式 i32 是每个元素的类型。分号之后,数字 5 表明该数组包含 5 个元素。
let a: [i32; 5] = [1, 2, 3, 4, 5];
// 指定初始值,和个数的数组
let a = [3; 5];
访问数组元素
数组是可以在栈上分配的已知固定大小的单个内存块。可以使用索引访问数组的元素
let a = [1, 2, 3, 4, 5];
let first = a[0];
let second = a[1];
在这个例子中,名为 first 的变量将获得值 1,因为它是数组中索引 [0]
处的值。名为 second 的变量将从数组中的索引 [1]
中获取得 2。
当你尝试使用索引访问元素时,Rust 将检查你指定的索引是否小于数组长度。如果索引大于或等于数组长度,Rust 会出现 panic。这种检查必须在运行时进行,尤其是在这种情况下,因为编译器可能无法知道用户之后运行代码时将输入什么值。
函数
Rust 代码中的函数和变量名使用下划线命名法(snake case,直译为蛇形命名法)规范风格。在下划线命名法中,所有字母都是小写并使用下划线分隔单词。
Rust 中的函数定义以 fn 开始,后跟着函数名和一对圆括号。大括号告诉编译器函数体在哪里开始和结束。
可以使用函数名后跟圆括号来调用我们定义过的任意函数。函数的定义位置无关紧要,不用非要像c语言那样一定要在面之前或者在面之前需要一个定义.Rust 不关心函数定义于何处,只要定义了就行。
参数
函数也可以被定义为拥有参数(parameter),参数是特殊变量,是函数签名的一部分。当函数拥有参数(形参)时,可以为这些参数提供具体的值(实参)。技术上讲,这些具体值被称为实参(argument),但是在日常交流中,人们倾向于不区分使用 parameter 和 argument 来表示函数定义中的变量或调用函数时传入的具体值。
在函数签名中,必须声明每个参数的类型。这是一个 Rust 设计中经过慎重考虑的决定:要求在函数定义中提供类型标注,意味着编译器几乎从不需要你在代码的其他地方注明类型来指出你的意图。
语句和表达式
Rust 是一门基于表达式(expression-based)的语言.
语句(statement)是执行一些操作但不返回值的指令。表达式(expression)计算并产生一个值
{
let x = 3;
x + 1
}
这是一个表达式,注意,x + 1
行的末尾没有分号,这与你目前见过的大部分代码行不同。表达式的结尾没有分号。如果在表达式的末尾加上分号,那么它就转换为语句,而语句不会返回值。在接下来探讨函数返回值和表达式时,请记住这一点。
记住,代码块的值是其最后一个表达式的值,而数字本身就是一个表达式
带有返回值的函数
函数可以向调用它的代码返回值。我们并不对返回值命名,但要在箭头(->)
后声明它的类型。在 Rust 中,函数的返回值等同于函数体最后一个表达式的值。使用 return 关键字和指定值,可以从函数中提前返回;但大部分函数隐式返回最后一个表达式。这是一个有返回值函数的例子:
fn five() -> i32 {
5
}
fn main() {
let x = five();
println!("The value of x is: {}", x);
}
在 five 函数中没有函数调用、宏,甚至没有 let 语句——只有数字 5 本身。这在 Rust 中是一个完全有效的函数。注意,函数返回值的类型也被指定好,即 -> i32
。
注释
// 单行注释
/*
多行注释
*/
/// 文档注释
/// 说明函数功能等
/**
* 文档注释
* 用于说明函数功能等
*/
控制流
if表达式
if number < 5 {
println!("condition was true");
} else {
println!("condition was false");
}
if 表达式中与条件关联的代码块有时被叫做分支(arm)和 match 表达式中的分支一样.
另外值得注意的是代码中的条件必须是 bool 值。如果条件不是 bool 值,我们将得到一个错误
else if
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");
}
在let语句中使用if
因为 if 是一个表达式,我们可以在 let 语句的右侧使用它来将结果赋值给一个变量
let condition = true;
let number = if condition { 5 } else { 6 };
使用循环重复执行
loop
loop 关键字告诉 Rust 一遍又一遍地执行一段代码直到你明确要求停止。
loop {
println!("again!");
}
Rust 也提供了一种从代码中跳出循环的方法。可以使用 break 关键字来告诉程序何时停止循环,循环中的 continue 关键字告诉程序跳过这个循环迭代中的任何剩余代码,并转到下一个迭代。
如果存在嵌套循环,break 和 continue 应用于此时最内层的循环。你可以选择在一个循环上指定一个循环标签(loop label),然后将标签与 break 或 continue 一起使用,使这些关键字应用于已标记的循环而不是最内层的循环。下面是一个包含两个嵌套循环的示例:
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);
}
从循环返回
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!!!");
}
使用for遍历集合
fn main() {
let a = [10, 20, 30, 40, 50];
for element in a {
println!("the value is: {}", element);
}
}
fn main() {
for number in (1..4).rev() {
println!("{}!", number);
}
println!("LIFTOFF!!!");
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。