Skip to main content

变量和数据类型

变量和可变性

Rust的变量不同于其他编程语言的变量,其本质上是一种绑定语义,即将一个变量名与一个值绑定在一起。变量名和值建立关联关系。

变量声明

使用let关键字声明,先声明后使用。

let x: i8 = 1;
let x = 1; //等价于:let x:i32 =1;

变量声明以let关键字开头,x为变量名,变量名后紧跟冒号和数据类型。Rust编译器具有变量类型的自动推导功能。在可以根据赋值类型或上下文信息推导出变量类型的情况下,冒号和数据类型可以省略。

变量名必须由字母、数字、下划线组成,字母区分大小写且不能以数字开头,也不能只有下划线。Rust中下划线是一种特殊的标识符,其含义是“忽略这个变量”,后续章节中会用到这个标识符。

变量的可变性

let声明的变量默认是不可变的,在第一次赋值后不能通过再次赋值来改变它的值,即声明的变量是只读状态。

1  fn main() {
2 let x = 3;
3 x = 5; // error
4 println!("x: {}", x);
5 }

如果尝试对不可变变量x进行二次赋值,即进行写操作,这是不被允许的。编译代码,将会抛出cannot assign twice to immutable variable x的错误提示。根据错误提示信息可知,不能对不可变变量进行二次赋值。

变量遮蔽

Rust允许在同一个代码块中声明一个与之前已声明变量同名的新变量,新变量会遮蔽之前的变量,即无法再去访问前一个同名的变量,这样就实现了变量遮蔽。

 1  fn main() {
2 let x = 3;
3 let x = x + 2; // 第一个变量被遮蔽, x = 5
4 let x = x * 2; // 第二个变量被遮蔽, x = 10
5 println!("x: {}", x);
6
7 let x = "Hello, Rust!"; //变量x已由i32类型变为&str类型
8 println!("x: {}", x);
9 }
10
11 // x: 10
12 // x: Hello, Rust!

变量遮蔽的实质是通过let关键字声明了一个新的变量,只是名称恰巧与前一个变量名相同而已,但它们是两个完全不同的变量,处于不同的内存空间,值可以不同,值的类型也可以不同。

常量

常量是指绑定到一个标识符且不允许改变的值,一旦定义后就没有任何方法能改变其值了。Rust声明常量的语法如下:

const MAX_NUM: u32 = 1024;

Rust使用const关键字来声明常量。常量名通常是大写字母,且必须指定常量的数据类型。

常量与不可变变量的区别主要在于:

  1. 常量声明使用const关键字,且必须注明值的类型。
  2. 通过变量遮蔽的方式可以让不可变变量的值改变(本质上是新的变量,只是同名而已)。但是,常量不能遮蔽,不能重复定义。
  3. 常量可以在任何作用域中声明,包括全局作用域。在声明它的作用域中,常量在整个程序生命周期内都有效,这使得常量可以作为多处代码共同使用的全局范围的值。
  4. 常量只能被赋值为常量表达式或数学表达式,不能是函数返回值,或是其他在运行时才能确定的值。

基本数据类型

整数类型

整数是指没有小数部分的数字,整数可以分为有符号整数和无符号整数。按照存储字节大小,整数类型可以进一步分为1字节、

2字节、4字节、8字节、16字节(1字节=8位)。Rust默认的整数类型是i32,isize和usize主要作为数组或集合的索引类型使用,其长度依赖于运行程序的计算机系统。在64位计算机系统上,其长度是64位;在32位计算机系统上,其长度是32位。

长度有符号无符号
8位i8u8
16位i16u16
32位i32u32
64位i64u64
128位i128u128
archisizeusize

整数类型声明,前缀0b0o0x表示二进制、八进制和十六进制的数字。

