|
|
|
@ -1,17 +1,17 @@
|
|
|
|
|
# 数值类型
|
|
|
|
|
我朋友有一个领导(读者:你朋友?黑人问号)说过一句话:所有代码就是0和1,简单的很。咱不评价这句话的正确性,但是计算机底层由01组成倒是真的。
|
|
|
|
|
我朋友有一个领导(读者:你朋友?黑人问号)说过一句话:所有代码就是 0 和 1 ,简单的很。咱不评价这句话的正确性,但是计算机底层由 01 组成倒是真的。
|
|
|
|
|
|
|
|
|
|
计算机和数值关联在一起的时间,远比我们想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论Rust的数值类型以及相关的运算符。
|
|
|
|
|
计算机和数值关联在一起的时间,远比我们想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论 Rust 的数值类型以及相关的运算符。
|
|
|
|
|
|
|
|
|
|
## 整数和浮点数
|
|
|
|
|
|
|
|
|
|
Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1.0`,`1.1`,...)。整数、浮点数的运算和你在其它语言上见过的一致,都是通过常见的运算符来完成。
|
|
|
|
|
Rust 使用一个相对传统的语法来创建整数(`1`, `2`,...)和浮点数(`1.0`,`1.1`,...)。整数、浮点数的运算和你在其它语言上见过的一致,都是通过常见的运算符来完成。
|
|
|
|
|
|
|
|
|
|
> 不仅仅是数值类型,Rust也允许在复杂类型上定义运算符,例如在自定义类型上定义`+`运算符,这种行为被称为运算符重载, Rust具体支持的可重载运算符见[这里](../../appendix/operators.md#运算符)
|
|
|
|
|
> 不仅仅是数值类型,Rust 也允许在复杂类型上定义运算符,例如在自定义类型上定义 `+` 运算符,这种行为被称为运算符重载,Rust 具体支持的可重载运算符见[这里](../../appendix/operators.md#运算符)
|
|
|
|
|
|
|
|
|
|
#### 整数类型
|
|
|
|
|
|
|
|
|
|
**整数**是没有小数部分的数字。之前使用过的`i32`类型, 表示有符号的32位整数(`i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型:
|
|
|
|
|
**整数**是没有小数部分的数字。之前使用过的 `i32` 类型,表示有符号的 32位 整数( `i` 是英文单词 *integer* 的首字母,与之相反的是 `u`,代表无符号 `unsigned` 类型)。下表显示了 Rust 中的内置的整数类型:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 长度 | 有符号类型 | 无符号类型 |
|
|
|
|
@ -23,12 +23,12 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1
|
|
|
|
|
| 128-位 | `i128` | `u128` |
|
|
|
|
|
| 视架构而定 | `isize` | `usize` |
|
|
|
|
|
|
|
|
|
|
类型定义的形式统一为:有无符号 + 类型大小(位数)。**无符号数**表示数字只能取正数,而**有符号**则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[补码](https://en.wikipedia.org/wiki/Two%27s_complement)形式存储。
|
|
|
|
|
类型定义的形式统一为:`有无符号 + 类型大小(位数)`。**无符号数**表示数字只能取正数,而**有符号**则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以[补码](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。
|
|
|
|
|
|
|
|
|
|
此外,`isize` 和 `usize` 类型取决于程序运行的计算机cpu类型: 若cpu是32位的,则这两个类型是32位的,同理,若cpu是64位,那么它们则是64位.
|
|
|
|
|
此外,`isize` 和 `usize` 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。
|
|
|
|
|
|
|
|
|
|
整形字面量可以用下表的形式书写:
|
|
|
|
|
|
|
|
|
@ -42,13 +42,13 @@ Rust使用一个相对传统的语法来创建整数(`1`,`2`,...)和浮点数(`1
|
|
|
|
|
| 字节 (仅限于 `u8`) | `b'A'` |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
这么多类型,有没有一个简单的使用准则?答案是肯定的,Rust整形默认使用`i32`,例如`let i = 1`,那`i`就是`i32`类型,因此你可以首选它,同时该类型也往往是性能最好的。 `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,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。
|
|
|
|
|
> 在当使用 `--release` 参数进行 release 模式构建时,Rust **不**检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(*two’s complement wrapping*)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 `u8` 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 *panic*,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。
|
|
|
|
|
>
|
|
|
|
|
> 要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
|
|
|
|
|
>
|
|
|
|
@ -100,7 +100,7 @@ fn main() {
|
|
|
|
|
这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。[附录](../../appendix/operators.md)中给出了 Rust 提供的所有运算符的列表。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
再来看一个综合性的示例:
|
|
|
|
|
再来看一个综合性的示例:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
@ -135,12 +135,12 @@ println!("{:02}", forty_twos[0]);
|
|
|
|
|
|
|
|
|
|
浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因:
|
|
|
|
|
1. **浮点数往往是你想要数字的近似表达**
|
|
|
|
|
浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如`0.1`在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行
|
|
|
|
|
浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如 `0.1` 在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行
|
|
|
|
|
|
|
|
|
|
2. **浮点数在某些特性上是反直觉的**
|
|
|
|
|
例如大家都会觉得浮点数可以进行比较,对吧?是的,它们确实可以使用`>`,`>=`等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为`f32`,`f64`上的比较运算实现的是`std::cmp::PartialEq`特征(类似其他语言的接口), 但是并没有实现`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`。
|
|
|
|
|
|
|
|
|
|
为了避免上面说的两个陷阱,你需要遵守以下准则:
|
|
|
|
|
- 避免在浮点数上测试相等性
|
|
|
|
@ -154,11 +154,11 @@ fn main() {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
你可能以为,这段代码没啥问题吧,实际上它会panic(程序崩溃,抛出异常),因为二进制精度问题,导致了0.1 + 0.2并不严格等于0.3,它们可能在小数点N位后存在误差。
|
|
|
|
|
你可能以为,这段代码没啥问题吧,实际上它会 *panic*(程序崩溃,抛出异常),因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。
|
|
|
|
|
|
|
|
|
|
那如果非要进行比较呢?可以考虑用这种方式`(0.1 + 0.2 - 0.3).abs() < 0.00001`,具体小于多少,取决于你对精度的需求.
|
|
|
|
|
那如果非要进行比较呢?可以考虑用这种方式 `(0.1 + 0.2 - 0.3).abs() < 0.00001` ,具体小于多少,取决于你对精度的需求。
|
|
|
|
|
|
|
|
|
|
讲到这里,相信大家基本已经明白了,为什么操作浮点数时要格外的小心,但是还不够,下面再来一段代码,直接震撼你的灵魂:
|
|
|
|
|
讲到这里,相信大家基本已经明白了,为什么操作浮点数时要格外的小心,但是还不够,下面再来一段代码,直接震撼你的灵魂:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
@ -197,15 +197,15 @@ 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语言圣经(Rust Course)` 的底气!
|
|
|
|
|
|
|
|
|
|
#### NaN
|
|
|
|
|
|
|
|
|
|
对于数学上未定义的结果,例如对负数取平方根`-42.1.sqrt()`,会产生一个特殊的结果:Rust的浮点数类型使用`NaN`(not a number)来处理这些情况。
|
|
|
|
|
对于数学上未定义的结果,例如对负数取平方根 `-42.1.sqrt()` ,会产生一个特殊的结果:Rust 的浮点数类型使用 `NaN` (not a number)来处理这些情况。
|
|
|
|
|
|
|
|
|
|
**所有跟`NaN`交互的操作,都会返回一个`NaN`**,而且`NaN`不能用来比较,下面的代码会崩溃:
|
|
|
|
|
**所有跟 `NaN` 交互的操作,都会返回一个 `NaN`**,而且 `NaN` 不能用来比较,下面的代码会崩溃:
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
@ -214,7 +214,7 @@ fn main() {
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
出于防御性编程的考虑,可以使用`is_nan()`等方法,可以用来判断一个数值是否是`NaN`:
|
|
|
|
|
出于防御性编程的考虑,可以使用 `is_nan()` 等方法,可以用来判断一个数值是否是 `NaN` :
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
fn main() {
|
|
|
|
@ -227,7 +227,8 @@ 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);
|
|
|
|
@ -253,18 +254,18 @@ for i in 'a'..='z' {
|
|
|
|
|
|
|
|
|
|
## 有理数和复数
|
|
|
|
|
|
|
|
|
|
Rust的标准库相比其它语言,对于准入的门槛较高,因此有理数和复数并未包含在标准库中:
|
|
|
|
|
Rust 的标准库相比其它语言,对于准入的门槛较高,因此有理数和复数并未包含在标准库中:
|
|
|
|
|
- 有理数和复数
|
|
|
|
|
- 任意大小的整数和任意精度的浮点数
|
|
|
|
|
- 固定精度的十进制小数,常用于货币相关的场景
|
|
|
|
|
|
|
|
|
|
好在社区已经开发出高质量的Rust数值库:[num](https://crates.io/crates/num).
|
|
|
|
|
好在社区已经开发出高质量的 Rust 数值库:[num](https://crates.io/crates/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`
|
|
|
|
|
按照以下步骤来引入 `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`
|
|
|
|
|
|
|
|
|
|
```rust
|
|
|
|
|
use num::complex::Complex;
|
|
|
|
@ -281,11 +282,11 @@ use num::complex::Complex;
|
|
|
|
|
|
|
|
|
|
## 总结
|
|
|
|
|
|
|
|
|
|
之前提到了过Rust的数值类型和运算跟其他语言较为相似,但是实际上,除了语法上的不同之外,还是存在一些差异点:
|
|
|
|
|
之前提到了过 Rust 的数值类型和运算跟其他语言较为相似,但是实际上,除了语法上的不同之外,还是存在一些差异点:
|
|
|
|
|
|
|
|
|
|
- **Rust拥有相当多的数值类型**. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数
|
|
|
|
|
- **类型转换必须是显式的**. Rust永远也不会偷偷把你的16bit整数转换成32bit整数
|
|
|
|
|
- **Rust的数值上可以使用方法**. 例如你可以用以下方法来将`13.14`取整: `13.14_f32.round()`, 在这里我们使用了类型后缀,因为编译器需要知道`13.14`的具体类型
|
|
|
|
|
- **Rust 拥有相当多的数值类型**. 因此你需要熟悉这些类型所占用的字节数,这样就知道该类型允许的大小范围以及你选择的类型是否能表达负数
|
|
|
|
|
- **类型转换必须是显式的**. Rust 永远也不会偷偷把你的 16bit 整数转换成 32bit 整数
|
|
|
|
|
- **Rust 的数值上可以使用方法**. 例如你可以用以下方法来将 `13.14` 取整: `13.14_f32.round()`,在这里我们使用了类型后缀,因为编译器需要知道 `13.14 `的具体类型
|
|
|
|
|
|
|
|
|
|
数值类型的讲解已经基本结束,接下来,来看看字符和布尔类型。
|
|
|
|
|
|
|
|
|
|