Rust语言圣经08 - 数值类型深入剖析

Posted 编程学院

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Rust语言圣经08 - 数值类型深入剖析相关的知识,希望对你有一定的参考价值。

原文链接:https://course.rs/basic/base-type/numbers.html
 
欢迎大家加入Rust编程学院,中国最好的Rust学习社区

  1. 官网:https://college.rs
  2. QQ群:1009730433

数值类型

计算机和数值关联在一起的时间,远比你想象的要长,因此数值类型可以说是有计算机以来就有的类型,下面内容将深入讨论Rust的数值类型以及相关的运算符。

整数和浮点数

Rust使用一个相对传统的语法来创建整数(1,2,…)和浮点数(1.0,1.1,…)。整数、浮点数的运算和你在其它语言上见过的一致,都是通过常见的运算符来完成。

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

整数类型

整数是没有是没有小数部分的数字。在之前,我们使用过i32类型, 表示有符号的32位整数(i 是英文单词 integer 的首字母,与之相反的是 u,代表无符号 unsigned 类型)。下表显示了 Rust 中的内置的整数类型, 在有符号和和无符号的列中(例如 i16)的每个定义形式都可用于声明整数类型。

Rust 中的整数类型

长度有符号类型无符号类型
8 位i8u8
16 位i16u16
32 位i32u32
64 位i64u64
128-位i128u128
视架构而定isizeusize

类型定义的形式统一为:有无符号 + 类型大小(位数)。无符号数表示数字只能取正数,而有符号则表示数字即可以取正数又可以取负数。就像在纸上写数字一样:当要强调符号时,数字前面可以带上正号或负号;然而,当很明显确定数字为正数时,就不需要加上正号了。有符号数字以二进制补码形式存储。

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

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

整型的字面量可以可以写成下表 3-2 中任意一种。注意,除了字节字面量之外的所有的数字字面量都允许使用类型后缀,例如 57u8,还有可以使用 _ 作为可视分隔符,如 1_000

表 3-2: Rust 的整型字面量

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

那么该使用哪种类型的整型呢?如果不确定,Rust 的默认值通常是个不错的选择,整型默认是 i32:这通常是最快的,即便在 64 位系统上也是。isizeusize 的主要应用场景是用作某些集合的索引。

整型溢出

比方说有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出。关于这一行为 Rust 有一些有趣的规则。当在 debug 模式编译时,Rust 会检查整型溢出若存在这些问题则使程序在编译时 panic。Rust 使用这个术语来表明程序因错误而退出。 该章节会详细介绍 panic。

在当使用 --release 参数进行 release 模式构建时,Rust 检测溢出。相反当检测到整型溢出时,Rust 会进行一种被称为二进制补码的方式进行(two’s complement wrapping)操作。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖整型溢出包裹的行为不是一种正确的做法。

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

  • 使用 wrapping_* 方法在所有模式下进行包裹,例如 wrapping_add
  • 如果使用 checked_* 方法时发生溢出,则返回 None
  • 使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值
  • 使用 saturating_* 方法使值达到最小值或最大值

浮点类型

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

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

fn main() 
    let x = 2.0; // f64

    let y: f32 = 3.0; // f32

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

数字运算

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

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;

这些语句中的每个表达式都使用了数学运算符,并且计算结果为一个值,然后绑定到一个变量上。附录中给出了 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]);
 

浮点数陷阱

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

  1. 浮点数往往是你想要数字的近似表达
    浮点数类型是基于二进制实现的,但是我们想要计算的数字往往是基于十进制,例如0.1在二进制上并不存在精确的表达形式,但是在十进制上就存在。这种不匹配性导致一定的歧义性,更多的,虽然浮点数能代表真实的数值,但是由于底层格式问题,它往往受限于定长的浮点数精度,如果你想要表达完全精准的真实数字,只有使用无限精度的浮点数才行

  2. 浮点数在某些特性上是反直觉的
    例如你觉得浮点数可以进行比较,对吧?是的,它们确实可以使用>,>=等进行比较,但是在某些场景下,这种直觉上的比较特性反而会害了你。因为f32,f64上的比较运算实现的是std::cmp::PartialEq特征, 但是并没有实现std::cmp::Eq特征,但是后者在其它数值类型上都有定义,说了这么多,可能大家还是云里雾里,用一个例子来举例:

Rust的HashMap数据结构,是一个KV类型的hash map实现,它对于K没有特定类型的限制,但是要求能用作K的类型必须实现了std::cmp::Eq特征,因为这意味着你无法使用浮点数作为HashMap的Key,来存储键值对,但是作为对比,Rust的整数类型、字符串类型、布尔类型都实现了该特征,因此可以作为HashMap的Key。

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

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

来看个小例子:

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 + 0.2 - 0.3).abs() < 0.00001,具体小于多少,取决于你对精度的需求.

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

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.24结尾,但是0.3以3结尾,因此f64下的测试失败了,并且抛出了异常。

是不是blow your mind away? 没关系,在本书的后续章节中类似的直击灵魂的代码还很多,这就是我们敢号称Rust语言圣经的勇气!

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!("未定义的数学行为")
    

有理数和复数

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

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

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

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

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

总结

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

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

数值类型的讲解已经基本结束,接下来来看看字符串。

以上是关于Rust语言圣经08 - 数值类型深入剖析的主要内容,如果未能解决你的问题,请参考以下文章

Rust语言圣经28 - 深入类型转换

Rust语言圣经30 - Panic原理剖析

Rust语言圣经27 - 深入了解特征

Rust学习教程28 - 深入类型转换

Rust学习教程28 - 深入类型转换

Rust学习教程28 - 深入类型转换