let integer1:u32 = 17;  // 类型声明
let integer2 = 17u32; // 类型后缀声明
let integer3 = 17; // 类型推断,默认i32
let integer4:u32 = 0b10101; // 二进制
let integer5:u32 = 0o21; // 八进制
let integer6:u32 = 0x11; // 十六进制
let integer7:u32 = 1_000_000; // 下划线分隔数字
let integer8:u32 = 1_000_000u32; // 下划线分隔数字,类型后缀

浮点数类型

浮点数是指带小数点的数字,比如0.0、1.0、-2.1、9.99999等。按照存储大小,浮点数分为f32f64两类。Rust默认的浮点数类型是f64

f32:单精度浮点数,小数点后至少有6位有效数字。

f64:双精度浮点数,小数点后至少有15位有效数字。

// 浮点数类型
// 浮点数类型有两种,分别是:f32, f64
// 32位浮点数:f32
// 64位浮点数:f64
let float1:f32 = 3.14; // 类型声明
let float2 = 3.14f32; // 类型后缀声明
let float3 = 3.14; // 类型推断,默认f64
let float4:f32 = 3.14e2; // 科学计数法
ler float5 = 11_000.555_0001; // 下划线分隔数字

布尔类型

Rust使用bool来声明布尔类型的变量,声明的语法如下所示。布尔类型只有两个可能的取值,即true或false,一般用于逻辑表达式中。

1  let t: bool = true;           // 显式类型声明
2 let f = false; // 隐式类型声明

字符类型

Rust使用UTF-8作为底层的编码。字符类型代表的是一个Unicode标量值(Unicode Scalar Value),包括数字、字母、Unicode和其他特殊字符。每个字符占4个字节。字符类型char由单引号来定义,其声明语法如下所示。

let a = 'a';      // 正确
let b = "a"; // 错误,这是字符串切片 &str

转义字符

let newline = '\n';  // 换行
let tab = '\t'; // 制表符
let quote = '\''; // 单引号

Unicode 码点

let hex_char = '\x2A';       // ASCII 字符 '*'(等价于 '2A')
let unicode_char = '\u{CA0}'; // Unicode 字符 "ಠ"(卡纳达语字符)
  1. 转换为整数

使用 as 运算符将 char 转换为整数类型,但高位可能被截断27:

assert_eq!('A' as u8, 65);     // ASCII 转换
assert_eq!('ಠ' as u16, 0xCA0); // Unicode 转换(截断为 16 位)
  1. 整数转字符

u8 可直接通过 as 转换为 char,其他整数需使用 std::char::from_u32() 并检查有效性27:

let valid_char = std::char::from_u32(0xCA0).unwrap(); // Some('ಠ')
let invalid_char = std::char::from_u32(0xDEAD); // None

范围类型

范围类型常用来生成从一个整数开始到另一个整数结束的整数序列,有左闭右开和全闭两种形式,比如(1..5)是左闭右开区间,表示生成1、2、3、4这4个数字;(1..=5)是全闭区间,表示生成1、2、3、4、5这5个数字。

// 范围类型
print!("(1..5):");
for i in 1..5 {
print!(" {}", i);
}
println!();

print!("(1..=5).rev:");
for i in (1..=5).rev() {
print!(" {}", i);
}
println!();

let sum: i32 = (1..=5).sum(); // 范围求和
println!("sum: {}", sum);

复合数据类型

复合数据类型是由其他类型组合而成的类型。Rust的复合数据类型有元组、数组、结构体、枚举等。

元组类型

一个或多个类型的元素组合成的复合类型,使用小括号()把所有元素放在一起。元素之间使用逗号分隔。元组中的每个元素都有各自的类型,且这些元素的类型可以不同。元组的长度固定,一旦定义就不能再增长或缩短。如果显式指定了元组的数据类型,那么元素的个数必须和数据类型的个数相同。

使用“元组名.索引”来访问元组中相应索引位置的元素,元素的索引从0开始计数。

fn main() {
let t1:(i8, f32, bool) = (1, 2.2, false);
let t2 = (8, 2, (false, 1000));
let t3 = (100, ); // tuple with one element

println!("{}, {}", t1.0, (t2.2).0);
// 1, false

println!("{}", t3.0);
// 100

let (x, y, z) = t1;
println!("x:{}, y:{}, z:{}", x, y, z);
// x:1, y:2.2, z:false
}

