文章目录
团队博客: 汽车电子社区
一、概述
Rust基本类型有如下几种:
1、数值类型 : 有符号整数 (i8, i16, i32, i64, isize)、 无符号整数 (u8, u16, u32, u64, usize) 、浮点数 (f32, f64)、以及有理数、复数。
2、字符串 :字符串字面量和字符串切片 &str。
3、布尔类型 : true和false。
4、字符类型 : 表示单个 Unicode 字符,存储为 4 个字节。
5、单元类型: 即 () ,其唯一的值也是 ()。
二、数值类型
2.1、整数类型
整数是没有小数部分的数字。之前使用过的 i32 类型,表示有符号的 32 位整数( i 是英文单词 integer 的首字母,与之相反的是 u,代表无符号 unsigned 类型)。下表显示了 Rust 中的内置的整数类型:
isize 和 usize 类型取决于程序运行的计算机 CPU 类型: 若 CPU 是 32 位的,则这两个类型是 32 位的,同理,若 CPU 是 64 位,那么它们则是 64 位。
整形字面量可以用下表的形式书写:
整型溢出:
假设有一个 u8 ,它可以存放从 0 到 255 的值。那么当你将其修改为范围之外的值,比如 256,则会发生整型溢出。关于这一行为 Rust 有一些有趣的规则:当在 debug 模式编译时,Rust 会检查整型溢出,若存在这些问题,则使程序在编译时 panic(崩溃,Rust 使用这个术语来表明程序因错误而退出)。
在当使用 --release 参数进行 release 模式构建时,Rust 不检测溢出。相反,当检测到整型溢出时,Rust 会按照补码循环溢出(two's complement wrapping)的规则处理。简而言之,大于该类型最大值的数值会被补码转换成该类型能够支持的对应数字的最小值。比如在 u8 的情况下,256 变成 0,257 变成 1,依此类推。程序不会 panic,但是该变量的值可能不是你期望的值。依赖这种默认行为的代码都应该被认为是错误的代码。
要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
1、使用 wrapping_* 方法在所有模式下都按照补码循环溢出规则处理,例如 wrapping_add。
2、如果使用 checked_* 方法时发生溢出,则返回None值。
3、使用 overflowing_* 方法返回该值和一个指示是否存在溢出的布尔值。
4、使用 saturating_* 方法使值达到最小值或最大值。
2.2、浮点类型
浮点类型数字 是带有小数点的数字,在 Rust 中浮点类型数字也有两种基本类型: f32 和 f64,分别为 32 位和 64 位大小。默认浮点类型是 f64,在现代的 CPU 中它的速度与 f32 几乎相同,但精度更高。
cpp
fn main() {
let x = 2.0; // f64
let y: f32 = 3.0; // f32
}
浮点数陷阱:
浮点数由于底层格式的特殊性,导致了如果在使用浮点数时不够谨慎,就可能造成危险,有两个原因:
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。
为了避免上面说的两个陷阱,你需要遵守以下准则:
1、避免在浮点数上测试相等性。
2、当结果在数学上可能存在未定义时,需要格外的小心。
NaN
对于数学上未定义的结果,例如对负数取平方根 -42.1.sqrt() ,会产生一个特殊的结果:Rust 的浮点数类型使用 NaN (not a number)来处理这些情况。
所有跟 NaN 交互的操作,都会返回一个 NaN,而且 NaN 不能用来比较,下面的代码会崩溃:
cpp
fn main() {
let x = (-42.0_f32).sqrt();
assert_eq!(x, x);
}
出于防御性编程的考虑,可以使用 is_nan() 等方法,可以用来判断一个数值是否是 NaN :
cpp
fn main() {
let x = (-42.0_f32).sqrt();
if x.is_nan() {
println!("未定义的数学行为")
}
}
2.3、数字运算
Rust 支持所有数字类型的基本数学运算:加法、减法、乘法、除法和取模运算。下面代码各使用一条 let 语句来说明相应运算的用法:
cpp
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;
}
cpp
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]);
}
2.4、位运算
Rust的运算基本上和其他语言一样。
cpp
fn main() {
// 二进制为00000010
let a:i32 = 2;
// 二进制为00000011
let b:i32 = 3;
println!("(a & b) value is {}", a & b);
println!("(a | b) value is {}", a | b);
println!("(a ^ b) value is {}", a ^ b);
println!("(!b) value is {} ", !b);
println!("(a << b) value is {}", a << b);
println!("(a >> b) value is {}", a >> b);
let mut a = a;
// 注意这些计算符除了!之外都可以加上=进行赋值 (因为!=要用来判断不等于)
a <<= b;
println!("(a << b) value is {}", a);
}
2.5、序列(Range)
Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1...5,生成从 1 到 4 的连续数字,不包含 5 ;1...=5,生成从 1 到 5 的连续数字,包含 5,它的用途很简单,常常用于循环中:
cpp
for i in 1..=5 {
println!("{}",i);
}
序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。如下是一个使用字符类型序列的例子:
cpp
for i in 'a'..='z' {
println!("{}",i);
}
2.6、有理数和复数
Rust 的标准库相比其它语言,准入门槛较高,因此有理数和复数并未包含在标准库中:
1、有理数和复数。
2、任意大小的整数和任意精度的浮点数。
3、固定精度的十进制小数,常用于货币相关的场景。
好在社区已经开发出高质量的 Rust 数值库:num。
cpp
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)
}
三、字符、布尔、单元类型
3.1、字符类型
cpp
fn main() {
let c = 'z';
let z = 'ℤ';
let g = '国';
let heart_eyed_cat = '😻';
}
Rust 的字符不仅仅是 ASCII,所有的 Unicode 值都可以作为 Rust 字符,包括单个的中文、日文、韩文、emoji 表情符号等等,都是合法的字符类型。Unicode 值的范围从 U+0000 ~ U+D7FF 和 U+E000 ~ U+10FFFF。不过"字符"并不是 Unicode 中的一个概念,所以人在直觉上对"字符"的理解和 Rust 的字符概念并不一致。
由于 Unicode 都是 4 个字节编码,因此字符类型也是占用 4 个字节:
cpp
fn main() {
let x = '中';
println!("字符'中'占用了{}字节的内存大小",std::mem::size_of_val(&x));
}
Rust 的字符只能用 '' 来表示, "" 是留给字符串的
3.2、布尔类型(bool)
Rust 中的布尔类型有两个可能的值:true 和 false,布尔值占用内存的大小为 1 个字节:
cpp
fn main() {
let t = true;
let f: bool = false; // 使用类型标注,显式指定f的类型
if f {
println!("这是段毫无意义的代码");
}
}
3.3、单元类型
单元类型就是 () ,对,你没看错,就是 () ,唯一的值也是 () ,一些读者读到这里可能就不愿意了,你也太敷衍了吧,管这叫类型?
只能说,再不起眼的东西,都有其用途,在目前为止的学习过程中,大家已经看到过很多次 fn main() 函数的使用吧?那么这个函数返回什么呢?
没错, main 函数就返回这个单元类型 (),你不能说 main 函数无返回值,因为没有返回值的函数在 Rust 中是有单独的定义的:发散函数( diverge function ),顾名思义,无法收敛的函数。
例如常见的 println!() 的返回值也是单元类型 ()。
再比如,你可以用 () 作为 map 的值,表示我们不关注具体的值,只关注 key。 这种用法和 Go 语言的 struct{} 类似,可以作为一个值用来占位,但是完全不占用任何内存。