以下内容不是原创,仅作为个人学习笔记使用。
变量绑定与解构
变量绑定
在 Rust 中,我们这样写: let a = "hello world" ,同时给这个过程起了另一个名字:变量绑定。
为何不用赋值而用绑定呢(其实你也可以称之为赋值,但是绑定的含义更清晰准确)?这里就涉及 Rust 最核心的原则——所有权,简单来讲,任何内存对象都是有主人的,而且一般情况下完全属于它的主人,绑定就是把这个对象绑定给一个变量,让这个变量成为它的主人。
可变性
Rust 的变量在默认情况下是不可变的。如果想要使变量可变,就需要使用mut关键字让变量变为可变的。
例如:
fn main() {
let x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
保存文件,再使用 cargo run 运行它,迎面而来的是一条错误提示:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
2 | let x = 5;
| -
| |
| first assignment to `x`
| help: consider making this binding mutable: `mut x`
3 | println!("The value of x is: {}", x);
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
error: aborting due to previous error
具体的错误原因是 cannot assign twice to immutable variable x(无法对不可变的变量进行重复赋值),因为我们想为不可变的 x 变量再次赋值。
这种错误是为了避免无法预期的错误发生在我们的变量上:一个变量往往被多处代码所使用,其中一部分代码假定该变量的值永远不会改变,而另外一部分代码却无情的改变了这个值,在实际开发过程中,这个错误是很难被发现的,特别是在多线程编程中。
为了让变量声明为可变,将 src/main.rs 改为以下内容:
fn main() {
let mut x = 5;
println!("The value of x is: {}", x);
x = 6;
println!("The value of x is: {}", x);
}
运行程序将得到下面结果:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
Finished dev [unoptimized + debuginfo] target(s) in 0.30s
Running `target/debug/variables`
The value of x is: 5
The value of x is: 6
选择可变还是不可变,更多的还是取决于你的使用场景,例如不可变可以带来安全性,但是丧失了灵活性和性能(如果你要改变,就要重新创建一个新的变量,这里涉及到内存对象的再分配)。而可变变量最大的好处就是使用上的灵活性和性能上的提升。
使用下划线开头忽略未使用的变量
如果希望告诉 Rust 不要警告未使用的变量,为此可以用下划线作为变量名的开头:
fn main() {
let _x = 5;
let y = 10;
}
使用 cargo run 运行下试试:
warning: unused variable: `y`
--> src/main.rs:3:9
|
3 | let y = 10;
| ^ help: 如果 y 故意不被使用,请添加一个下划线前缀: `_y`
|
= note: `#[warn(unused_variables)]` on by default
可以看到,两个变量都是只有声明,没有使用,但是编译器却独独给出了 y 未被使用的警告,充分说明了 _ 变量名前缀在这里发挥的作用。
值得注意的是,这里编译器还很善意的给出了提示( Rust 的编译器非常强大,这里的提示只是小意思 ): 将 y 修改 _y 即可。这里就不再给出代码,留给大家手动尝试并观察下运行结果。
变量解构
例如:
fn main() {
let (a, mut b): (bool,bool) = (true, false);
// a = true,不可变; b = false,可变
println!("a = {:?}, b = {:?}", a, b);
b = true;
assert_eq!(a, b);
}
暂时没看懂,详细的看后面吧。
变量与常量
- 常量不允许使用 mut。常量不仅仅默认不可变,而且自始至终不可变,因为常量在编译完成后,已经确定它的值。
- 常量使用 const 关键字而不是 let 关键字来声明,并且值的类型必须标注。
下面是一个常量声明的例子,其常量名为 MAX_POINTS,值设置为 100,000。(Rust 常量的命名约定是全部字母都使用大写,并使用下划线分隔单词,另外对数字字面量可插入下划线以提高可读性):
const MAX_POINTS: u32 = 100_000;
常量可以在任意作用域内声明,包括全局作用域,在声明的作用域内,常量在程序运行的整个过程中都有效。对于需要在多处代码共享一个不可变的值时非常有用,例如游戏中允许玩家赚取的最大点数或光速。
变量遮蔽(shadowing)
Rust 允许声明相同的变量名,在后面声明的变量会遮蔽掉前面声明的,如下所示:
fn main() {
let x = 5;
// 在main函数的作用域内对之前的x进行遮蔽
let x = x + 1;
{
// 在当前的花括号作用域内,对之前的x进行遮蔽
let x = x * 2;
println!("The value of x in the inner scope is: {}", x);
}
println!("The value of x is: {}", x);
}
当运行此程序,将输出以下内容:
$ cargo run
Compiling variables v0.1.0 (file:///projects/variables)
...
The value of x in the inner scope is: 12
The value of x is: 6
变量遮蔽的用处在于,如果你在某个作用域内无需再使用之前的变量(在被遮蔽后,无法再访问到之前的同名变量),就可以重复的使用变量名字,而不用绞尽脑汁去想更多的名字。
基本类型
整型溢出
假设有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)。
在当使用 --release 参数进行 release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two’s complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。
要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
- 使用 wrapping_* 方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add
- 如果使用 checked_* 方法时发生溢出,则返回 None 值
- 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
- 使用 saturating_* 方法使值达到最小值或最大值
下面是一个演示wrapping_*方法的示例:
fn main() {
let a : u8 = 255;
let b = a.wrapping_add(20);
println!("{}", b); // 19
}
浮点数陷阱
- 浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因:
浮点数往往是你想要数的近似表达浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如
0.1在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行 - 浮点数在某些特性上是反直觉的 例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用 >,>=
等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为 f32 , f64 上的比较运算实现的是
std::cmp::PartialEq 特征(类似其他语言的接口),但是并没有实现 std::cmp::Eq
特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例:
为了避免上面说的两个陷阱,你需要遵守以下准则:
- 避免在浮点数上测试相等性
- 当结果在数学上可能存在未定义时,需要格外的小心
来看个小例子:
fn main() {
// 断言0.1 + 0.2与0.3相等
assert!(0.1 + 0.2 == 0.3);
}
你可能以为,这段代码没啥问题吧,实际上它会 panic(程序崩溃,抛出异常),因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。
讲到这里,相信大家基本已经明白了,为什么操作浮点数时要格外的小心,但是还不够,下面再来一段代码,直接震撼你的灵魂:
fn main() {
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);
println!("abc (f32)");
println!(" 0.1 + 0.2: {:x}", (abc.0 + abc.1).to_bits());
println!(" 0.3: {:x}", (abc.2).to_bits());
println!();
println!("xyz (f64)");
println!(" 0.1 + 0.2: {:x}", (xyz.0 + xyz.1).to_bits());
println!(" 0.3: {:x}", (xyz.2).to_bits());
println!();
assert!(abc.0 + abc.1 == abc.2);
assert!(xyz.0 + xyz.1 == xyz.2);
}
运行该程序,输出如下:
abc (f32)
0.1 + 0.2: 3e99999a
0.3: 3e99999a
xyz (f64)
0.1 + 0.2: 3fd3333333333334
0.3: 3fd3333333333333
thread 'main' panicked at 'assertion failed: xyz.0 + xyz.1 == xyz.2',
➥ch2-add-floats.rs.rs:14:5
note: run with `RUST_BACKTRACE=1` environment variable to display
➥a backtrace
仔细看,对 f32 类型做加法时,0.1 + 0.2 的结果是 3e99999a,0.3 也是 3e99999a,因此 f32 下的 0.1 + 0.2 == 0.3 通过测试,但是到了 f64 类型时,结果就不一样了,因为 f64 精度高很多,因此在小数点非常后面发生了一点微小的变化,0.1 + 0.2 以 4 结尾,但是 0.3 以3结尾,这个细微区别导致 f64 下的测试失败了,并且抛出了异常。
位运算
主要讲一下!
fn main() {
// 二进制为00000010
let a:i32 = 2;
// 二进制为00000011
let b:i32 = 3;
println!("(a & b) value is {}", a & b);
println!("(a | b) value is {}", a | b);
println!("(a ^ b) value is {}", a ^ b);
println!("(!b) value is {} ", !b);
println!("(a << b) value is {}", a << b);
println!("(a >> b) value is {}", a >> b);
let mut a = a;
// 注意这些计算符除了!之外都可以加上=进行赋值 (因为!=要用来判断不等于)
a <<= b;
println!("(a << b) value is {}", a);
}
其结果为:
(a & b) value is 2
(a | b) value is 3
(a ^ b) value is 1
(!b) value is -4
(a << b) value is 16
(a >> b) value is 0
(a << b) value is 16
在 Rust 中,位运算是在补码表示的整数上执行的。当你对 !b 进行按位取反时,实际上是将二进制表示中的每一位取反,0 变为 1,1 变为 0。
在这里,b 的二进制表示是 00000011,当你对其进行按位取反时,得到 11111100。这个二进制数在补码表示法中代表一个负数。
在补码表示法中,负数的表示采用了二进制的补码形式。具体而言,对于一个 32 位整数,其最高位(最左边的位)是符号位,0 表示正数,1 表示负数。剩下的 31 位表示数值部分。当符号位为 1 时,可以使用补码运算来计算实际的数值。
对于 11111100,其符号位是 1,表示负数。接下来,我们计算其补码:
- 取反(按位取反): 00000011
- 加一: 00000011 + 1 = 00000100
所以,11111100 的补码是 00000100。在十进制中,这个补码对应的是 -4。因此,!b 的值是 -4。
序列(Range)
Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5,生成从 1 到 4 的连续数字,不包含 5 ;1..=5,生成从 1 到 5 的连续数字,包含 5,它的用途很简单,常常用于循环中:
for i in 1..=5 {
println!("{}",i);
}
最终程序输出:
1
2
3
4
5
序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。
字符类型(char)
下面的代码展示了几个颇具异域风情的字符:
fn main() {
let c = 'z';
let z = 'ℤ';
let g = '国';
let heart_eyed_cat = '😻';
}
如果大家是从有年代感的编程语言过来,可能会大喊一声:这 XX 叫字符?是的,在 Rust 语言中这些都是字符,Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。Unicode 值的范围从 U+0000 ~ U+D7FF 和 U+E000 ~ U+10FFFF。不过“字符”并不是 Unicode 中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。
注意,我们还没开始讲字符串,但是这里提前说一下,和一些语言不同,Rust 的字符只能用 '' 来表示, "" 是留给字符串的。
单元类型
单元类型就是 () ,对,你没看错,就是 () ,唯一的值也是 () ,一些读者读到这里可能就不愿意了,你也太敷衍了吧,管这叫类型?
只能说,再不起眼的东西,都有其用途,在目前为止的学习过程中,大家已经看到过很多次 fn main() 函数的使用吧?那么这个函数返回什么呢?
没错, main 函数就返回这个单元类型 (),你不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function ),顾名思义,无法收敛的函数。
例如常见的 println!() 的返回值也是单元类型 ()。
再比如,你可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key。 这种用法和 Go 语言的 struct{} 类似,可以作为一个值用来占位,但是完全不占用任何内存。
语句和表达式
Rust 的函数体是由一系列语句组成,最后由一个表达式来返回值,例如:
fn add_with_extra(x: i32, y: i32) -> i32 {
let x = x + 1; // 语句
let y = y + 5; // 语句
x + y // 表达式
}
语句会执行一些操作但是不会返回一个值,而表达式会在求值后返回一个值,因此在上述函数体的三行代码中,前两行是语句,最后一行是表达式。
对于 Rust 语言而言,这种基于语句(statement)和表达式(expression)的方式是非常重要的,你需要能明确的区分这两个概念, 但是对于很多其它语言而言,这两个往往无需区分。基于表达式是函数式语言的重要特征,表达式总要返回值。
其实,在此之前,我们已经多次使用过语句和表达式。
表达式会进行求值,然后返回一个值。例如 5 + 6,在求值后,返回值 11,因此它就是一条表达式。
表达式可以成为语句的一部分,例如 let y = 6 中,6 就是一个表达式,它在求值后返回一个值 6(有些反直觉,但是确实是表达式)。
调用一个函数是表达式,因为会返回一个值,调用宏也是表达式,用花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
}
该语句块是表达式的原因是:它的最后一行是表达式,返回了 x + 1 的值,注意 x + 1 不能以分号结尾,否则就会从表达式变成语句, 表达式不能包含分号。这一点非常重要,一旦你在表达式后加上分号,它就会变成一条语句,再也不会返回一个值,请牢记!
最后,表达式如果不返回任何值,会隐式地返回一个 () 。
fn main() {
assert_eq!(ret_unit_type(), ())
}
fn ret_unit_type() {
let x = 1;
// if 语句块也是一个表达式,因此可以用于赋值,也可以直接返回
// 类似三元运算符,在Rust里我们可以这样写
let y = if x % 2 == 1 {
"odd"
} else {
"even"
};
// 或者写成一行
let z = if x % 2 == 1 { "odd" } else { "even" };
}
函数
在函数界,有一个函数只闻其名不闻其声,可以止小孩啼!在程序界只有 hello,world! 可以与之媲美,它就是 add 函数:
fn add(i: i32, j: i32) -> i32 {
i + j
}
该函数如此简单,但是又是如此的五脏俱全,声明函数的关键字 fn ,函数名 add(),参数 i 和 j,参数类型和返回值类型都是 i32,总之一切那么的普通,但是又那么的自信,直到你看到了下面这张图:
当你看懂了这张图,其实就等于差不多完成了函数章节的学习,但是这么短的章节显然对不起读者老爷们的厚爱,所以我们来展开下。
函数要点
- 函数名和变量名使用蛇形命名法(snake case),例如 fn add_two() -> {}
- 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
- 每个函数参数都需要标注类型
函数返回
函数的返回值就是函数体最后一条表达式的返回值,当然我们也可以使用 return 提前返回,下面的函数使用最后一条表达式来返回一个值:
fn plus_five(x:i32) -> i32 {
x + 5
}
fn main() {
let x = plus_five(5);
println!("The value of x is: {}", x);
}
x + 5 是一条表达式,求值后,返回一个值,因为它是函数的最后一行,因此该表达式的值也是函数的返回值。
再来看两个重点:
- let x = plus_five(5),说明我们用一个函数的返回值来初始化 x 变量,因此侧面说明了在 Rust
中函数也是表达式,这种写法等同于 let x = 5 + 5; - x + 5 没有分号,因为它是一条表达式,这个在上一节中我们也有详细介绍
再来看一段代码,同时使用 return 和表达式作为返回值:
fn plus_or_minus(x:i32) -> i32 {
if x > 5 {
return x - 5
}
x + 5
}
fn main() {
let x = plus_or_minus(5);
println!("The value of x is: {}", x);
}
plus_or_minus 函数根据传入 x 的大小来决定是做加法还是减法,若 x > 5 则通过 return 提前返回 x - 5 的值,否则返回 x + 5 的值。
Rust 中的特殊返回类型
对于 Rust 新手来说,有些返回类型很难理解,而且如果你想通过百度或者谷歌去搜索,都不好查询,因为这些符号太常见了,根本难以精确搜索到。
例如单元类型 (),是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值:
- 函数没有返回值,那么返回一个 ()
- 通过 ; 结尾的表达式返回一个 ()
例如下面的 report 函数会隐式返回一个 ():
use std::fmt::Debug;
fn report<T: Debug>(item: T) {
println!("{:?}", item);
}
与上面的函数返回值相同,但是下面的函数显式的返回了 ():
fn clear(text: &mut String) -> () {
*text = String::from("");
}
在实际编程中,你会经常在错误提示中看到该 () 的身影出没,假如你的函数需要返回一个 u32 值,但是如果你不幸的以 表达式; 的方式作为函数的最后一行代码,就会报错:
fn add(x:u32,y:u32) -> u32 {
x + y;
}
错误如下:
error[E0308]: mismatched types // 类型不匹配
--> src/main.rs:6:24
|
6 | fn add(x:u32,y:u32) -> u32 {
| --- ^^^ expected `u32`, found `()` // 期望返回u32,却返回()
| |
| implicitly returns `()` as its body has no tail or `return` expression
7 | x + y;
| - help: consider removing this semicolon
造成这样的原因,是因为只有表达式能返回值,而 ; 结尾的是语句,在 Rust 中,一定要严格区分表达式和语句的区别,这个在其它语言中往往是被忽视的点。
永不返回的发散函数 !
当用 ! 作函数返回类型的时候,表示该函数永不返回( diverge function ),特别的,这种语法往往用做会导致程序崩溃的函数:
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
}
下面的函数创建了一个无限循环,该循环永不跳出,因此函数也永不返回:
fn forever() -> ! {
loop {
//...
};
}