Rust从入门到精通04-数据类型
Rust 是 静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
在 Rust 中,每一个值都属于某一个 数据类型(data type),分为两大类:
①、标量(基本数据类型)(scalar):整型、浮点型、布尔类型、字符类型
②、复合(compound):元祖(tuple)、数组(array)、结构体(struct)
0、静态类型和动态类型区别
静态类型和动态类型是编程语言的两种基本类型系统。如果一门语言不谈数据类型,大概率是动态类型。
本质上它们主要的区别在于类型检查发生的时间和方式。
静态类型语言:
- 在编译时期进行类型检查。这意味着在你的代码运行之前,编译器就会检查数据类型的正确性。如果数据类型不匹配,编译器将引发错误,代码无法编译。
- 由于静态类型语言在编译时期就完成了类型检查,因此它们通常具有更好的性能,因为运行时没有类型检查的开销。
- 静态类型语言通常需要显式声明变量的类型。这使得代码更具可读性,并且更易于维护和调试,因为类型信息可以帮助开发者理解变量和函数的行为。
- 静态类型语言的例子包括:Java、C++、C#、Rust 等。
动态类型语言:
- 在运行时期进行类型检查。这意味着在代码执行时,解释器会检查数据类型的正确性。如果数据类型不匹配,将引发运行时错误。
- 动态类型语言在编写代码时具有更大的灵活性,因为你不需要显式声明变量的类型。这使得代码更简洁,但也可能导致类型错误更难检测和调试。
- 动态类型语言的性能通常不如静态类型语言,因为它们在运行时需要进行类型检查。
- 动态类型语言的例子包括:Python、Ruby、JavaScript、PHP 等。
1、标量-基本数据类型 scalar
每个类型有一个单独的值。
1.1 整型
表示没有小数部分的数字,分为有符号(以 i 开头)和无符号(以 u 开头)整型。
数字类型的默认类型是 i32。
| 长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 |
u8 |
| 16-bit | i16 |
u16 |
| 32-bit | i32 |
u32 |
| 64-bit | i64 |
u64 |
| 128-bit | i128 |
u128 |
| arch | isize |
usize |
每一个有符号的整型可以储存包含从 -($2^{n - 1}$) 到 $2^{n - 1}$ - 1 在内的数字,这里 n 是整型定义的长度。所以 i8 可以储存从 -$2^7$到 $2^7$ - 1 在内的数字,也就是从 -128 到 127。无符号的变体可以储存从 0 到 $2^{n - 1}$ 的数字,所以 u8 可以储存从 0 到 $2^8 - 1$ 的数字,也就是从 0 到 255。
另外,isize 和 usize 类型依赖运行程序的计算机架构:64 位架构上它们是 64 位的, 32 位架构上它们是 32 位的。
这里我说下为啥会有这种依赖计算机架构的数据长度,主要有两点:
①、性能优化:与系统架构直接对应,这意味着它们可以被优化以适应特定平台的最佳性能。例如,在 64 位系统上,usize 可以高效地处理较大的数据结构,因为它能够表示的地址空间更大。
②、跨平台兼容:使用 usize 和 isize 可以让同一段 Rust 代码在不同架构的系统上运行而无需修改。这对于编写库或框架特别重要,因为这些库或框架需要在各种硬件和操作系统上工作。
1.1.1 所有数字字面量,可以在任意地方添加下划线_
①、十进制字面量 12_222,使用下划线按三位数字一组隔开
②、十六进制字面量 0xff,使用0x开头
③、八进制字面量 0o66,使用0o(小写字母o)开头
④、二进制字面量 0b1111_0000,使用0b开头,按4位数字一组隔开
⑤、字符的字节表示 b'A',对一个ASCII字符,在其前面加b前缀,直接得到此字符的ASCII码值
fn int_test(){
//所有数字字面量,可以在任意地方添加下划线_
let x : u32 = 1_2_3;
let y = x + 1;
//打印结果为 124
println!("{}",y);
}
fn main() {
let i1 = 12_222;
let i2 = 0xff;
let i3 = 0o66;
let i4 = 0b1111_0000;
let i5 = b'A';
// i1=12222;i2=255;i3=54;i4=240;i5=65
println!("i1={};i2={};i3={};i4={};i5={}",i1,i2,i3,i4,i5)
}
1.1.2 字面量可以跟类型后缀,表示数字具体类型
//字面量可以跟类型后缀,表示数字具体类型
fn int_test2(){
let x = 123i32;
let y = x + 1;
//打印结果为 124
println!("{}",y);
}
1.1.3 直接对整型字面量调用函数
//直接对整型字面量调用函数
fn int_test3(){
let x : i32 = 9;
//打印结果为 729
println!("9 power 3 = {}",x.pow(3));
}
1.1.4 整数溢出
Rust 对于整数溢出的处理方式如下:
①、默认情况下,在debug模式下编译器会自动插入整数溢出检查,一旦发生溢出,则会引发 panic;
②、在 release 模式下(使用 --release 参数),不检查整数溢出,而是自动舍弃高位的方式。
要显式处理可能的溢出,可以使用标准库针对原始数字类型提供的这些方法:
- 使用
wrapping_*方法在所有模式下都按照补码循环溢出规则处理,例如wrapping_add - 如果使用
checked_*方法时发生溢出,则返回None值 - 使用
overflowing_*方法返回该值和一个指示是否存在溢出的布尔值 - 使用
saturating_*方法使值达到最小值或最大值
fn main() {
let a : u8 = 255;
let b = a.wrapping_add(20);
println!("{}", b); // 19
}
这里其实也体现了 rust 这门语言的安全性,大家如果有项目开发经验,对于整数溢出导致的bug,其实是很难排查的,项目没有任何错误日志,但是得到的数据就是不对,往往令人崩溃,而 rust 直接程序 panic,这样极大的方便我们去排查问题。
1.1.5 如何选择
通常默认类型 i32 即可,它通常是最快的,性能最好的。
isize 和 usize 的主要应用场景是用作集合的索引。
1.2 浮点
Rust 有两个原生的 浮点数(floating-point numbers)类型,它们是带小数点的数字。是基于 IEEE 754-2008 标准的浮点类型,分别是 f32 和 f64,分别占 32 位和 64 位。默认类型是 f64,因为在现代 CPU 中,它与 f32 速度几乎一样,不过精度更高。
fn float_test(){
//123.0 f32类型
let f1 = 123.0f32;
//0.1 f64类型
let f2 = 0.1f64;
}
1.2.1 浮点数陷阱
浮点数基本上是近似数表达,因为浮点数是基于二进制实现的。
比如对于 0.1 ,在十进制是精确存在的,但是在二进制上并不存在精确的表达形式,所以要避免对浮点数进行相等性测试。
fn main() {
assert!(0.1 + 0.2 == 0.3);
}
运行报错。因为二进制精度问题,导致了 0.1 + 0.2 并不严格等于 0.3,它们可能在小数点 N 位后存在误差。
如果非要比较,考虑用这种方式 (0.1_f64 + 0.2 - 0.3).abs() < 0.00001 ,具体小于多少,取决于你对精度的需求。

