2.2.1 数值类型

1 数值类型

Rust 使用一个相对传统的语法来创建整数( 12,...)和浮点数( 1.01.1,...)。

不仅仅是数值类型,Rust 也允许在复杂类型上定义运算符,例如在自定义类型上定义 + 运算符,这种行为被称为运算符重载,Rust 具体支持的可重载运算符见 附录 B。

1.1 整数类型

整数 是没有小数部分的数字。下表显示了 Rust 中的内置的整数类型:

长度 有符号类型 无符号类型
8 位 i8 u8
16 位 i16 u16
32 位 i32 u32
64 位 i64 u64
128 位 i128 u128
视架构而定 isize usize

类型定义的形式统一为:有无符号 + 类型大小(位数)

  • 无符号数 表示数字只能取正数和 0,
  • 有符号 则表示数字可以取正数、负数还有 0。

每个有符号类型规定的数字范围是 \(-(2^{n-1}) = 2^{n-1}- 1\),其中 n 是该定义形式的位长度。因此 i8 可存储数字范围是 -128 ~ 127。无符号类型可以存储的数字范围是 \(0 \sim 2^n-1\),所以 u8 能够存储的数字为 0 ~ 255。

此外, isizeusize 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。

整形字面量可以用下表的形式书写:

数字字面量 示例
十进制 98_222
十六进制 0xff
八进制 0o77
二进制 0b1111_0000
字节 (仅限于 u8) b'A'

这么多类型,有没有一个简单的使用准则?答案是肯定的, Rust 整型默认使用 i32,例如 let i = 1,那 i 就是 i32 类型,因此你可以首选它,同时该类型也往往是性能最好的。 isizeusize 的主要应用场景是用作集合的索引。

1.1.1 整型溢出

假设有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生 整型溢出

  • 当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)。

  • 在当使用 --release 参数进行 release 模式构建时,Rust 检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出( two’s complement wrapping)的规则处理。

要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:

  • 使用 wrapping_* 方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add
  • 如果使用 checked_* 方法时发生溢出,则返回 None
  • 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
  • 使用 saturating_* 方法,可以限定计算后的结果不超过目标类型的最大值或低于最小值,例如:
1
2
3
4
5
#![allow(unused)]
fn main() {
assert_eq!(100u8.saturating_add(1), 101);
assert_eq!(u8::MAX.saturating_add(127), u8::MAX);
}

下面是一个演示 wrapping_* 方法的示例:

1
2
3
4
5
fn main() {
let a : u8 = 255;
let b = a.wrapping_add(20);
println!("{}", b); // 19
}

1.2 浮点类型

浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32f64,分别为 32 位和 64 位大小。默认浮点类型是 f64,在现代的 CPU 中它的速度与 f32 几乎相同,但精度更高。

下面是一个演示浮点数的示例:

1
2
3
4
5
fn main() {
let x = 2.0; // f64

let y: f32 = 3.0; // f32
}

浮点数根据 IEEE-754 标准实现。 f32 类型是单精度浮点型, f64 为双精度。

1.2.1 浮点数陷阱

浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因:

  1. 浮点数往往是你想要数字的近似表达 浮点数类型是基于二进制实现的,例如 0.1 在二进制上并不存在精确的表达形式,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行
  2. 浮点数在某些特性上是反直觉的 可以使用 >>= 等进行比较。但可以有问题,因为 f32f64 上的比较运算实现的是 std::cmp::PartialEq 特征(类似其他语言的接口),但是并没有实现 std::cmp::Eq 特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例:

为了避免上面说的两个陷阱,你需要遵守以下准则:

  • 避免在浮点数上测试相等性
  • 当结果在数学上可能存在未定义时,需要格外的小心

来看个小例子:

1
2
3
4
5
fn main() {
// 断言0.1 + 0.2与0.3相等
assert!(0.1 + 0.2 == 0.3);
}

它会 panic(程序崩溃,抛出异常),因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。

那如果非要进行比较呢?可以考虑用这种方式 (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 ,具体小于多少,取决于你对精度的需求。

讲到这里,相信大家基本已经明白了,为什么操作浮点数时要格外的小心,但是还不够,下面再来一段代码,直接震撼你的灵魂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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);
}

运行该程序,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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 的结果是 3e99999a0.3 也是 3e99999a,因此 f32 下的 0.1 + 0.2 == 0.3 通过测试,但是到了 f64 类型时,结果就不一样了,因为 f64 精度高很多,因此在小数点非常后面发生了一点微小的变化, 0.1 + 0.24 结尾,但是 0.33 结尾,这个细微区别导致 f64 下的测试失败了,并且抛出了异常。

