一、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
函数接收两个参数 width
和 height
。在函数体中,使用 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
是原来 number
加 1
后的结果,原迭代变量 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等。整型可以分有符号
和无符号
两种,有符号则表示可以存储负数,无符号则只能存储正数。
按照存储数据的大小,整型可以进一步分为:
下面介绍整型变量声明语法:
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标准。
浮点数的声明语法如下:
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 布尔类型
布尔类型,有两种值: true
和 false
布尔类型的声明语法如下:
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标准库中实现了以下八种数据类型:
2.7.1 线性序列类型
在 Rust 中,线性序列类型是一类按顺序存储多个元素的容器。以下是三种主要的线性序列类型及其详细介绍:
优先用 Vec:如果主要是随机访问或尾部操作。
优先用 VecDeque:如果需要双端操作(如队列、栈)或少量随机访问。
优先用 LinkedList:如果需要频繁在任意位置插入 / 删除,但不关心随机访问(如实现链表、图的邻接表)。
2.7.1.1 Vec<T>(有序存储可变长数组)
特点
连续内存存储:元素在内存中连续排列,支持高效随机访问(O (1))。
动态增长:可通过
push
、extend
等方法动态调整大小。尾部操作高效:在尾部添加 / 删除元素(
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 中,HashMap
和BTreeMap
是两种常用的键值对容器,它们分别基于哈希表和 B 树实现,适用于不同的场景。以下是对这两种容器类型的详细介绍:
选择建议
优先用
HashMap
:如果不需要顺序,且键的哈希性能良好(如整数、字符串)。用
BTreeMap
:如果需要按键排序或范围查询(如数据库索引、配置文件)。
2.7.2.1 HashMap<K, V> 基于哈希表的无序键值对
特点
基于哈希表:通过哈希函数将键映射到存储位置,支持平均 O (1) 时间复杂度的插入、查询和删除操作。
无序存储:元素的顺序不固定,取决于哈希值和插入顺序。
键唯一性:每个键只能出现一次,重复插入会覆盖旧值。
哈希函数:键类型
K
必须实现Hash
和Eq
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
:如果不需要顺序,且元素的哈希性能良好(如整数、字符串)。优先用
BTreeSet
:如果需要按顺序遍历元素或进行范围查询(如数据库索引、配置文件)。
元素类型要求:
HashSet
的元素必须实现Hash
和Eq
(如String
,i32
)。BTreeSet
的元素必须实现Ord
(Ord
包含Eq
,因此也需实现Hash
)。
性能考虑:
HashSet
在大量数据下通常更快,但依赖哈希函数质量。BTreeSet
的性能更稳定,适合需要有序性的场景。
2.7.3.1 HashSet<T> 基于哈希表的有序集合
特点
基于哈希表:使用哈希函数确定元素存储位置,支持平均 O (1) 时间复杂度的插入、查询和删除操作。
无序存储:元素的顺序不固定,取决于哈希值和插入顺序。
元素唯一性:每个元素只能出现一次,重复插入会被忽略。
哈希要求:元素类型
T
必须实现Hash
和Eq
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 编程打下坚实的基础。
- 0
- 1
-
分享