基本类型
此笔记记录于Rust Course,大多数为其中的摘要,少数为笔者自己的理解
数值类型
整数类型
长度 | 有符号类型 | 无符号类型 |
---|---|---|
8 位 | i8 | u8 |
16 位 | i16 | u16 |
32 位 | i32 | u32 |
64 位 | i64 | u64 |
128 位 | i128 | u128 |
视架构而定 | isize | usize |
这么多类型,有没有一个简单的使用准则?答案是肯定的, Rust 整型默认使用 i32
,例如 let i = 1
,那 i
就是 i32
类型,因此你可以首选它,同时该类型也往往是性能最好的。isize
和 usize
的主要应用场景是用作集合的索引。
整型溢出
假设有一个 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
}
浮点类型
浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32
和 f64
,分别为 32 位和 64 位大小。默认浮点类型是 f64
,在现代的 CPU 中它的速度与 f32
几乎相同,但精度更高。
浮点数根据 IEEE-754
标准实现。f32
类型是单精度浮点型,f64
为双精度。
浮点数陷阱
- 浮点数往往是你想要数字的近似表达,IEEE-754 的问题
- 浮点数在某些特性上是反直觉的
关于第二点:f32
, f64
上的比较运算实现的是 std::cmp::PartialEq
特征(类似其他语言的接口),但是并没有实现 std::cmp::Eq
特征。Rust 的 HashMap
数据结构,是一个 KV 类型的 Hash Map 实现,它对于 K
没有特定类型的限制,但是要求能用作 K
的类型必须实现了 std::cmp::Eq
特征,因此这意味着你无法使用浮点数作为 HashMap
的 Key
,来存储键值对,但是作为对比,Rust 的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为 HashMap
的 Key
。
为了避免上面说的两个陷阱,你需要遵守以下准则:
- 避免在浮点数上测试相等性
- 当结果在数学上可能存在未定义时,需要格外的小心
NaN
对于数学上未定义的结果,例如对负数取平方根 -42.1.sqrt()
,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN
(not a number)来处理这些情况。
所有跟 NaN
交互的操作,都会返回一个 NaN
,而且 NaN
不能用来比较,下面的代码会崩溃:
fn main() {
let x = (-42.0_f32).sqrt();
assert_eq!(x, x);
}
出于防御性编程的考虑,可以使用 is_nan()
等方法,可以用来判断一个数值是否是 NaN
:
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}
数字运算
fn main() {
// 加法
let sum = 5 + 10;
// 减法
let difference = 95.5 - 4.3;
// 乘法
let product = 4 * 30;
// 除法
let quotient = 56.7 / 32.2;
// 求余
let remainder = 43 % 5;
}
fn main() {
// 编译器会进行自动推导,给予 twenty i32 的类型
let twenty = 20;
// 类型标注
let twenty_one: i32 = 21;
// 通过类型后缀的方式进行类型标注:22 是 i32 类型
let twenty_two = 22i32;
// 只有同样类型,才能运算
let addition = twenty + twenty_one + twenty_two;
println!("{} + {} + {} = {}", twenty, twenty_one, twenty_two, addition);
// 对于较长的数字,可以用_进行分割,提升可读性
let one_million: i64 = 1_000_000;
println!("{}", one_million.pow(2));
// 定义一个 f32 数组,其中 42.0 会自动被推导为 f32 类型
let forty_twos = [
42.0,
42f32,
42.0_f32,
];
// 打印数组中第一个值,并控制小数位为 2 位
println!("{:.2}", forty_twos[0]);
}
位运算
运算符 | 说明 |
---|---|
& 位与 | 相同位置均为 1 时则为 1,否则为 0 |
| 位或 | 相同位置只要有 1 时则为 1,否则为 0 |
^ 异或 | 相同位置不相同则为 1,相同则为 0 |
! 位非 | 把位中的 0 和 1 相互取反,即 0 置为 1,1 置为 0 |
<< 左移 | 所有位向左移动指定位数,右位补 0 |
>> 右移 | 所有位向右移动指定位数,带符号移动(正数补 0,负数补 1) |
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);
}
序列(Range)
Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5
,生成从 1 到 4 的连续数字,不包含 5 ;1..=5
,生成从 1 到 5 的连续数字,包含 5,它的用途很简单,常常用于循环中
for i in 'a'..='z' {
println!("{}",i);
}
使用 As 完成类型转换
Rust 中可以使用 As 来完成一个类型到另一个类型的转换,其最常用于将原始类型转换为其他原始类型,但是它也可以完成诸如将指针转换为地址、地址转换为指针以及将指针转换为其他指针等功能。
有理数和复数
Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复数并未包含在标准库中:
- 有理数和复数
- 任意大小的整数和任意精度的浮点数
- 固定精度的十进制小数,常用于货币相关的场景
好在社区已经开发出高质量的 Rust 数值库:num。
按照以下步骤来引入 num
库:
- 创建新工程
cargo new complex-num && cd complex-num
- 在
Cargo.toml
中的[dependencies]
下添加一行num = "0.4.0"
- 将
src/main.rs
文件中的main
函数替换为下面的代码 - 运行
cargo run
use num::complex::Complex;
fn main() {
let a = Complex { re: 2.1, im: -1.2 };
let b = Complex::new(11.1, 22.2);
let result = a + b;
println!("{} + {}i", result.re, result.im)
}
字符类型
所有的 Unicode
值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。Unicode
值的范围从 U+0000 ~ U+D7FF
和 U+E000 ~ U+10FFFF
。不过“字符”并不是 Unicode
中的一个概念,所以人在直觉上对“字符”的理解和 Rust 的字符概念并不一致。
由于 Unicode
都是 4 个字节编码,因此字符类型也是占用 4 个字节
Rust 的字符只能用 ''
来表示, ""
是留给字符串的。
布尔类型
Rust 中的布尔类型有两个可能的值:true
和 false
,布尔值占用内存的大小为 1
个字节
单元类型
单元类型就是 ()
,对,你没看错,就是 ()
,唯一的值也是 ()
,一些读者读到这里可能就不愿意了,你也太敷衍了吧,管这叫类型?
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)的方式是非常重要的,你需要能明确的区分这两个概念, 但是对于很多其它语言而言,这两个往往无需区分。基于表达式是函数式语言的重要特征,表达式总要返回值。
语句
let a = 8;
let b: Vec<f64> = Vec::new();
let (a, c) = ("hi", false);
以上都是语句,它们完成了一个具体的操作,但是并没有返回值,因此是语句。
由于 let
是语句,因此不能将 let
语句赋值给其它值,如下形式是错误的:
let b = (let a = 8);
表达式
表达式会进行求值,然后返回一个值。例如 5 + 6
,在求值后,返回值 11
,因此它就是一条表达式。
表达式可以成为语句的一部分,例如 let y = 6
中,6
就是一个表达式,它在求值后返回一个值 6
(有些反直觉,但是确实是表达式)。
花括号包裹最终返回一个值的语句块也是表达式,总之,能返回值,它就是表达式:
fn main() {
let y = {
let x = 3;
x + 1
};
println!("The value of y is: {}", y);
}
表达式如果不返回任何值,会隐式地返回一个 ()
。
函数
fn add(i: i32, j: i32) -> i32 {
i + j
}
- 函数名和变量名使用蛇形命名法(snake case),例如
fn add_two() -> {}
- 函数的位置可以随便放,Rust 不关心我们在哪里定义了函数,只要有定义即可
- 每个函数参数都需要标注类型
在 Rust 中函数就是表达式,因此我们可以把函数的返回值直接赋给调用者。
函数的返回值就是函数体最后一条表达式的返回值,当然我们也可以使用 return
提前返回,下面的函数使用最后一条表达式来返回一个值:
fn plus_or_minus(x:i32) -> i32 {
if x > 5 {
return x - 5
}
x + 5
}
无返回值()
单元类型 ()
,是一个零长度的元组。它没啥作用,但是可以用来表达一个函数没有返回值:
- 函数没有返回值,那么返回一个
()
- 通过
;
结尾的表达式返回一个()
永不返回的发散函数
当用 !
作函数返回类型的时候,表示该函数永不返回( diverge function ),类比 typescript 中的 never 类型
fn dead_end() -> ! {
panic!("你已经到了穷途末路,崩溃吧!");
}