|
|
|
@ -1,6 +1,7 @@
|
|
|
|
|
# 数值类型
|
|
|
|
|
我朋友有一个领导(读者:你朋友?黑人问号)说过一句话:所有代码就是0和1,简单的很。咱不评价这句话的正确性,但是计算机底层由01组成,倒是真的。
|
|
|
|
|
|
|
|
|
|
计算机和数值关联在一起的时间,远比你想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论Rust的数值类型以及相关的运算符。
|
|
|
|
|
计算机和数值关联在一起的时间,远比我们想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论Rust的数值类型以及相关的运算符。
|
|
|
|
|
|
|
|
|
|
## 整数和浮点数
|
|
|
|
|
|
|
|
|
@ -10,9 +11,8 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1
|
|
|
|
|
|
|
|
|
|
#### 整数类型
|
|
|
|
|
|
|
|
|
|
**整数**是没有是没有小数部分的数字。在之前,我们使用过`i32`类型, 表示有符号的32位整数(`i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型, 在有符号和和无符号的列中(例如 *i16*)的每个定义形式都可用于声明整数类型。
|
|
|
|
|
**整数**是没有小数部分的数字。之前使用过的`i32`类型, 表示有符号的32位整数(`i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型:
|
|
|
|
|
|
|
|
|
|
<span class="caption">Rust 中的整数类型</span>
|
|
|
|
|
|
|
|
|
|
| 长度 | 有符号类型 | 无符号类型 |
|
|
|
|
|
|------------|---------|---------|
|
|
|
|
@ -26,14 +26,12 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1
|
|
|
|
|
类型定义的形式统一为:有无符号 + 类型大小(位数)。**无符号数**表示数字只能取正数,而**有符号**则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[二进制补码](https://en.wikipedia.org/wiki/Two%27s_complement)形式存储。
|
|
|
|
|
|
|
|
|
|
每个有符号类型规定的数字范围是 -(2<sup>n - 1</sup>) ~ 2<sup>n -
|
|
|
|
|
1</sup> - 1,其中 `n` 是该定义形式的位长度。所以 `i8` 可存储数字范围是 -(2<sup>7</sup>) ~ 2<sup>7</sup> - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2<sup>n</sup> - 1,所以 `u8` 能够存储的数字为 0 ~ 2<sup>8</sup> - 1,即 0 ~ 255。
|
|
|
|
|
1</sup> - 1,其中 `n` 是该定义形式的位长度。因此 `i8` 可存储数字范围是 -(2<sup>7</sup>) ~ 2<sup>7</sup> - 1,即 -128 ~ 127。无符号类型可以存储的数字范围是 0 ~ 2<sup>n</sup> - 1,所以 `u8` 能够存储的数字为 0 ~ 2<sup>8</sup> - 1,即 0 ~ 255。
|
|
|
|
|
|
|
|
|
|
此外,`isize` 和 `usize` 类型取决于程序运行的计算机cpu类型: 若cpu是32位的,则这两个类型是32位的,同理,若cpu是64位,那么它们则是64位64位.
|
|
|
|
|
此外,`isize` 和 `usize` 类型取决于程序运行的计算机cpu类型: 若cpu是32位的,则这两个类型是32位的,同理,若cpu是64位,那么它们则是64位.
|
|
|
|
|
|
|
|
|
|
整形字面量可以用下表的形式书写:
|
|
|
|
|
|
|
|
|
|
整型的字面量可以可以写成下表 3-2 中任意一种。注意,除了字节字面量之外的所有的数字字面量都允许使用类型后缀,例如 `57u8`,还有可以使用 `_` 作为可视分隔符,如 `1_000`。
|
|
|
|
|
|
|
|
|
|
<span class="caption">表 3-2: Rust 的整型字面量</span>
|
|
|
|
|
|
|
|
|
|
| 数字字面量 | 示例 |
|
|
|
|
|
|------------------|---------------|
|
|
|
|
@ -43,13 +41,14 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1
|
|
|
|
|
| 二进制 | `0b1111_0000` |
|
|
|
|
|
| 字节 (仅限于 `u8`) | `b'A'` |
|
|
|
|
|
|
|
|
|
|
那么该使用哪种类型的整型呢?如果不确定,Rust 的默认值通常是个不错的选择,整型默认是 `i32`:这通常是最快的,即便在 64 位系统上也是。`isize` 和 `usize` 的主要应用场景是用作某些集合的索引。
|
|
|
|
|
|
|
|
|
|
这么多类型,有没有一个简单的使用准则?答案是肯定的,Rust整形默认使用`i32`,例如`let i = 1`,那`i`就是`i32`类型,因此你可以首选它,同时该类型也往往是性能最好的。 `isize` 和 `usize` 的主要应用场景是用作集合的索引。
|
|
|
|
|
|
|
|
|
|
> ##### 整型溢出
|
|
|
|
|
>
|
|
|
|
|
> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 会检查整型溢出若存在这些问题则使程序在编译时 *panic*。Rust 使用这个术语来表明程序因错误而退出。 [该章节](../../errors/panic.md)会详细介绍 panic。
|
|
|
|
|
> 比方说有一个 `u8` ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生**整型溢出**。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 *panic*(崩溃,Rust 使用这个术语来表明程序因错误而退出)。
|
|
|
|
|
>
|
|
|
|
|
> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码的方式进行(*two’s complement wrapping*)操作。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。
|
|
|
|
|
> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码的方式进行(*two’s complement wrapping*)操作。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出的行为不是一种正确的做法。
|
|
|
|
|
>
|
|
|
|
|
> 要显式处理溢出的可能性,可以使用标准库针对原始数字类型提供的以下的一系列方法:
|
|
|
|
|
>
|
|
|
|
@ -60,7 +59,7 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1
|
|
|
|
|
|
|
|
|
|
#### 浮点类型
|
|
|
|
|
|
|
|
|
|
**浮点类型数字** 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: `f32` 和 `f64`,分别为 32 位和 64 位大小。默认浮点类型是 `f64`,因为在现代的 CPU 中它的速度与 `f32` 几乎相同,但精度更高。
|
|
|
|
|
**浮点类型数字** 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: `f32` 和 `f64`,分别为 32 位和 64 位大小。默认浮点类型是 `f64`,在现代的 CPU 中它的速度与 `f32` 几乎相同,但精度更高。
|
|
|
|
|
|
|
|
|
|
下面是一个演示浮点数的示例:
|
|
|
|
|
|
|
|
|
@ -72,11 +71,11 @@ fn main() {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
浮点数根据 IEEE-754 标准表示。`f32` 类型是单精度浮点型,`f64` 为双精度。
|
|
|
|
|
浮点数根据 `IEEE-754` 标准表示。`f32` 类型是单精度浮点型,`f64` 为双精度。
|
|
|
|
|
|
|
|
|
|
#### 数字运算
|
|
|
|
|
|
|
|
|
|
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。下面代码演示了各使用一条 `let` 语句来说明相应运算的用法:
|
|
|
|
|
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。下面代码各使用一条 `let` 语句来说明相应运算的用法:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
@ -105,43 +104,43 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
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!("{:02}", forty_twos[0]);
|
|
|
|
|
}
|
|
|
|
|
// 编译器会进行自动推导,给予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!("{:02}", forty_twos[0]);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 浮点数陷阱
|
|
|
|
|
|
|
|
|
|
浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因:
|
|
|
|
|
1. **浮点数往往是你想要数字的近似表达**
|
|
|
|
|
浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如0.1在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行
|
|
|
|
|
浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如`0.1`在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行
|
|
|
|
|
|
|
|
|
|
2. **浮点数在某些特性上是反直觉的**
|
|
|
|
|
例如你觉得浮点数可以进行比较,对吧?是的,它们确实可以使用`>`,`>=`等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为`f32`,`f64`上的比较运算实现的是`std::cmp::PartialEq`[特征](../../advance/trait.md), 但是并没有实现`std::cmp::Eq`特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例:
|
|
|
|
|
例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用`>`,`>=`等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为`f32`,`f64`上的比较运算实现的是`std::cmp::PartialEq`特征(类似其他语言的接口), 但是并没有实现`std::cmp::Eq`特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例:
|
|
|
|
|
|
|
|
|
|
Rust的HashMap数据结构,是一个KV类型的hash map实现,它对于K没有特定类型的限制,但是要求能用作K的类型必须实现了`std::cmp::Eq`特征,因为这意味着你无法使用浮点数作为HashMap的Key,来存储键值对,但是作为对比,Rust的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为HashMap的Key。
|
|
|
|
|
Rust的HashMap数据结构,是一个KV类型的hash map实现,它对于`K`没有特定类型的限制,但是要求能用作`K`的类型必须实现了`std::cmp::Eq`特征,因此这意味着你无法使用浮点数作为`HashMap`的`Key`,来存储键值对,但是作为对比,Rust的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为`HashMap`的`Key`。
|
|
|
|
|
|
|
|
|
|
为了避免上面说的两个陷阱,你需要遵守以下准则:
|
|
|
|
|
- 避免在浮点数上测试相等性
|
|
|
|
@ -163,22 +162,22 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
|
let abc: (f32, f32, f32) = (0.1, 0.2, 0.3);
|
|
|
|
|
let xyz: (f64, f64, f64) = (0.1, 0.2, 0.3);
|
|
|
|
|
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!("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!();
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
assert!(abc.0 + abc.1 == abc.2);
|
|
|
|
|
assert!(xyz.0 + xyz.1 == xyz.2);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
运行该程序,输出如下:
|
|
|
|
@ -198,13 +197,13 @@ 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`下的测试失败了,并且抛出了异常。
|
|
|
|
|
仔细看,对`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`下的测试失败了,并且抛出了异常。
|
|
|
|
|
|
|
|
|
|
是不是**blow your mind away**? 没关系,在本书的后续章节中类似的直击灵魂的代码还很多,这就是我们敢号称`Rust语言圣经`的勇气!
|
|
|
|
|
是不是**blow your mind away**? 没关系,在本书的后续章节中类似的直击灵魂的地方还很多,这就是敢号称`Rust语言圣经`的底气!
|
|
|
|
|
|
|
|
|
|
#### NaN
|
|
|
|
|
|
|
|
|
|
对于数学上未定义的结果,例如对负数取平方根`-42.1.sqrt()`,会产生一个特殊的结果:Rust的浮点数类型使用NaN(not a number)来处理这些情况。
|
|
|
|
|
对于数学上未定义的结果,例如对负数取平方根`-42.1.sqrt()`,会产生一个特殊的结果:Rust的浮点数类型使用`NaN`(not a number)来处理这些情况。
|
|
|
|
|
|
|
|
|
|
**所有跟`NaN`交互的操作,都会返回一个`NaN`**,而且`NaN`不能用来比较,下面的代码会崩溃:
|
|
|
|
|
|
|
|
|
@ -228,11 +227,11 @@ fn main() {
|
|
|
|
|
|
|
|
|
|
## 序列(Range)
|
|
|
|
|
|
|
|
|
|
Rust提供了一个非常遍历的方式,让我们能生成连续的数值,例如`1..5`,生成从1到4的连续数字,不包含5; `1..5`,生成从1到5的连续数字,包含5,它的用户很简单,常常用于循环中:
|
|
|
|
|
Rust提供了一个非常简洁的方式,用来生成连续的数值,例如`1..5`,生成从1到4的连续数字,不包含5; `1..=5`,生成从1到5的连续数字,包含5,它的用途很简单,常常用于循环中:
|
|
|
|
|
```rust
|
|
|
|
|
for i in 1..=5 {
|
|
|
|
|
println!("{}",i);
|
|
|
|
|
}
|
|
|
|
|
for i in 1..=5 {
|
|
|
|
|
println!("{}",i);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
最终程序输出:
|
|
|
|
@ -244,24 +243,18 @@ Rust提供了一个非常遍历的方式,让我们能生成连续的数值,
|
|
|
|
|
5
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
序列只允许用于数字或字符类型,因为编译器会在编译时检查序列不为空。字符和数字值是 Rust 仅有的可以判断范围是否为空的类型。
|
|
|
|
|
|
|
|
|
|
如下是一个使用字符类型序列的例子:
|
|
|
|
|
序列只允许用于数字或字符类型,因为编译器会在编译时检查序列不为空。如下是一个使用字符类型序列的例子:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
let x = 'c';
|
|
|
|
|
|
|
|
|
|
match x {
|
|
|
|
|
'a'..='j' => println!("early ASCII letter"),
|
|
|
|
|
'k'..='z' => println!("late ASCII letter"),
|
|
|
|
|
_ => println!("something else"),
|
|
|
|
|
for i in 'a'..='z' {
|
|
|
|
|
println!("{}",i);
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 有理数和复数
|
|
|
|
|
|
|
|
|
|
Rust的标准库相比其它语言,对于准入的门槛较高,因此有理数和复数并未包含在标准库中:
|
|
|
|
|
- 有理数和复数对应的数据库
|
|
|
|
|
- 有理数和复数
|
|
|
|
|
- 任意大小的整数和任意精度的浮点数
|
|
|
|
|
- 固定精度的十进制小数,常用于货币相关的场景
|
|
|
|
|
|
|
|
|
@ -292,9 +285,9 @@ use num::complex::Complex;
|
|
|
|
|
|
|
|
|
|
- **Rust拥有相当多的数值类型**. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数
|
|
|
|
|
- **类型转换必须是显式的**. Rust永远也不会偷偷把你的16bit整数转换成32bit整数
|
|
|
|
|
- **Rust的数值上可以使用方法**. 例如你可以用以下方法来将24.5取整: `13.14_f32.round()`, 在这里我们使用了类型后缀,因为编译器需要知道`13.14`的具体类型
|
|
|
|
|
- **Rust的数值上可以使用方法**. 例如你可以用以下方法来将`24.5`取整: `13.14_f32.round()`, 在这里我们使用了类型后缀,因为编译器需要知道`13.14`的具体类型
|
|
|
|
|
|
|
|
|
|
数值类型的讲解已经基本结束,接下来来看看字符串。
|
|
|
|
|
数值类型的讲解已经基本结束,接下来来看看字符和布尔类型。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|