NexaGrid技术博客

NexaGrid技术博客

一、Rust从基础到实战应用(变量与数据类型)

85
2025-06-23
一、Rust从基础到实战应用(变量与数据类型)

Rust 作为一门系统级编程语言,以其内存安全和高性能著称。而变量与数据类型作为编程的基础,是我们迈入 Rust 编程世界的重要基石。本文将详细介绍 Rust 中变量的定义、特性以及丰富的数据类型。

1. 变量

1.1 定义变量(变量的可变性)

在Rust中与常见的一些编程语言不同的是,Rust默认所有变量是不可变的,所以当变量需要更新时,需要对变量使用 mut 可变声明。

变量声明的语法如下:

let x: i8 = 1; 
let x = 1; // 等价于 let x: i32 = 1;
// x = 2; // 如果取消注释, 这步编译器将会报错

let mut x = 1; // 此时x是可变的
x = 2;

1.2 变量遮蔽

在同一个代码块(作用域)中新声明的同名变量将会遮蔽之前的变量,无法再访问之前同名的变量。

1.2.1 不同数据类型的变量遮蔽

在 Rust 里,变量遮蔽允许新变量覆盖旧变量,并且新变量可以是不同的数据类型。

fn main() {
    // 定义一个整型变量
    let width = 5;
    println!("第一次定义的 width 是整数,值为: {}", width);

    // 变量遮蔽,将 width 重新定义为字符串类型
    let width = "five";
    println!("被遮蔽后的 width 是字符串,值为: {}", width);
}

解释:首先定义了一个整型变量 width,值为 5。之后通过 let 关键字再次定义 width,这次将其定义为字符串类型 "five",旧的 width 变量被遮蔽。

1.2.2 函数参数中的变量遮蔽

在函数内部,参数也可以被函数体里新定义的变量遮蔽。

fn calculate_area(width: u32, height: u32) {
    // 这里 width 是函数参数
    println!("传入的 width 参数值为: {}", width);

    // 变量遮蔽,在函数内部重新定义 width
    let width = width * 2;
    println!("被遮蔽后的 width 值为: {}", width);

    let area = width * height;
    println!("计算得到的面积是: {}", area);
}

fn main() {
    calculate_area(5, 10);
}

解释calculate_area 函数接收两个参数 widthheight。在函数体中,使用 let 关键字重新定义 width,新的 width 是原来 width 的两倍,原参数 width 被遮蔽。

1.2.3 循环中的变量遮蔽

在循环里,也能够使用变量遮蔽。

fn main() {
    let numbers = [1, 2, 3, 4, 5];
    for number in numbers.iter() {
        // 这里 number 是循环迭代变量
        println!("当前迭代的 number 值为: {}", number);

        // 变量遮蔽,重新定义 number
        let number = *number + 1;
        println!("被遮蔽后的 number 值为: {}", number);
    }
}

解释:在 for 循环中,number 是迭代数组 numbers 得到的元素。在循环体中,使用 let 关键字重新定义 number,新的 number 是原来 number1 后的结果,原迭代变量 number 被遮蔽。

1.2.4 块作用域中的变量遮蔽

在不同的块作用域中,变量遮蔽同样可行。

fn main() {
    let value = 10;
    println!("块作用域外的 value 值为: {}", value);

    {
        // 变量遮蔽,在块作用域内重新定义 value
        let value = "ten";
        println!("块作用域内被遮蔽后的 value 值为: {}", value);
    }

    println!("离开块作用域后,value 恢复为原来的值: {}", value);
}

解释:在 main 函数中先定义了一个整型变量 value。之后在一个新的块作用域里,使用 let 关键字重新定义 value 为字符串类型 "ten",原 value 变量在该块作用域内被遮蔽。离开块作用域后,原 value 变量恢复作用。

2. 常用数据类型

2.1 整型

整数是没有小数部分的数字,比如0,-1,1,9999等。整型可以分有符号无符号 两种,有符号则表示可以存储负数,无符号则只能存储正数。