1.3 布尔类型
布尔类型(bool)代表的是“是”和“否”的二值逻辑。它有两个值:
true和false
一般用在逻辑表达式中,可以执行“与”“或”“非”等运算,常用于条件判断和控制流操作。
布尔值占用内存的大小为 1 个字节
fn bool_test(){
let x = true;
//取反运算
let y = !x;
//逻辑与,带短路功能
let z = x && y;
//逻辑或,带短路功能
let z = x || y;
if x {
println!("Rust is awesome!");
}
}
1.4、字符类型
字符类型由 char 表示。它可以描述任何一个符合 unicode 标准的字符值。在代码中,单个的字符字面量用单引号包围(不同于字符串用双引号):
1.4.1 4个字节字符
let heart_eyed_cat = '😻';
因为 char 类型的设计目的是描述任意一个 unicode 字符,因此它占据的内存空间不是1个字节,而是 4 个字节。
这意味着它可以比 ASCII 表示更多内容。在 Rust 中,拼音字母(Accented letters),中文、日文、韩文等字符,emoji(绘文字)以及零长度的空白字符都是有效的 char 值。
Unicode 标量值包含从 U+0000 到 U+D7FF 和 U+E000 到 U+10FFFF 在内的值。
1.4.2 1个字节字符-u8
let x : u8 = 1;
对于 ASCII 字符其实只需要占据一个字节的空间,因此Rust 提供了单字节字符字面量来表示 ASCII 字符。
注意:我们还可以通过一个字母 b 在字符或者字符串前面,代表这个字面量存储在 u8 类型数组中,这样占用空间比 char 型数组要小一些。
let x : u8 = 1; let y : u8 = b'A';
1.5 序列(Range)
Rust 提供了一个非常简洁的方式,用来生成连续的数值,例如 1..5,生成从 1 到 4 的连续数字,不包含 5 ;1..=5,生成从 1 到 5 的连续数字,包含 5,它的用途很简单,常常用于循环中:
for i in 1..=5 {
println!("{}",i);
}
//打印结果
1
2
3
4
5
序列只允许用于数字或字符类型,原因是:它们可以连续,同时编译器在编译期可以检查该序列是否为空,字符和数字值是 Rust 中仅有的可以用于判断是否为空的类型。如下是一个使用字符类型序列的例子:
for i in 'a'..='z' {
println!("{}",i);
}
2、复合compound
复合类型(Compound types)可以将多个值组合成一个类型
2.1 元祖(tuple)
①、由圆括号()包含一组表达式组成;
②、长度固定,一旦声明,其长度不会增大或缩小。
③、rust中可以存放不同类型的数据类型
2.1.2 实例
fn tuple_test1(){
//包含两个元素:1和false
let a = (1i32,false);
//包含两个元素:1和元祖,元祖包含两个字符1和2
let b = (1,("1","2"));
}
2.1.3 如果元祖只有一个元素,应该添加一个逗号,用来区分括号表达式和元祖
//如果元祖只有一个元素,应该添加一个逗号,用来区分括号表达式和元祖
fn tuple_test2(){
//a 是一个元祖,只有一个元素1
let a = (1,);
//b 是一个括号表达式,它是 i32类型
let b = (1);
}
2.1.4 访问元祖元素
①、模式匹配解构
//元祖:模式匹配
fn tup_test4(){
let tup = (1,1.1,2);
let (x,y,z) = tup;
println!("x={},y={},z={}",x,y,z);
}
②、数字索引
//元祖:数字索引
fn tup_test5(){
let tup = (1,1.1,2);
println!("x={},y={},z={}",tup.0,tup.1,tup.2);
}
2.1.5 元祖总结
元祖是长度固定,可以存放不同元素的集合。
通常用作函数的返回值,因为你想把多个不同类型的值一次返回的话,元祖就很有用了。
另外,当没有任何元素的时候,元组退化成 (),就叫做unit类型,是Rust中一个非常重要的基础类型和值,unit类型唯一的值实例就是(),与其类型本身的表示相同。比如一个函数没有返回值的时候,它实际默认返回的是这个unit值。
2.2 数组(array)
①、由中括号[] 包含一组表达式组成;
②、数组中每个元素的类型必须相同(元祖tuple可以不同);
③、长度固定,一旦声明,其长度不会增大或缩小。
可以看到,数组的长度居然不能改变,这是因为固定尺寸的数据类型是可以直接放栈上的,创建和回收都比在堆上动态分配的动态数组性能要好,Rust 出于此考虑做出了限制。
2.2.1 实例
有三种方式声明。
//数组:实例
fn array_test1(){
//1、省略类型和长度
let a = [1,1,1,1];
//2、声明类型和长度
let b:[i32;4] = [1,1,1,1];
//3、声明初始值和长度
let c = [1;4];
println!("{}",a == b);//true
println!("{}",a == c);//true
println!("{}",c == b);//true
}
2.2.2 访问数组元素
①、通过下标访问
初始下标是0
//数组:访问元素
fn array_test2(){
let a = [1,2,3,4];
println!("a[0]={}",a[0]);
println!("a[1]={}",a[1]);
println!("a[2]={}",a[2]);
}
②、通过 get() 方法
注意返回值是 Option
//数组:访问元素
fn array_test3(){
let a = [1,2,3,4];
let first = a.get(0);
let last = a.get(4);
println!("{:?}",first);//Some(1)
println!("{:?}",last);//None
}
2.2.3 数组越界访问异常
如果声明的数组有4个,但是访问下标大于或等于4,编译时就会抛出异常。
//数组:访问元素
fn array_test3(){
let a = [1,2,3,4];
println!("a[4]={}",a[4]);
}