数组类型

相同类型的元素组成的复合类型,使用[T;n]表示,T表示元素类型,n表示长度。

声明和初始化有3种方式:

//1.指定数组类型,为每个元素赋初始值。初始值放入中括号“[]”中,之间使用逗号“,”分隔
let arr:[i32:5] = [1, 2, 3, 4, 5];

//2.省略数组类型,自动推断
let arr = [1, 2, 3, 4, 5];

//3.省略数组类型,为所有元素使用默认值初始化。
let arr = [1; 5]; // 等价于:let arr = [1, 1, 1, 1, 1];

使用数组名[索引]来访问数组中相应索引位置的元素,元素的索引从0开始计数。

访问数组元素时最常遇到的问题是索引越界,实际项目开发中,建议使用动态数组VecVec是允许增长和缩短长度的容器类型,其提供的get方法在访问元素时可以有效避免索引越界问题。

结构体类型

结构体是一个自定义数据类型,通过struct关键字加自定义命名,可以把多个类型组合在一起成为新的类型。结构体中以name:type格式定义字段,name是字段名,type是字段类型。

struct Student {
name: String,
age: u8,
}

fn main() {
let student1 = Student {
name: String::from("Alice"),
age: 20,
};

let student2 = Student {
name: String::from("Bob"),
age: 22,
};

println!("Student 1: {} is {} years old.", student1.name, student1.age);
println!("Student 2: {} is {} years old.", student2.name, student2.age);
}

有两种特殊的结构:元组结构体和单元结构体。

元组结构体通过 struct 关键字定义,形如具名元组,字段无名称但有序。例如:

struct Color(i32, i32, i32); // 表示 RGB 颜色
struct Point(i32, i32); // 表示二维坐标

即使两个元组结构体的字段类型完全相同(如 ColorPoint 的字段均为 i32),它们仍会被视为不同的类型。

单元结构体无任何字段的结构体,定义时仅需名称和分号:

struct UnitStruct; // 单元结构体

其内存占用为零(Zero-Sized Type, ZST),编译时会被优化。可以作为类型系统中的标识符,用于泛型约束或 trait 实现。例如,表示算法类型或状态机的不同状态。

枚举类型

通过 enum 关键字定义,其包含若干枚举值,可以使用“枚举名::枚举值”访问枚举值。

无参数的枚举类型。

#[derive(Debug)]
enum ColorNoParam {
Red,
Yellow,
Blue,
}
fn main() {
let color_no_param = ColorNoParam::Red;
match color_no_param {
ColorNoParam::Red => println!("{:?}", ColorNoParam::Red),
ColorNoParam::Yellow => println!("{:?}", ColorNoParam::Yellow),
ColorNoParam::Blue => println!("{:?}", ColorNoParam::Blue),
}
}
// Red

有参数的枚举类型。

enum ColorNoParam {
Red(String),
Yellow(String),
Blue(String),
}
fn main() {
println!("{:?}", ColorNoParam::Red(String::from("red")));
}

容器类型

Rust 标准库 std::collections 提供了 4 类通用容器类型,涵盖 8 种核心数据结构

image-20250330184217543

Vec

Vec是动态可变长数组,动态数组在内存中开辟了一段连续内存块用于存储元素,且只能存储相同类型的元素。

动态数组的创建,有3中方式

// 使用Vec::new函数创建空的动态数组
let mut v2: Vec<i32> = Vec::new();
// 使用Vec::with_capacity函数创建指定容量的动态数组
let mut v2: Vec<i32> = Vec::with_capacity(10);
// 使用Vec!宏创建动态数组,编译器会根据传入的参数类型推导出Vec的类型
let mut v3 = vec![1, 2, 3, 4, 5];
let mut v4 = vec![0;10] // 创建一个长度为10,元素值为0的动态数组