按照存储数据的大小,整型可以进一步分为:

长度

有符号

无符号

8

i8

u8

16

i16

u16

32

i32

u32

64

i64

u64

128

i128

u128

arch(视当前机器CPU架构而定)

isize

usize

下面介绍整型变量声明语法:

let integer = 17;  // 默认是i32类型
let integer: u32 = 17;  // 类型显式声明
let integer: 17u32; // 类型后缀声明
let integer: u32 = 0b10001; // 二进制
let integer: u32 = 0o21; // 八进制
let integer: u32 = 0x11; // 十六进制
let integer = 100_000; // 数字可读性分隔符_, 编译器会自动去除_

2.2 浮点数类型

浮点数用于表示带有小数部分的数字。Rust提供了两种浮点数类型,遵循IEEE754标准。

长度

符号

32

f32

64

f64

浮点数的声明语法如下:

let float = 1.0; // 默认是f64类型, 因为现代计算机64位和32位计算性能相近,精度更高。可以减少累计误差。
let float: f32 = 1.0; // 类型显式声明
let float = 1.0f32; // 类型后缀声明
let float: f64 = 1.0 // 类型显式声明
let float = 1.0f64; // 类型后缀声明
注意浮点数精度陷阱:由于浮点数实质是二进制近似的结果,所以当浮点数运算时,要时刻注意精度问题,或者采用专门的定点数库或者十进制算数库(rust_decimal)

2.3 布尔类型

布尔类型,有两种值: truefalse

布尔类型的声明语法如下:

let val_true = true;
let val_false = false;

2.4 字符(Char)类型

Rust中的字符类型内部存储的是unicode字符,字符类型使用4字节大小存储字符。Rust使' 包裹字符。

字符的声明语法如下:

let c = 'c';

2.5 范围类型

Rust使用 .. 表示左闭右开区间,使用 ..= 表示闭区间。

let range = 1..5;

for i in range {
  println!("i: {}", i);
}

// i: 1
// i: 2
// i: 3
// i: 4

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

// i: 1
// i: 2
// i: 3
// i: 4
// i: 5

2.6 复合数据类型

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

2.6.1 元组类型

  • 元组类型是由一个或多个类型的元素组合成的复合类型,使用小括号“()”把所有元素放在一起。元素之间使用逗号“,”分隔。

  • 元组中的每个元素都有各自的类型,且这些元素的类型可以不同。

  • 元组的长度固定,一旦定义就不能再增长或缩短。

fn main() {
    let tup:(i32, f32, bool) = (1, 3.14, true); //显式声明元组类型
    println!("索引: {} {} {}", tup.0, tup.1, tup.2); //用点进行索引访问

    let (x, y, z) = tup;  // 元组解构,将对应位置的元素分别赋值给变量x, y, z
    println!("元组解构: x:{} y:{} z:{}", x, y, z);

    let con_tup = (1, 2.1, (false, 3.24)); //嵌套元组
    println!("嵌套元组: {} {} ({}, {})", con_tup.0, con_tup.1, con_tup.2.0, con_tup.2.1);

    let (x, y, (z, w)) = con_tup; //嵌套元组解构
    println!("嵌套元组解构: x:{} y:{} z:{} w:{}", x, y, z, w);
}

2.6.2 数组类型

数组类型是由相同类型的元素组合成的复合类型,我们可以使用[T; n]表示,T代表元素类型,n代表长度即元素个数。

隐式声明时,编译器会自动推导变量类型。

  • 数组有如下3种声明和初始化方式:

