2022年7月

最近学习rust的时候,了解到rust的浮点数实现是和js是一样的, 也就导致了我们在js上遇到的精度问题, 在rust同样也能遇到.

首先我们来理清, rust的默认浮点类型是f64, 而js由于和其他语言不同, 无论是整数和浮点数都是number类型, 也是64位固定长度, 也就是标准的双精度浮点数,

双精度浮点数(double)是计算机使用的一种数据类型,使用 64 位(8字节) 来存储一个浮点数。 它可以表示十进制的15或16位有效数字,其可以表示的数字的绝对值范围大约是:-1.79E+308 ~ +1.79E+308 [1] 。

既然2种语言底层的标准都是一样的, 都是使用了IEEE 754标准中的double精度, 那我们就直接使用大家熟悉的js来做demo.

为什么使用double精度标准

对比单精度标准来说, 虽然double精度占用比单精度高(8byte > 4byte), 这也就间接意味着cpu在处理上, 单精度会有优势, 但是单精度的致命缺陷就是有效数少而且范围也会更小, 总的来说适用性略低, 而且在现代cpu来说, 处理速度上基本是可以忽略不计的.所以在rust中默认的浮点数类型就是f64, 如果有需要就选择f32(单精度)

double精度如何存储

直接从wiki上抄一张图下来

618px-IEEE_754_Double_Floating_Point_Format.svg.png

  • 符号位:1 位 (+, -)
  • 指数:11 位 (次方)
  • 有效位数精度:52 位

运算过程

我们通常会用十进制来表达浮点数, 但是我们rust/js底层都是用二进制实现浮点数类型的, 比如说我们写一句这样的代码:

var a = 0.1;

在我们程序员眼中它可能就是绝对等于0.1的, 但是在内部实现中, 它需要转换为二进制, 但是在二进制中就是无限精度类型, 也就变成了下面这样:

0.1 -> 0.0001 1001 1001 1001...(1100循环)

但是由于我们底层的标准, 有效位数的精度是52位, 我们在做浮点运算的时候, 多余的数字都会被截断, 所以在js从二进制转换为十进制之后, 就不是我们预想的答案了(在一定精度结果是对的)
, 同理在rust/js中我们也不要使用浮点数做比较, 因为是一个危险不受信赖的计算结果, 也希望精度问题能够引起大家重视, 因为有很多危险的事件是由转换精度触发的:

对于Ariane 4火箭的工作代码在Ariane 5中被重新使用,但是Ariane 5更高速的运算引擎在火箭航天计算机中的算法程序中触发了一个bug。该错误存在于将64位浮点数转换为16位带符号整数的程序中。更快的运算引擎导致了Ariane 5中的64位数据要比Ariane 4中更长,直接诱发了溢出条件,最终导致了航天计算机的崩溃。首先501航天飞机的备份计算机崩溃,然后0.05秒之后,主计算机也崩溃了。这些计算机崩溃直接导致了火箭的主要处理器使火箭的运算引擎过载,同时导致火箭在发射40秒后解体破碎。

顺带提一句, rust中对于整型有溢出处理, 在release环境下, 会按照补码循环溢出的规则去解决, 但是这仍然会造成结果不一致的错误.

如何解决

rust: 我不知道咋解决, 我才学rust
js: 大把的精度库, 最流行的方案就是底层使用string了, bignumber.js, 就可以避免浮点数陷阱;

基础类型

rust是一门静态编程语言, 所以我们有必要知道它的类型, rust的类型可以分为基本类型和复合类型(也可以称之为复杂类型), 基本类型指的就是原子化最小的类型, 它不能转换为其他类型;

rust不像ts, ts是js的超集, ts可以更好的推断类型, 但是rust拥有一个很聪明的编译器, 你可以无需指定类型, 让编译器去自动推断, 但是某些情况下编译器无法推断类型, 比如这样

let guess = "42".parse().expect("Not a number!");

鬼知道guess是什么类型, 所以编译器会报错, 你只需要显式的指定类型即可;

数值类型

rust创建数值非常简单

let a = 1;

整数

我们继续来探讨整数类型, 我们之前了解过的i32类型, 表示有符号的32位整数

i表示integer, 与之相反的是u, 代表无符号