动态数组的修改

let mut v: Vec<i32> = Vec::new();
// 向动态数组中添加元素
v.push(1);
v.push(2);
v.push(3);

println!("v: {:?}", v);
//v: [1, 2, 3]

// 修改元素
v[1] = 10; // 修改索引为1的元素
println!("v: {:?}", v);

删除动态数组中的元素

// 1.使用pop方法删除并返回动态数组的最后一个元素,如果数组为空则返回None。
println!("pop: {:?}", v.pop()); // Some(3)
println!("v: {:?}", v);
// 2.使用remove方法删除并返回指定索引的元素,如果索引越界则panic。
println!("remove: {:?}", v.remove(0)); // 10

其他操作

// 3.使用clear方法清空动态数组,删除所有元素。
v.clear();
println!("v: {:?}", v);
// 4.使用len方法获取动态数组的长度。
println!("len: {:?}", v.len()); // 0
// 5.使用is_empty方法判断动态数组是否为空。
println!("is_empty: {:?}", v.is_empty()); // true
// 6.使用contains方法判断动态数组是否包含指定元素。
v.push(1);
v.push(2);
v.push(3);
println!("contains: {:?}", v.contains(&2)); // true
// 7.使用iter方法获取动态数组的迭代器。
for i in v.iter() {
println!("iter: {:?}", i);
}
// 8.使用iter_mut方法获取动态数组的可变迭代器。
for i in v.iter_mut() {
*i += 1; // 修改元素值
}
println!("v: {:?}", v);
// 9.使用sort方法对动态数组进行排序。
v.sort();
println!("v: {:?}", v);
// 10.使用reverse方法对动态数组进行反转。
v.reverse();
println!("v: {:?}", v);
// 11.使用extend方法将另一个动态数组的元素添加到当前动态数组的末尾。
let mut v5 = vec![4, 5, 6];
v.extend(v5);
println!("v: {:?}", v);
// 12.使用split_at方法将动态数组分割成两个部分,返回一个元组。
let (left, right) = v.split_at(3);
println!("left: {:?}", left);
println!("right: {:?}", right);
// 13.使用join方法将动态数组的元素连接成一个字符串。
let v6 = vec!["hello", "world"];
let s = v6.join(" ");
println!("s: {:?}", s);
// 14.使用clone方法克隆动态数组。
let v7 = v.clone();
println!("v7: {:?}", v7);
// 15.使用as_slice方法将动态数组转换为切片。
let slice = v.as_slice();
println!("slice: {:?}", slice);
// 16.使用as_mut_slice方法将动态数组转换为可变切片。
let mut slice_mut = v.as_mut_slice();
slice_mut[0] = 10; // 修改元素值

动态数组的访问

// 1.使用“实例名[索引]”语法访问指定索引的元素。
let v = vec![1, 2, 3, 4, 5];
println!("v[0]: {:?}", v[1]); // 2
// 2.使用get方法以索引作为参数访问元素
println!("get: {:?}", v.get(1)); // Some(2)

VecDeque

双端队列是一种同时具有栈(先进后出)和队列(先进先出)特征的数据结构,只能在队列两端进行添加或删除元素操作。

创建VecDeque有以下两种方式:

use std::collections::VecDeque;

fn main() {
// 创建vecdeque有两种方式
// 1. 使用VecDeque::new()创建一个空的VecDeque
let mut vec_deque: VecDeque<i32> = VecDeque::new();
// 2. 使用VecDeque::with_capacity()从一个Vec创建一个VecDeque
let mut vec_deque2: VecDeque<i32> = VecDeque::with_capacity(10);

}

访问

use std::collections::VecDeque;