1.2.2 NaN

对于数学上未定义的结果,例如对负数取平方根 -42.1.sqrt() ,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN (not a number) 来处理这些情况。

所有跟 NaN 交互的操作,都会返回一个 NaN,而且 NaN 不能用来比较,下面的代码会崩溃:

1
2
3
4
fn main() {
let x = (-42.0_f32).sqrt();
assert_eq!(x, x);
}

出于防御性编程的考虑,可以使用 is_nan() 等方法,可以用来判断一个数值是否是 NaN

1
2
3
4
5
6
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}

1.3 数字运算

Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。下面代码各使用一条 let 语句来说明相应运算的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。 附录 B 中给出了 Rust 提供的所有运算符的列表。

再来看一个综合性的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.4 位运算

Rust 的位运算基本上和其他语言一样

运算符 说明
& 位与 相同位置均为1时则为1,否则为0
| 位或 相同位置只要有1时则为1,否则为0
^ 异或 相同位置不相同则为1,相同则为0
! 位非 把位中的0和1相互取反,即0置为1,1置为0
<< 左移 所有位向左移动指定位数,右位补0
>> 右移 所有位向右移动指定位数,带符号移动(正数补0,负数补1)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fn main() {
// 无符号8位整数,二进制为00000010
let a: u8 = 2; // 也可以写 let a: u8 = 0b_0000_0010;

// 二进制为00000011
let b: u8 = 3;

// {:08b}:左高右低输出二进制01,不足8位则高位补0
println!("a value is {:08b}", a);

println!("b value is {:08b}", b);

println!("(a & b) value is {:08b}", a & b);

println!("(a | b) value is {:08b}", a | b);

println!("(a ^ b) value is {:08b}", a ^ b);

println!("(!b) value is {:08b}", !b);

println!("(a << b) value is {:08b}", a << b);

println!("(a >> b) value is {:08b}", a >> b);

let mut a = a;
// 注意这些计算符除了!之外都可以加上=进行赋值 (因为!=要用来判断不等于)
a <<= b;
println!("(a << b) value is {:08b}", a);
}

1.5 序列(Range)

Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5,生成从 1 到 4 的连续数字,不包含 5 ; 1..=5,生成从 1 到 5 的连续数字,包含 5,它的用途很简单,常常用于循环中:

1
2
3
4
5
6
#![allow(unused)]
fn main() {
for i in 1..=5 {
println!("{}",i);
}
}

最终程序输出:

1
2
3
4
5
1
2
3
4
5

序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。如下是一个使用字符类型序列的例子:

1
2
3
4
5
6
#![allow(unused)]
fn main() {
for i in 'a'..='z' {
println!("{}",i);
}
}

1.6 使用 As 完成类型转换

Rust 中可以使用 As 来完成一个类型到另一个类型的转换,

  • 其最常用于将原始类型转换为其他原始类型,
  • 也可以完成诸如将指针转换为地址、地址转换为指针以及将指针转换为其他指针等功能。

1.7 有理数和复数

Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复数并未包含在标准库中:

  • 有理数和复数
  • 任意大小的整数和任意精度的浮点数
  • 固定精度的十进制小数,常用于货币相关的场景

好在社区已经开发出高质量的 Rust 数值库: num。

按照以下步骤来引入 num 库:

  1. 创建新工程 cargo new complex-num && cd complex-num
  2. Cargo.toml 中的 [dependencies] 下添加一行 num = "0.4.0"
  3. src/main.rs 文件中的 main 函数替换为下面的代码
  4. 运行 cargo run
1
2
3
4
5
6
7
8
9
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)
}

2 总结

之前提到了过 Rust 的数值类型和运算跟其他语言较为相似,但是实际上,除了语法上的不同之外,还是存在一些差异点:

  • Rust 拥有相当多的数值类型. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数
  • 类型转换必须是显式的. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数
  • Rust 的数值上可以使用方法. 例如你可以用以下方法来将 13.14 取整: 13.14_f32.round(),在这里我们使用了类型后缀,因为编译器需要知道 13.14 的具体类型

2.2.1 数值类型
http://binbo-zappy.github.io/2025/01/05/rust圣经/2-2-1-数值类型/
作者
Binbo
发布于
2025年1月5日
许可协议