fn main(){
    //1.显式声明:i32类型的数组, 长度为10;进行初始化为1-10
    let arr:[i32; 10] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    //rust中 println!宏的参数: {:?} 可以打印debug的复合类型数据,比如数组、元组、结构体等
    println!("数组: {:?}", arr); //打印数组

    //2.隐式声明:i32类型的数组, 长度为10;进行初始化为1-10
    let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    println!("数组: {:?}", arr);

    //3.省略数组类型,为所有元素使用相同的值进行初始化。10个元素值都是1
    let arr = [1; 10];
    println!("数组: {:?}", arr);
}
2.6.2.1 向量Vec
  • 动态大小: 向量可以在运行时动态调整容量,无需预先知道元素数量。

  • 连续内存存储:元素在内存中连续存储,支持高效访问和遍历。

  • 泛型支持:通过 Vec<T> 可任意存储<T>元素。

fn main() {
    let mut array: Vec<i64> = Vec::with_capacity(10); // 指定长度
    let mut array: Vec<i64> = Vec::new();
    
    let mut array: Vec<i64> = vec![]; // 宏命令声明
    let mut array = vec![1;10]; 
    
    println!("x: {}", array[1]); // x: 1
    array[1] = 2;
    println!("x: {}", array[1]); // x: 2
}

2.6.3 结构体类型

struct User {
  username: &'static str,
  password: &'static str,
  is_enabled: bool, // 最后一个字段可以忽略,
}

impl User {
  // 关联函数
  fn validate_password(&self, input_password: &str) -> bool {
    return self.password == input_password
  }
}

fn main() {
    let user = User {
        username: "test",
        password: "test",
        is_enabled: true
    };
    
    println!("validation: {}", user.validate_password("test"));
}

// validation: true
2.6.3.1 元组结构体

元组结构体是指没有命名字段的结构体。有时当类似Color, Position这类常识的数据结构,采用元组结构体方式声明可以大大减少定义字段的开发时间。

struct Color(u8, u8, u8);
let red = Color(255, 0, 0);
2.6.3.2 单元结构体

单元结构体是没有任何字段的结构体。当我们需要一个确定的、不需要数据的值时,可以使用单元结构体完成。

struct Empty;
let empty = Empty;

2.6.4 枚举类型

// 声明枚举类型
enum Direction {
    North,
    East,
    South,
    West,
}

// 使用枚举
let direction = Direction::North;

// 携带数据的枚举
enum IPAddress {
  IPV4(String),
  IPV6(String)
}

2.7 容器类型

collections标准库中实现了以下八种数据类型:

线性序列

Vec<T>

有序存储可变长数组

VecDeque<T>

有序存储可变长双端队列

LinkedList<T>

随机存储双向链表

键值对

HashMap<K, V>

基于哈希表的无序键值对

BTreeMap<K, V>

基于B树的有序键值对,按Key排序

集合

HashSet<T>

基于哈希表的无序集合

BTreeSet<T>

基于B树的有序集合

优先队列

BinaryHeap<T>

基于二叉堆的优先队列

2.7.1 线性序列类型

在 Rust 中,线性序列类型是一类按顺序存储多个元素的容器。以下是三种主要的线性序列类型及其详细介绍:

  • 优先用 Vec:如果主要是随机访问或尾部操作。

  • 优先用 VecDeque:如果需要双端操作(如队列、栈)或少量随机访问。

  • 优先用 LinkedList:如果需要频繁在任意位置插入 / 删除,但不关心随机访问(如实现链表、图的邻接表)。

操作

Vec<T>

VecDeque<T>

LinkedList<T>

随机访问

O (1)(最快)

O (1)(稍慢)

O (n)(最慢)

尾部插入(push_back)

O (1)(可能重分配)

O(1)

O(1)

头部插入(push_front)

O (n)(需移动元素)

O (1)(最快)

O(1)

中间插入 / 删除

O (n)(需移动元素)

O (n)(需移动元素)

O (1)(最快,需先定位)

内存局部性

高(连续存储)

中(环形缓冲区)