类型统一定义为 “有无符号” + “位数(bit)”, 无符号表示就是正数, 有符号就是正负; 简单的使用准则需要我们记住, rust整形默认使用i32, 可以首选i32, 而且性能也是最好的;

我们在处理整型的时候, 如果设置的值超过了范围, 那么rust会区分模式, 做不同的决策, 比如在debug模式中, 程序会报错退出; 如果是release环境, 会被补码为范围内的最小值, 因此这样的结果是不符合预期的, 会被认为是错误的.

浮点数

浮点数默认类型是f64, 但是rust有一个浮点数的陷阱, 我们通常会使用十进制表达浮点数, 但是rust底层是使用二进制实现浮点数类型的, 比如说0.5在十进制上存在精确表达, 但是在二进制中不存在, 所以就会造成歧义, 所以想要表达完整的真实浮点数字, 就必须使用无限精度的浮点数才行.

还有一个注意的点就是, 我们不要对浮点进行比较, 这和js一样, 由于rust的浮点类型底层是二进制, 而js同样在浮点数运算时也是二进制, 都会存在不准确的问题; 在rust中对浮点数进行比较的时候, float声明了PartialEq, 注意它不是Eq; 在map数据结构中的k类型, rust是需要你传入声明过Eq 的类型, 很显然float不满足要求; 而且当我们使用浮点做比较的时候, 会造成编译器错误.

NaN

和js一样, 我们可以在rust中做一个防御性编程:

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

序列

rust提供了一种简洁的方式生成连续的数值

// 生成1-5 (包括5)
1..=5
// 生成1-5 (不包括5)
1..5

字符, 布尔, 单元类型

这一部分比较简单,

char

fn main() {
    let c = 'z';
    let z = 'ℤ';
    let g = '国';
    let heart_eyed_cat = 'hhh';
}

不管是ascii, 还是unicloud都算作rust字符char; 另外字符是用’’包裹的, “”是表达为字符串

bool

rust中的布尔类型和其他语言一样, 存在true/false

单元类型

0长度的元组, 单元类型使用()来表达, 我们可以用单元类型占位, 它不占用任何内存, rust函数中的main还有print函数都会返回一个单元类型. 我们不能说main函数没有返回值, 因为没有返回值在rust中是有单独的定义的(发散函数: diverge function) , 顾名思义, 无法收敛的函数.

语句和表达式

在rust的函数体中是一系列语句组成, 最后是由表达式返回, 比如这样

fn add(x: i32, y: i32) -> i32 {
    let x = x + 2; // 语句
    let y = y + 3; // 语句
    x + y // 表达式
}

语句

 let x = x + 2; // 语句
 let y = y + 3; // 语句

由于let是语句, 我们不能将let赋值给其他值:

let b = (let a = 8);

这样编译器会报错…

表达式

表达式会进行运算/求值, 比如1+2, 会返回3, 我们在语句上面写了这段代码:

 let x = x + 2; 

这里的x + 2确实是一个表达式, 只要记住只要能返回值, 它就是表达式 而且表达式不能包含分号, 比如下面一段代码

let y = {
    let x = 3;
    x + 1
};

如果x + 1包含了分号, 那么此时这个语句块不会返回任何值了, 也就不算作表达式了, 所以我们在rust中编写函数的时候, 如果要返回值, 就要注意这里不需要加分号;

函数

rust的函数很简单, 下面是一个例子, 有关键字, 函数名称, 形参名称和类型, 函数返回值类型, 以及一个代码块, 就构成了一个最简单的函数:

fn add(i: i32, j: i32) -> i32 {
   i + j
 }
  1. 首先rust是强类型的语言, 我们在编写函数的时候, 参数的类型是必须的, 如果不提供参数的类型是会报错的.
  2. 函数的返回值就是代码块的最后一行的表达式返回值, 当然我们也可以使用return关键字提前返回也可以
  3. 在单元类型中, 我们提到了无返回值, 我们可以使用单元类型作为返回, 而一些表达式你不幸加了分号变成了语句, 那么此时也会返回一个单元类型.
  4. 在上文提到了diverge function, 我们可以使用!作为函数返回类型, 这一般会作为程序崩溃的函数, 代表永不返回:
fn end() -> ! {
  panic!("it is over...");
}