可以想一下,为什么 rust 在编译期间就能报错?
就是因为我们前面说过 数组的长度是确定的,Rust在编译时就分析并提取了这个数组类型占用空间长度的信息,因此直接阻止了你的越界访问。
2.2.4 避免数组越界程序崩溃
如果我们不确定读取数组的索引是否合法,上面通过索引的方式访问就会发生异常,导致程序奔溃。
为了避免这种情况,我们可以使用 get(index) 的方法来获取数组中的元素,其返回值是 Option
//数组:访问元素
fn array_test3(){
let a = [1,2,3,4];
let first = a.get(0);
let last = a.get(4);
println!("{:?}",first);//Some(1)
println!("{:?}",last);//None
}
2.3 结构体(struct)
结构体和元祖类似,都可以把多个类型组合到一起,作为新的类型。
结构体又可以分为三种具体类型:
// 具名结构体
struct Name_Struct {
x : f32,
y : f32,
}
// 元祖结构体
struct Tuple_Struct(f32,f32);
// 单元结构体
struct Unit_Struct;
2.3.1 具名结构体
//结构体
fn struct_test1(){
struct Point{
x : i32,
y : i32,
}
let p = Point{x:0,y:0};
println!("{},{}",p.x,p.y);
}
①、每个元素之间采用逗号分开,最后一个逗号可以省略不写。
②、类型依旧跟在冒号后面,但是不能使用自动类型推导功能,必须显示指定。
局部变量和结构体变量一致,可以省略掉重复的冒号初始化
//局部变量和结构体变量一致,可以省略掉重复的冒号初始化
fn struct_test2(){
struct Point{
x : i32,
y : i32,
}
let x = 10;
let y = 20;
let p = Point{x,y};
println!("{},{}",p.x,p.y);
}
2.3.2 元祖结构体tuple struct
这是前面介绍的 tuple 和 struct 两种类型的混合,tuple struct 结构有名字,但是成员没有名字。
名字加圆括号,类型有单独的名字,成员没有单独的名字。
fn tuple_struct(){
struct Color (
i32,
i32,
i32
);
}
访问方法
通过下标访问:
fn tuple_struct(){
struct Color (
i32,
i32,
i32
);
let v1 = Color(1,2,3);
println!("{},{},{}",v1.0,v1.1,v1.2)
}
2.3.3 单元结构体
// 单元结构体
struct Unit_Struct;
单元结构体不会占用任何内存空间。
3、枚举 enum
如果说 tuple、struct、tuple struct 在 Rust 中代表的是多个类型的“与”关系,那么 enurn类型在 Rust 中代表的就是多个类型的“或”关系。
Rust 的 enum 中的每个元素的定义语法与 struct 的定义语法类似。可以像空结构体一样,不指定它的类型;也可以像 tuple struct 一样,用圆括号加无名成员;还可以像正常结构体一样,用大括号加带名字的成员。
fn main() {
let x = enum_define::Int(12);
let y = enum_define::Float(3.2);
let z = enum_define::Move {x:1,y:2};
let k = enum_define::Color(255,255,255);
match x {
enum_define::Int(i) => {
println!("{}",i);
},
enum_define::Float(f) => {
println!("{}",f);
},
enum_define::Move{x,y} => {
println!("{} {}",x,y);
},
enum_define::Color(x,y,z) => {
println!("{}{}{}",x,y,z);
}
}
}
enum enum_define{
Int(i32),
Float(f32),
Move{x:i32,y:i32},
Color(i32,i32,i32),
}
可以看到枚举也是一种复合数据类型,但是与结构体不同,结构体类型是里面的所有字段(所有类型)同时起作用,来产生一个具体的实例,而枚举类型是其中的一个变体起作用,来产生一个具体实例。
4、特殊数据类型
4.1 Never 类型
表示不可能返回值的数据类型。
①、类型理论中,叫做底类型,底类型不包含任何值,但它可以合一到任何其它类型;
②、Never 类型用感叹号“!" 表示;
③、目前还未稳定,但是rust内部已经开始用了。
5、如何选择数据类型
选择合适的数据类型是编程中的关键决策之一,它涉及到正确表达数据、优化内存使用和确保代码的正确性。
下面是一些指导原则,可以帮助我们选择合适的数据类型:
- 理解数据的含义和特性:在选择数据类型之前,要充分理解数据的含义、范围和操作。考虑数据的值是否有负数、是否需要支持小数点、是否具有固定的长度等特性。这将有助于缩小选择的范围。
- 选择最小且最符合需求的类型:在数据类型中,应该选择最小的类型,以便节省内存空间和提高性能。如果数据的范围较小,可以选择较小的整数类型(例如
u8或i8)而不是更大的整数类型。同时,要确保所选类型可以容纳数据的所有可能值。 - 平衡灵活性和类型安全:Rust 强调类型安全,但也要考虑代码的灵活性和易用性。选择数据类型时,需要在类型安全和灵活性之间进行权衡。更具体地说,要避免使用过于宽泛的类型(如使用
i32来表示只可能是 0 或 1 的布尔值),以免引入潜在的错误。 - 考虑数据的可变性:如果数据需要在使用过程中进行修改,应该选择可变的数据类型,如使用
mut关键字声明可变变量或使用可变的数据结构(如Vec或HashMap)。 - 利用复合数据类型:Rust 提供了丰富的复合数据类型,如结构体和枚举。如果数据具有复杂的结构或多个变体,考虑使用结构体或枚举来组织数据,以提高代码的可读性和可维护性。
- 考虑上下文和需求:选择数据类型时,要考虑当前的上下文和问题的需求。例如,如果需要高性能的数值计算,可能需要选择使用专门的数值库。如果需要跨线程通信,可能需要选择具备线程安全性的数据类型。
- 进行测试和验证:选择数据类型后,要进行充分的测试和验证,确保所选类型能够正确地表达数据和满足预期的操作。
最重要的是要记住,选择数据类型是一个根据具体情况进行的决策,并且可能随着代码的演进而需要进行调整和优化。
6、常见错误
6.1 类型转换必须通过 as 关键字显式声明
//类型转换必须通过 as 关键字显式声明
fn switch_test(){
let var1 : i8 = 1;
let var2 : i32 = var1;
}
报错如下:

增加 as 关键字显示声明即可。
//类型转换必须通过 as 关键字显式声明
fn switch_test(){
let var1 : i8 = 1;
let var2 : i32 = var1 as i32;
}
6.2 复合数据类型允许递归,但是不允许直接嵌套
//复合数据类型不允许直接嵌套
fn recursive(){
struct recur {
data : i32,
rec : recur
}
}
报错如下:

浙公网安备 33010602011771号