低(分散存储

2.7.1.1 Vec<T>(有序存储可变长数组
特点
  • 连续内存存储:元素在内存中连续排列,支持高效随机访问(O (1))。

  • 动态增长:可通过pushextend等方法动态调整大小。

  • 尾部操作高效:在尾部添加 / 删除元素(push/pop)的时间复杂度为 O (1)。

使用场景
  • 需要随机访问元素(如通过索引访问)。

  • 主要在尾部进行插入 / 删除操作。

  • 追求内存局部性以提高缓存命中率。

常用方法
  • push(value)/pop():尾部添加 / 删除元素。

  • insert(index, value)/remove(index):指定位置插入 / 删除元素。

  • get(index):安全访问元素(返回Option<&T>)。

  • len()/is_empty():获取长度 / 检查是否为空。

  • resize(new_len, value):调整 Vec 大小。

使用示例如下:

fn main() {
    // 创建Vec
    let mut v = Vec::new();
    v.push(1);
    v.push(2);
    v.push(3);
    
    // 随机访问
    println!("第一个元素: {}", v[0]); // 输出: 1
    
    // 遍历
    for elem in &v {
        println!("元素: {}", elem);
    }
    
    // 删除尾部元素
    let last = v.pop(); // Some(3)
    
    // 插入元素(可能导致重新分配内存)
    v.insert(1, 4); // 在索引1处插入4
}
2.7.1.2 VecDeque<T>(有序存储可变长双端队列
特点
  • 环形缓冲区:基于动态数组实现,支持高效的头部和尾部操作。

  • 双端操作:在头部和尾部插入 / 删除元素的时间复杂度均为 O (1)。

  • 随机访问:支持通过索引访问元素(O (1)),但性能略低于 Vec。

使用场景
  • 需要同时在头部和尾部高效插入 / 删除元素(如队列、栈)。

  • 需要随机访问,但操作频率低于 Vec。

常用方法
  • push_front(value)/pop_front():头部添加 / 删除元素。

  • push_back(value)/pop_back():尾部添加 / 删除元素。

  • get(index):通过索引访问元素。

  • rotate_left(n)/rotate_right(n):元素循环移动。

use std::collections::VecDeque;

fn main() {
    // 创建VecDeque
    let mut deque = VecDeque::new();
    deque.push_back(1); // 尾部添加
    deque.push_front(0); // 头部添加
    
    // 随机访问
    println!("第一个元素: {}", deque[0]); // 输出: 0
    
    // 从头部删除
    let first = deque.pop_front(); // Some(0)
    
    // 从尾部删除
    let last = deque.pop_back(); // Some(1)
}
2.7.1.3 LinkedList<T>(随机存储双向链表
特点
  • 链式存储:元素通过节点(Node)连接,每个节点包含元素值和前后指针。

  • 高效插入删除:在任意位置插入或删除元素的时间复杂度为 O (1)(需先定位到节点)。

  • 不支持随机访问:访问元素需从头 / 尾遍历(O (n))。

使用场景
  • 需要频繁在中间插入或删除元素。

  • 不需要随机访问,只需要顺序遍历。

  • 不关心内存碎片化(节点分散存储)。

常用方法
  • push_front(value)/pop_front():头部添加 / 删除元素。

  • push_back(value)/pop_back():尾部添加 / 删除元素。

  • iter():获取迭代器(顺序遍历)。

  • split_off(index):从指定位置分割链表。

use std::collections::LinkedList;

fn main() {
    // 创建LinkedList
    let mut list = LinkedList::new();
    list.push_back(1); // 尾部添加
    list.push_front(0); // 头部添加
    
    // 遍历
    for elem in &list {
        println!("元素: {}", elem);
    }
    
    // 删除头部元素
    let first = list.pop_front(); // Some(0)
    
    // 在尾部追加另一个LinkedList
    let mut another = LinkedList::new();
    another.push_back(3);
    list.append(&mut another);
}

2.7.2 键值对类型

在 Rust 中,HashMapBTreeMap是两种常用的键值对容器,它们分别基于哈希表和 B 树实现,适用于不同的场景。以下是对这两种容器类型的详细介绍:

特性

HashMap

BTreeMap

底层结构

哈希表

B 树

元素顺序

无序

按键排序(升序)

时间复杂度

O (1)(平均)

O(log n)

适用场景

快速查找、不关心顺序

需要有序遍历或范围查询

键的要求

实现HashEq

实现Ord(必须可排序)

选择建议
  • 优先用HashMap:如果不需要顺序,且键的哈希性能良好(如整数、字符串)。

  • BTreeMap:如果需要按键排序或范围查询(如数据库索引、配置文件)。

2.7.2.1 HashMap<K, V> 基于哈希表的无序键值对
特点
  • 基于哈希表:通过哈希函数将键映射到存储位置,支持平均 O (1) 时间复杂度的插入、查询和删除操作。

  • 无序存储:元素的顺序不固定,取决于哈希值和插入顺序。

  • 键唯一性:每个键只能出现一次,重复插入会覆盖旧值。

  • 哈希函数:键类型K必须实现HashEq trait。

常用方法
  • insert(key, value):插入或更新键值对。

  • get(key):返回键对应值的引用(Option<&V>)。

  • remove(key):删除键值对。

  • contains_key(key):检查键是否存在。

  • len():返回元素数量。

  • clear():清空 Map。

使用示例如下:

use std::collections::HashMap;

fn main() {
    // 创建HashMap
    let mut map = HashMap::new();
    
    // 插入元素
    map.insert("apple", 1);
    map.insert("banana", 2);
    map.insert("cherry", 3);
    
    // 访问元素
    if let Some(count) = map.get("apple") {
        println!("苹果的数量是: {}", count); // 输出: 苹果的数量是: 1
    }
    
    // 遍历元素(顺序不确定)
    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
    
    // 删除元素
    map.remove("banana");
    
    // 检查键是否存在
    println!("是否有香蕉? {}", map.contains_key("banana")); // 输出: false
}
2.7.2.2 BTreeMap<K, V> 基于B树的有序键值对
特点
  • 基于 B 树:通过 B 树结构维护键值对,支持 O (log n) 时间复杂度的插入、查询和删除操作。

  • 按键排序:元素始终按键的顺序存储(实现了Ord trait 的键)。

  • 键唯一性:与HashMap相同,每个键只能出现一次。

  • 有序遍历:遍历时按键的顺序返回元素。

使用场景
  • 需要按键的顺序遍历元素(如范围查询)。

  • 键的类型实现了Ord trait(如整数、字符串)。

  • 对元素顺序有要求(如按字母排序的字典)。

使用示例如下:

use std::collections::BTreeMap;

fn main() {
    // 创建BTreeMap
    let mut map = BTreeMap::new();
    
    // 插入元素(顺序不重要)
    map.insert(3, "cherry");
    map.insert(1, "apple");
    map.insert(2, "banana");
    
    // 遍历元素(按键升序排列)
    for (key, value) in &map {
        println!("{}: {}", key, value);
    }
    // 输出:
    // 1: apple
    // 2: banana
    // 3: cherry
    
    // 范围查询(例如,获取键≥2的元素)
    for (key, value) in map.range(2..) {
        println!("范围查询: {}: {}", key, value);
    }
    // 输出:
    // 范围查询: 2: banana
    // 范围查询: 3: cherry
}

2.7.3 集合类型

在 Rust 中,HashSet<T>BTreeSet<T>是两种常用的集合类型,用于存储唯一元素。它们分别基于哈希表和 B 树实现,提供不同的性能特性和排序保证。以下是对这两种容器的详细介绍:

特性

HashSet<T>

BTreeSet<T>

底层结构

哈希表

B 树

元素顺序

无序

有序(按Ord排序)

时间复杂度

O (1)(平均)

O(log n)

适用场景

快速查找、不关心顺序

需要有序遍历或范围查询

元素要求

实现HashEq

实现Ord(必须可排序)

  • 优先用HashSet:如果不需要顺序,且元素的哈希性能良好(如整数、字符串)。

  • 优先用BTreeSet:如果需要按顺序遍历元素或进行范围查询(如数据库索引、配置文件)。

元素类型要求

  • HashSet的元素必须实现HashEq(如String, i32)。

  • BTreeSet的元素必须实现OrdOrd包含Eq,因此也需实现Hash)。

性能考虑

  • HashSet在大量数据下通常更快,但依赖哈希函数质量。

  • BTreeSet的性能更稳定,适合需要有序性的场景。

2.7.3.1 HashSet<T> 基于哈希表的有序集合
特点
  • 基于哈希表:使用哈希函数确定元素存储位置,支持平均 O (1) 时间复杂度的插入、查询和删除操作。

  • 无序存储:元素的顺序不固定,取决于哈希值和插入顺序。

  • 元素唯一性:每个元素只能出现一次,重复插入会被忽略。

  • 哈希要求:元素类型T必须实现HashEq trait。

使用场景
  • 需要快速检查元素是否存在。

  • 不关心元素的顺序。

  • 需要高效去重。

常用方法
  • insert(value):插入元素(返回bool表示是否新增)。

  • remove(value):删除元素(返回bool表示是否成功)。

  • contains(value):检查元素是否存在。

  • len()/is_empty():获取大小 / 检查是否为空。

  • union()/intersection()/difference():集合操作。

use std::collections::HashSet;

fn main() {
    // 创建HashSet
    let mut set = HashSet::new();
    
    // 插入元素
    set.insert("apple");
    set.insert("banana");
    set.insert("cherry");
    
    // 检查元素是否存在
    println!("是否有apple? {}", set.contains("apple")); // 输出: true
    
    // 遍历元素(顺序不确定)
    for item in &set {
        println!("元素: {}", item);
    }
    
    // 删除元素
    set.remove("banana");
    
    // 集合操作:并集、交集、差集
    let mut other = HashSet::new();
    other.insert("banana");
    other.insert("date");
    
    let union: HashSet<_> = set.union(&other).cloned().collect();
    println!("并集: {:?}", union); // 输出: {"apple", "date", "cherry", "banana"}
}
2.7.3.2 BTreeSet<T> 基于B树的有序集合
特点
  • 基于 B 树:通过 B 树结构维护元素,支持 O (log n) 时间复杂度的插入、查询和删除操作。

  • 有序存储:元素始终按排序顺序存储(实现了Ord trait 的元素)。

  • 元素唯一性:与HashSet相同,每个元素只能出现一次。

  • 有序遍历:遍历时按元素排序返回。

使用场景
  • 需要按顺序遍历元素。

  • 需要范围查询(如获取所有大于某个值的元素)。

  • 元素类型实现了Ord trait(如整数、字符串)。

常用方法
  • HashSet类似,但增加了与顺序相关的功能:

    • range(start..end):返回指定范围的迭代器。

    • first()/last():返回第一个 / 最后一个元素。

    • split_off(value):将大于等于value的元素分离到新集合。

use std::collections::BTreeSet;

fn main() {
    // 创建BTreeSet
    let mut set = BTreeSet::new();
    
    // 插入元素(顺序不重要)
    set.insert(3);
    set.insert(1);
    set.insert(2);
    
    // 遍历元素(按键升序排列)
    for item in &set {
        println!("元素: {}", item);
    }
    // 输出:
    // 元素: 1
    // 元素: 2
    // 元素: 3
    
    // 范围查询(例如,获取≥2的元素)
    for item in set.range(2..) {
        println!("范围查询: {}", item);
    }
    // 输出:
    // 范围查询: 2
    // 范围查询: 3
}

单元类型

() 是单元类型,默认函数返回的就是单元类型。

总结

通过本文,我们学习了 Rust 中变量的可变性、变量遮蔽,以及多种数据类型,包括整型、浮点数类型、布尔类型等基础类型,还有元组、数组、结构体等复合类型,以及各种容器类型。掌握这些知识,将为我们进一步深入学习 Rust 编程打下坚实的基础。