fn main() {
// 创建vecdeque有两种方式
// 1. 使用VecDeque::new()创建一个空的VecDeque
let mut vec_deque: VecDeque<i32> = VecDeque::new();
// 2. 使用VecDeque::with_capacity()从一个Vec创建一个VecDeque
let mut vec_deque2: VecDeque<i32> = VecDeque::with_capacity(10);

// VecDeque的修改
// 1. push_front():在VecDeque的前面添加元素
vec_deque.push_front(1);
// 2. push_back():在VecDeque的后面添加元素
vec_deque.push_back(2);
// 3. pop_front():删除VecDeque的第一个元素
vec_deque.pop_front();
// 4. pop_back():删除VecDeque的最后一个元素
vec_deque.pop_back();
// 5. insert():在VecDeque的指定位置插入元素
vec_deque.insert(0, 3);
// 6. remove():删除VecDeque的指定位置的元素
vec_deque.remove(0);
// 7. clear():清空VecDeque
vec_deque.clear();

// 访问VecDeque的元素
// 1. get():获取VecDeque的指定位置的元素
let first = vec_deque.get(0);
// 2. front():获取VecDeque的第一个元素
let first = vec_deque.front();
// 3. back():获取VecDeque的最后一个元素
let last = vec_deque.back();

//4. 使用 实例名 [索引] 访问元素
let first = vec_deque[0];
}

HashMap

哈希表(HashMap)是基于哈希算法来存储键-值对的集合,其中所有的键必须是同一类型,所有的值也必须是同一类型,不允许有重复的键,但允许不同的键有相同的值。Rust使用HashMap结构体表示哈希表,它定义在标准库的std::collections模块中。使用HashMap结构体之前需要显式导入std::collections::HashMap

  1. HashMap的创建
use std::collections::HashMap;

fn main() {
// 创建
// 1.使用new()方法创建
let mut map:HashMap<String, i32> = HashMap::new();
// 2.使用HashMap::with_capacity()方法创建
let mut map2:HashMap<String, i32> = HashMap::with_capacity(10);

}
  1. HashMap的修改

修改HashMap的常见操作有插入/更新键-值对、只在键没有对应值时插入键-值对、以新旧两值的计算结果来更新键-值对和删除键-值对。

1)使用insert方法在HashMap中插入或更新键-值对。如果键不存在,执行插入操作并返回None。如果键已存在,执行更新操作,将对应键的值更新并返回旧值。

use std::collections::HashMap;

fn main() {
let mut map: HashMap<String, i32> = HashMap::new();
// 插入数据
let zhangsan1 = map.insert("zhangsan".to_string(), 18);
map.insert("lisi".to_string(), 20);

println!("{:?}", zhangsan1);
println!("{:?}", map);

let zhangsan2 = map.insert("zhangsan".to_string(), 20);
println!("{:?}", zhangsan2);
println!("{:?}", map);
}
// 运行结果
// None
// {"lisi": 20, "zhangsan": 18}
// Some(18)
// {"lisi": 20, "zhangsan": 20}

2)使用entry和or_insert方法检查键是否有对应值,没有对应值就插入键-值对,已有对应值则不执行任何操作。entry方法以键为参数,返回值是一个枚举类型Entry。Entry类型的or_insert方法以值为参数,在键有对应值时不执行任何操作。在键没有对应值时,将键与值组成键-值对插入HashMap。

use std::collections::HashMap;

fn main() {
// 使用entry方法插入键-值对
let mut map:HashMap<String, i32> = HashMap::new();
map.entry("zhangsan".to_string()).or_insert(18);
map.entry("lisi".to_string()).or_insert(20);
println!("{:?}", map);
// 使用entry方法更新键-值对
map.entry("zhangsan".to_string()).or_insert(2022);
println!("{:?}", map);
// {"lisi": 20, "zhangsan": 18}
// {"lisi": 20, "zhangsan": 18}
}

3)以新旧两值的计算结果来更新键-值对是指找到一个键对应值,结合新旧两值进行某些计算处理,以计算结果来更新键对应值。比如,老师发现本次考试试卷上出现了一道错题,决定为所有学生的分数都加上2分,那么就可以将每个学生的名字作为键,将对应分数加上2。

use std::collections::HashMap;

fn main() {
let mut map: HashMap<&str, i32> = HashMap::new();

map.insert("zhangsan", 97);
map.insert("lisi", 55);
map.insert("wangwu", 84);
println!("map: {:?}", map);

// 遍历整个map
for (_, value) in map.iter_mut() {
// 这里的value是可变引用, 实现所有value + 2
*value += 2;
}
println!("map: {:?}", map);
}
// map: {"zhangsan": 97, "wangwu": 84, "lisi": 55}
// map: {"zhangsan": 99, "wangwu": 86, "lisi": 57}

4)使用remove方法删除并返回指定的键-值对,如果不存在就返回None。

 1  use std::collections::HashMap;
2
3 fn main() {
4 let mut map: HashMap<&str, i32> = HashMap::new();
5
6 map.insert("zhangsan", 97);
7 map.insert("lisi", 86);
8 map.insert("wangwu", 55);
9 println!("{:?}", map);
10
11 let result = map.remove("wangwu");
12 println!("{:?}", map);
13 println!("{:?}", result);
14 }
15
16 // {"wangwu": 55, "lisi": 86, "zhangsan": 97}
17 // {"lisi": 86, "zhangsan": 97}
18 // Some(55)
  1. HashMap的访问

1)使用“实例名[键]”语法访问指定的键-值对。如果键不存在,将会导致程序错误。

 1  use std::collections::HashMap;
2
3 fn main() {
4 let mut map: HashMap<&str, i32> = HashMap::new();
5 map.insert("zhangsan", 97);
6
7 println!("zhangsan: {}", map["zhangsan"]);
8 // println!("wangwu: {}", map["wangwu"]);
9 }
10
11 // zhangsan: 97

2)使用get方法以键为参数访问指定的键-值对。如果键不存在,将会返回None.

 1  use std::collections::HashMap;
2
3 fn main() {
4 let mut map: HashMap<&str, i32> = HashMap::new();
5 map.insert("zhangsan", 97);
6
7 println!("zhangsan: {:?}", map.get("zhangsan"));
8 println!("wangwu: {:?}", map.get("wangwu"));
9 }
10
11 // zhangsan: Some(97)
12 // wangwu: None

字符串

字符串的本质是一种特殊的容器类型,是由零个或多个字符组成的有限序列。

字符串的创建

Rust常用的字符串有两种,一种是固定长度的字符串字面量str,另一种是可变长度的字符串对象String

  1. &str创建

Rust内置的字符串类型是str,它通常以引用的形式&str出现。字符串字面量&str是字符的集合,代表的是不可变的UTF-8编码的字符串的引用,创建后无法再为其追加内容或更改内容。

1)使用双引号创建字符串字面量

let s1 = "hello rust!";

2)使用as_str方法将字符串对象转换为字符串字面量

let str = String::from("Hello, Rust!");
let s2 = str.as_str();
  1. String的创建

字符串对象String是由Rust标准库提供的、拥有所有权的UTF-8编码的字符串类型,创建后可以为其追加内容或更改内容。String类型的本质是一个字段为Vec<u8>类型的结构体,它把字符内容存放在堆上,由指向堆上字节序列的指针(as_ptr方法)、记录堆上字节序列的长度(len方法)和堆分配的容量(capacity方法)3部分组成。

1)使用String::new函数创建空的字符串对象

let mut s1 = String::new();

2)使用String::from函数根据指定的字符串字面量创建字符串对象

let mut s2 = String::from("生而为人");

3)使用to_string方法将字符串字面值转换为字符串对象

let str = "你要开心";
let s = str.to_string();

字符串的修改

String类型字符串常见的修改操作有追加、插入、连接、替换和删除等。

1)使用push方法在字符串后追加字符,使用push_str方法在字符串后追加字符串字面量。这两个方法都是在原字符串上追加,并不会返回新的字符串。

要在字符串后追加字符,该字符串必须是可变的。

fn main() {
let mut s = String::from("hello");
s.push(',');
s.push_str(" world!");
println!("{}", s);
}
// hello, world!