Rust(1)基础语法
Author: Once Day Date: 2024年9月28日
一位热衷于Linux学习和开发的菜鸟,试图谱写一场冒险之旅,也许终点只是一场白日梦…
漫漫长路,有人对你微笑过嘛…
全系列文章请查看专栏: 源码分析_Once-Day的博客-CSDN博客
参考文章:
- Rust 程序设计语言 - Rust 程序设计语言 简体中文版 (kaisery.github.io)
- 简介 - Rust 参考手册 中文版 (rustwiki.org)
文章目录
- Rust(1)基础语法
- 1. 概述
- 1.1 介绍Rust
- 1.2 开发环境
- 1.3 cargo介绍
- 1.4 Rust换源
- 1.5 helloworld
- 2. 基础语法
- 2.1 变量可变性
- 2.2 基础数据类型
- 2.3 简单函数
- 2.4 控制流
- 2.5 引用与Slice类型
- 2.6 结构体
- 2.7 枚举
- 3. 项目管理
- 3.1 包和crate
- 3.2 模块
- 3.3 use导入模块
- 4. 常见集合
- 4.1 Vector
- 4.2 字符串编码
- 4.3 HashMap
1. 概述
1.1 介绍Rust
Rust 是一门由 Mozilla 研究院开发的系统编程语言,于 2010 年首次对外发布。经过多年的迭代和社区的共同努力,Rust 已经发展成为一门成熟、高效、安全的现代编程语言。
Rust 的设计目标是提供一种兼顾安全性和性能的系统级编程语言。它引入了一些创新的概念,如所有权系统(Ownership)、借用检查(Borrow Checker)和生命周期(Lifetime),从根本上避免了常见的内存安全问题,如空指针引用、缓冲区溢出等。同时,Rust 也提供了高层次的抽象和丰富的语言特性,如模式匹配、迭代器、泛型等,使得编写高质量、可维护的代码变得更加容易。
Rust 广泛应用于系统编程、嵌入式开发、网络服务、游戏引擎等领域。相比 C/C++,Rust 在保证高性能的同时,极大地提高了内存安全性,避免了很多常见的 bug 和安全漏洞。Rust 也非常适合编写并发程序,其内置的并发原语和所有权系统可以帮助开发者编写线程安全的代码。此外,Rust 还拥有一个活跃友好的社区,提供了大量高质量的库和工具,如 Cargo 包管理器、Rustfmt 代码格式化工具等,极大地提高了开发效率。
当然,Rust 也有一些不足之处。相比其他高级语言,Rust 的学习曲线较陡峭,尤其是所有权和生命周期的概念对新手来说可能不太容易理解。Rust 的编译速度也相对较慢,但这通常可以通过增量编译等技术来缓解。此外,虽然 Rust 社区一直在快速发展,但其生态系统的成熟度还不如 C/C++。
Rust 是一门非常优秀的系统级编程语言。它在安全性、性能、并发等方面都有出色的表现,非常适合开发高质量、可靠的系统软件。随着 Rust 的不断发展和完善,相信它将在更多的领域得到广泛应用。
1.2 开发环境
搭建 Rust 开发环境非常简单,只需按照以下步骤进行操作:
-
安装 Rust 编译器:访问 Rust 官方网站(https://www.rust-lang.org/),根据您的操作系统选择对应的安装包。Rust 提供了用户友好的安装脚本,在 Windows、macOS 和各种 Linux 发行版上都可以方便地安装。安装过程会自动配置好环境变量。
-
安装集成开发环境(可选):虽然可以使用任何文本编辑器编写 Rust 代码,但使用 IDE 可以获得更好的开发体验。JetBrains CLion、Visual Studio Code、Sublime Text 等都提供了 Rust 插件,支持语法高亮、代码补全、调试等功能。
-
创建新项目:Rust 自带的构建工具和包管理器 Cargo 可以轻松创建和管理项目。在命令行中输入
cargo new hello_world
即可创建一个新的 Rust 项目。 -
编译和运行:在项目根目录下执行
cargo build
可以编译项目,执行cargo run
可以编译并运行项目。Cargo 会自动下载和管理项目依赖的库。
得益于 Rust 优秀的跨平台支持,可以在不同的操作系统上编写和编译 Rust 代码,而无需修改代码。Rust 支持以下三个主要平台:
-
Windows:Rust 可以在 Windows 7 及更高版本上运行,支持 32 位和 64 位系统。Rust 提供了 Visual Studio 的集成插件,也可以与 MinGW 和 Cygwin 一起使用。
-
macOS:Rust 支持 macOS 10.7 及更高版本,可以与 Xcode 集成,也可以在命令行中使用。
-
Linux:Rust 支持各种 Linux 发行版,如 Ubuntu、Fedora、Arch Linux 等,Rust 可以与 GCC 或 Clang 一起使用。
除了以上三个主要平台,Rust 还支持其他一些平台,如 Android、iOS、WebAssembly 等。通过 Rust 的交叉编译功能,可以在一个平台上编译出针对其他平台的可执行文件。
Rust 标准库提供了大量跨平台的 API,如文件 I/O、网络编程、多线程等,可以帮助编写可移植的代码。Rust 还有一个丰富的第三方库生态系统,其中许多库也提供了跨平台支持。
下面实例展示如何在Linux服务器搭建rust开发环境,并且配置VScode集成开发环境:
(1) 打开终端并输入如下命令:
ubuntu->rust:$ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
...(省略大量字符)...Rust is installed now. Great!To get started you may need to restart your current shell.
This would reload your PATH environment variable to include
Cargo's bin directory ($HOME/.cargo/bin).To configure your current shell, you need to source
the corresponding env file under $HOME/.cargo.This is usually done by running one of the following (note the leading DOT):
. "$HOME/.cargo/env" # For sh/bash/zsh/ash/dash/pdksh
source "$HOME/.cargo/env.fish" # For fish
Rust会自动完成安装,但是这个过程需要联网,甚至需要网络代理,如果提示连接失败,可以去网上搜索一下其他方法。
可参考官网安装方式:Other Installation Methods - Rust Forge (rust-lang.org)。
(2) 检测是否正常安装了Rust,打开命令行输入(刚安装完毕需要重启终端才行):
ubuntu->~:$ rustc --version
rustc 1.81.0 (eeb90cda1 2024-09-04)
(3) 卸载和更新rust可以使用下面的命令:
# 更新到最新版本
rustup update
# 卸载 Rust 和 rustup
rustup self uninstall
一般使用VScode来作为IDE开发,安装Rust-analyzer和Rust Syntax这两个插件即可。
通常还需要下载rustc源码用于语法分析,会从https://static.rust-lang.org/dist/2024-09-05/rust-src-1.81.0.tar.xz
下载,这可能需要代理网络才能正常下载。
1.3 cargo介绍
Cargo 是 Rust 编程语言的官方构建系统和包管理器。它提供了一套标准化的工具和约定,用于管理 Rust 项目的依赖关系、构建过程和发布流程。
作为一个现代化的构建系统,Cargo 简化了 Rust 项目的开发和管理。它允许开发者通过一个中心化的配置文件 Cargo.toml
来声明项目的元数据、依赖项以及构建设置。通过使用 Cargo,开发者可以轻松地添加、更新和删除项目依赖,确保项目在不同环境下的一致性和可重复性。
Cargo 提供了一系列的命令,用于自动化常见的开发任务。例如,cargo build
命令可以编译项目及其依赖,生成可执行文件或库文件;cargo test
命令可以运行项目中的测试用例,确保代码的正确性;cargo doc
命令可以基于代码注释生成项目文档,方便开发者学习和理解代码。
除了管理单个项目,Cargo 还支持发布和共享 Rust 包。通过 cargo publish
命令,开发者可以将自己的库发布到中心化的 Rust 包注册中心 crates.io,供其他开发者使用。这种包共享机制促进了 Rust 社区的协作和代码复用,提高了开发效率。
1.4 Rust换源
参考文档:RsProxy,RsProxy 说明页 - 飞书云文档 (larkoffice.com)
Rust 官方提供了 rustup 工具来管理 Rust 的安装和更新。可以通过配置 rustup 的镜像源来加速 Rust 的安装和更新过程。
可以将rustup的更新频道设置为最小版,以减少不必要的更新。
rustup set profile minimal
设置 crates.io 镜像, 修改配置 ~/.cargo/config.toml
,已支持git协议和sparse协议,>=1.68 版本建议使用 sparse-index,速度更快。
[source.crates-io]
replace-with = 'rsproxy-sparse'
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
1.5 helloworld
参考文档:Hello, World! - Rust 程序设计语言 简体中文版 (kaisery.github.io)
下面来一步步来创建一个 Rust 的 Hello, World! 程序,并详细描述其过程。
-
安装 Rust:首先,确保您已经按照之前的步骤安装了 Rust 编译器和 Cargo 包管理器。在命令行中执行
rustc --version
和cargo --version
命令,检查 Rust 是否已经正确安装。 -
创建项目:在命令行中,切换到目标目录下面,并且使用以下命令创建一个新的 Rust 项目:
cargo new hello_world
这将创建一个名为
hello_world
的目录,其中包含以下文件:Cargo.toml
:项目的配置文件,包含项目的元数据和依赖项。src/main.rs
:项目的主要源代码文件。
-
编写代码:使用喜欢的文本编辑器打开
src/main.rs
文件,会看到 Cargo 已经为我们生成了一些示例代码:fn main() {println!("Hello, world!"); }
这就是一个完整的 Hello, World! 程序了。
main
函数是 Rust 程序的入口点,println!
是一个宏(macro),用于打印文本到控制台。 -
编译和运行:回到命令行,进入项目目录,执行以下命令编译项目:
cargo build
这将编译项目,并在
target/debug
目录下生成一个可执行文件。如果代码没有错误,应该会看到类似Compiling hello_world v0.1.0 (file:///your/project/path)
的输出。然后,执行以下命令运行程序:
cargo run
这将会编译(如果之前没有编译过)并运行程序。应该会在控制台看到
Hello, world!
的输出。
如果代码有错误,编译器会给出详细的错误信息,包括错误的位置和原因。Rust 的编译器错误信息非常友好,可以根据提示来修复代码。如果需要调试代码,可以使用 println!
宏来打印变量的值,或者使用 dbg!
宏来打印表达式的值。
Rust 也支持 GDB 和 LLDB 等调试器,可以使用 IDE 或命令行工具来进行调试。
2. 基础语法
2.1 变量可变性
在 Rust 中,变量的可变性是一个重要的概念。根据变量声明时是否使用 mut
关键字,变量可以被分为可变变量和不可变变量。
(1) 不可变变量,在 Rust 中,默认情况下,变量是不可变的(immutable)。这意味着一旦变量被赋值,它的值就不能再被修改。不可变变量提供了安全性和并发性的保证,防止意外的修改和数据竞争。例如:
let x = 5;
x = 6; // 编译错误:不能对不可变变量重新赋值
(2) 可变变量,如果需要修改变量的值,可以在声明变量时使用 mut
关键字将其标记为可变的(mutable)。可变变量允许在初始赋值后再次赋予新的值。例如:
let mut y = 5;
y = 6; // 合法的,可以对可变变量重新赋值
(3) 变量隐藏(Shadowing),Rust 允许在同一作用域内重新声明一个与之前同名的变量,这称为变量隐藏。新声明的变量会遮蔽(shadow)之前的同名变量,从而在作用域内替代它。隐藏的变量可以具有不同的类型和可变性。例如:
let x = 5;
let x = x + 1;
let x = "Hello";
在这个例子中,第二个 let
语句重新声明了变量 x
,遮蔽了之前的整数类型变量,并将其值加 1。第三个 let
语句再次重新声明了变量 x
,这次是一个字符串类型。
(4) 不可变变量与常量的区别,不可变变量和常量都表示值不可变,但它们之间有一些区别:
- 常量使用
const
关键字声明,而不可变变量使用let
关键字声明。 - 常量必须在声明时进行初始化,并且其值在编译时就确定了。不可变变量可以在声明后的任何位置进行初始化,其值可以在运行时计算。
- 常量可以在任何作用域中声明,包括全局作用域。不可变变量只能在函数内或代码块内声明。
- 常量的类型必须显式注明,而不可变变量的类型可以通过类型推断得出。
2.2 基础数据类型
Rust 是静态类型(statically typed)语言,也就是说在编译时就必须知道所有变量的类型。
Rust 是一门强类型语言,提供了丰富的内置数据类型。
(1) 整数类型:
- Rust 提供了有符号整数 (
i8
,i16
,i32
,i64
,i128
) 和无符号整数 (u8
,u16
,u32
,u64
,u128
) 类型。 - 默认的整数类型是
i32
,表示 32 位有符号整数。 - 整数溢出:当整数运算的结果超出其类型的范围时,Rust 默认会触发运行时的整数溢出错误,导致程序崩溃。可以通过使用
wrapping_*
、checked_*
、overflowing_*
和saturating_*
等方法来处理溢出行为。
(2) 浮点数类型:
- Rust 提供了两种浮点数类型:
f32
表示 32 位单精度浮点数,f64
表示 64 位双精度浮点数。 - 默认的浮点数类型是
f64
,因为现代 CPU 的性能优化使得f64
与f32
的速度几乎相同,但精度更高。
(3) 布尔类型:
- Rust 使用
bool
类型表示布尔值,它只有两个可能的值:true
和false
。 - 布尔值通常用于条件表达式和逻辑运算。
(4) 字符类型:
- Rust 使用
char
类型表示 Unicode 字符。 - 字符使用单引号
''
声明,例如let c = 'A';
。 - Rust 的字符类型表示的是 Unicode 标量值,可以表示更多的字符,包括中文、日文、韩文、表情符号等。
(5) 字符串类型:
- Rust 提供了两种字符串类型:字符串切片
&str
和String
。 - 字符串切片
&str
是对存储在其他地方的 UTF-8 编码字符串的引用。它通常用于函数参数和不可变的字符串引用。 String
类型是一个可增长、可变、有所有权的 UTF-8 编码字符串。它通常用于需要修改字符串内容的场景。- 字符串常量使用双引号
""
声明,例如let s = "Hello, world!";
。
(6) 元组类型:
- 元组是一种复合类型,可以包含多个不同类型的值。
- 元组使用括号
()
声明,元素之间用逗号分隔,例如let tuple = (1, 3.14, 'A');
。 - 可以使用模式匹配或点号语法来解构元组,例如
let (x, y, z) = tuple;
或let x = tuple.0;
。 - 单元组
()
是一个特殊的零元素元组,通常用于表示空的返回值或空的结构体。
(7) 数组类型:
- 数组是一种固定长度的、同类型元素的集合。
- 数组使用方括号
[]
声明,元素之间用逗号分隔,例如let arr = [1, 2, 3, 4, 5];
。 - 数组的长度是固定的,必须在编译时确定。可以使用
len()
方法获取数组的长度。 - 数组的索引从 0 开始,可以使用方括号
[]
和索引访问数组元素,例如let x = arr[0];
。
2.3 简单函数
在 Rust 中,函数是一个重要的组成部分,用于将代码划分为独立的、可重用的块。
(1) 函数定义:
-
使用
fn
关键字定义函数,后跟函数名、参数列表和返回类型。 -
函数的参数列表指定了参数的名称和类型,不同参数之间用逗号分隔。
-
函数的返回类型在
->
符号后指定,如果函数没有返回值,可以省略返回类型。 -
函数体由一系列语句和表达式组成,用大括号
{}
包围。fn add(x: i32, y: i32) -> i32 {x + y }
(2) 函数使用:
-
通过函数名后跟括号
()
和参数列表来调用函数。 -
参数按照函数定义中的顺序传递,并且必须与函数定义中的类型匹配。
let result = add(3, 5);
(3) 参数和返回类型:
-
函数的参数可以是任意有效的 Rust 类型,包括基本类型、复合类型和引用类型。
-
参数可以是可变的,通过在参数类型前加上
mut
关键字来声明。 -
函数的返回类型可以是任意有效的 Rust 类型,也可以是空元组
()
表示没有返回值。 -
如果函数只有一个表达式作为函数体,可以省略大括号和
return
关键字,直接将表达式作为返回值。fn multiply(x: i32, y: i32) -> i32 {x * y }
(4) 语句和表达式:
-
Rust 的函数体由语句和表达式组成。
-
语句是执行操作的指令,如变量声明、赋值、函数调用等,语句没有返回值。
-
表达式是计算值的代码块,可以作为函数的返回值或赋值给变量,表达式可以是字面量、变量、函数调用、运算符表达式等。
fn example() {let x = 5; // 语句,没有返回值let y = {let z = 3;z + 1 // 表达式,返回值为 4}; }
(5) 返回值:
-
函数可以使用
return
关键字显式地返回一个值,也可以通过省略分号将最后一个表达式作为返回值。 -
如果函数没有显式的返回值,它会隐式地返回一个空元组
()
。fn explicit_return(x: i32) -> i32 {return x + 1; }fn implicit_return(x: i32) -> i32 {x + 1 }
(6) 函数作为值:
-
在 Rust 中,函数也可以作为值进行传递和使用。
-
函数可以赋值给变量、作为参数传递给其他函数、作为结构体的字段等。
-
函数类型使用
fn
关键字表示,后跟参数类型和返回类型。fn apply(func: fn(i32) -> i32, x: i32) -> i32 {func(x) }let result = apply(add, 5);
Rust 的函数支持多种参数类型和返回类型,可以灵活地组合和使用。函数作为一等公民,可以作为值进行传递和操作,提供了强大的抽象和复用能力。同时,Rust 的语句和表达式的区分使得函数体的写法更加灵活和简洁。
2.4 控制流
在 Rust 中,控制语句用于控制程序的执行流程,包括条件分支、循环等。
(1) if 条件语句:
-
if
语句用于根据条件执行不同的代码块。 -
条件表达式必须是布尔类型(
bool
)。 -
可以使用
else if
和else
子句来处理多个条件分支。let x = 5; if x > 0 {println!("x is positive"); } else if x < 0 {println!("x is negative"); } else {println!("x is zero"); }
(2) loop 循环语句:
-
loop
语句用于创建一个无限循环。 -
可以使用
break
语句跳出循环,使用continue
语句跳过当前迭代并开始下一次迭代。let mut count = 0; loop {count += 1;if count >= 5 {break;} }
(3) while 循环语句:
-
while
语句用于根据条件重复执行代码块。 -
当条件表达式的值为
true
时,循环体被执行,否则循环终止。let mut count = 0; while count < 5 {count += 1; }
(4) for 循环语句:
-
for
语句用于遍历集合或迭代器中的元素。 -
可以使用
for
循环遍历数组、向量、范围等可迭代的对象。let arr = [1, 2, 3, 4, 5]; for item in arr.iter() {println!("{}", item); }
(5) 控制语句作为表达式:
-
在 Rust 中,控制语句也可以用作表达式,返回一个值。
-
if
表达式可以根据条件返回不同的值。 -
loop
表达式可以使用break
语句返回一个值。let x = if true { 5 } else { 10 };let y = loop {break 42; };
(6) 嵌套循环和标记:
-
Rust 支持在循环内部嵌套其他循环。
-
可以使用标记(
'label
)来标识外部循环,并在内部循环中使用break
或continue
语句跳转到指定的标记。'outer: for i in 0..5 {'inner: for j in 0..5 {if i * j >= 10 {break 'outer;}} }
2.5 引用与Slice类型
所有权(Ownership)、借用(Borrowing)和生命周期(Lifetime)是 Rust 语言的核心概念,它们共同构成了 Rust 的内存安全保证。
(1) 所有权(Ownership):
-
所有权是 Rust 中的一个核心概念,用于管理内存的分配和释放。
-
每个值都有一个所有者(变量),负责其内存的分配和释放。
-
一个值同时只能有一个所有者,当所有者离开作用域时,其拥有的值会被自动销毁。
-
当所有权转移时,原来的所有者不再有效,防止了悬垂引用和双重释放的问题。
{let s1 = String::from("hello");let s2 = s1; // 所有权转移给 s2,s1 不再有效 } // s2 离开作用域,内存被释放
(2) 自动销毁(Automatic Deallocation):
- Rust 通过所有权系统自动管理内存,无需手动分配和释放内存。
- 当一个值的所有者离开作用域时,Rust 会自动调用一个特殊的函数
drop
,释放该值所占用的内存。 - 这种自动销毁的机制避免了内存泄漏和使用已释放的内存等问题。
(3) 引用(Reference)和借用(Borrowing):
-
引用允许我们借用一个值的所有权,而不转移所有权。
-
引用分为不可变引用(
&T
)和可变引用(&mut T
)。 -
不可变引用允许读取值,但不能修改,可变引用允许读取和修改值。
-
引用的生命周期不能超过被引用值的生命周期,防止了悬垂引用的问题。
-
在同一作用域内,要么只能有一个可变引用,要么只能有多个不可变引用,但不能同时存在,防止了数据竞争。
fn main() {let s1 = String::from("hello");let len = calculate_length(&s1); // 不可变引用println!("The length of '{}' is {}.", s1, len); }fn calculate_length(s: &String) -> usize {s.len() }
(4) 悬垂引用(Dangling Reference)和编译期检查:
-
悬垂引用是指引用了一个已经被释放的内存。
-
Rust 在编译时通过所有权和生命周期的检查来防止悬垂引用的发生。
-
如果代码中存在悬垂引用,Rust 编译器会给出错误提示,避免了运行时的不安全访问。
fn main() {let reference_to_nothing = dangle(); }fn dangle() -> &String {let s = String::from("hello");&s // 返回一个局部变量的引用,编译错误! }
(5) Slice 类型:
-
Slice 是一种引用类型,用于引用连续的内存序列,如字符串切片(
&str
)和数组切片(&[T]
)。 -
Slice 由一个指向序列起始位置的指针和一个长度组成,长度表示切片包含的元素数量。
-
Slice 允许借用数组或字符串的一部分,而不需要引用整个数组或字符串。
-
Slice 的生命周期与其引用的数据的生命周期相同,确保了内存安全。
let s = String::from("hello world"); let hello = &s[0..5]; let world = &s[6..11];
所有权系统通过规范值的所有权和生命周期,实现了内存安全和自动管理。引用和借用允许在不转移所有权的情况下使用值,并通过编译期检查防止了悬垂引用和数据竞争。Slice 类型提供了对连续内存序列的借用,方便了对数组和字符串的部分引用。
2.6 结构体
Rust中的结构体(struct)是一种用户自定义的数据类型,可以将多个相关的值组合成一个有意义的整体。
(1) 结构体定义和使用:结构体使用关键字struct
进行定义,后跟结构体名称和包含字段的大括号{}
。每个字段都有一个名称和类型。定义结构体后,可以创建结构体实例,并使用点号.
访问其字段。
struct Person {name: String,age: u32,
}let person = Person {name: String::from("Alice"),age: 30,
};println!("{} is {} years old.", person.name, person.age);
(2) 初始化方法:Rust提供了多种初始化结构体的方法。除了上述的字段初始化语法外,还可以定义结构体的关联函数new
作为构造函数。
impl Person {fn new(name: String, age: u32) -> Person {Person { name, age }}
}let person = Person::new(String::from("Bob"), 25);
(3) 字段初始化语法:Rust支持简洁的字段初始化语法。如果变量名与字段名相同,可以省略字段名。
let name = String::from("Alice");
let age = 30;
let person = Person { name, age };
(4) 结构体更新语法:Rust提供了方便的结构体更新语法,可以基于已有的结构体实例创建一个新实例,同时可以选择性地更新某些字段的值。
let person1 = Person {name: String::from("Alice"),age: 30,
};let person2 = Person {name: String::from("Bob"),..person1
};
在上述代码中,person2
继承了person1
的age
字段,同时更新了name
字段的值。
(5) 元组结构体:元组结构体是一种特殊的结构体,它的字段没有名称,而是使用索引访问。元组结构体定义时,在结构体名称后面加上一个元组类型。
struct Color(u8, u8, u8);let black = Color(0, 0, 0);
let red = Color(255, 0, 0);
(6) 类单元结构体:类单元结构体是没有任何字段的结构体。它常用于实现某些特征(trait)或作为标记类型。
struct EmptyStruct;
(7) 结构体方法:Rust允许为结构体定义关联函数和方法。关联函数是在impl
块中定义的函数,可以通过结构体名称直接调用。方法是带有self
参数的函数,可以通过结构体实例调用。
impl Person {fn say_hello(&self) {println!("Hello, my name is {}.", self.name);}
}let person = Person {name: String::from("Alice"),age: 30,
};person.say_hello();
(8) 自动引用和解引用:Rust编译器可以自动为结构体的方法调用进行引用和解引用操作。当调用一个接收&self
或&mut self
参数的方法时,Rust会自动借用结构体实例的引用。当调用一个接收self
参数的方法时,Rust会自动获取结构体实例的所有权。
(9) 关联函数:关联函数是在impl
块中定义的函数,它们不以self
作为参数,而是直接与结构体关联。关联函数通常用于构造函数或者与结构体相关的工具函数。
impl Person {fn new(name: String, age: u32) -> Person {Person { name, age }}fn default() -> Person {Person {name: String::from("John Doe"),age: 0,}}
}let person1 = Person::new(String::from("Alice"), 30);
let person2 = Person::default();
2.7 枚举
枚举是Rust语言中一个非常重要和强大的特性,它允许我们定义一个类型,该类型可以具有多个可能的值。
(1) 枚举的定义,要定义一个枚举,使用enum
关键字,后跟枚举的名称。枚举可以包含多个变体(variant),每个变体可以关联不同类型的数据。
enum Message {Quit,Move { x: i32, y: i32 },Write(String),ChangeColor(i32, i32, i32),
}
在上面的例子中,Message
枚举有四个变体:Quit
没有关联任何数据,Move
包含两个i32
类型的值,Write
包含一个String
,ChangeColor
包含三个i32
值。
(2) Option枚举,Option
是Rust标准库中的一个预定义枚举,用于表示一个值可能存在(Some
)或不存在(None
)。它定义如下:
enum Option<T> {Some(T),None,
}
Option
在Rust中广泛使用,特别是在处理可能缺失的值时。它提供了一种安全而明确的方式来处理空值,避免了常见的空指针异常。
(3) match表达式和模式匹配,match
表达式是Rust中处理枚举和进行模式匹配的主要方式。它允许我们根据枚举的不同变体执行不同的代码。match
表达式的每个分支都由一个模式和相应的代码块组成。
let msg = Message::Move { x: 10, y: 20 };match msg {Message::Quit => println!("Quit"),Message::Move { x, y } => println!("Move to ({}, {})", x, y),Message::Write(text) => println!("Text message: {}", text),Message::ChangeColor(r, g, b) => println!("Change color to RGB({}, {}, {})", r, g, b),
}
在上面的例子中,我们使用match
表达式对msg
变量进行模式匹配。根据msg
的变体,将执行相应的代码块。在Message::Move
分支中,我们使用了绑定值的模式x
和y
,将Move
变体中的值绑定到这两个变量上。
(4) 匹配的穷尽性,在Rust中,match
表达式必须穷尽所有可能的情况。这意味着每个可能的枚举变体都必须有一个对应的匹配分支。如果我们遗漏了某个变体,编译器会给出一个警告。这种穷尽性检查有助于避免遗漏情况而引入潜在的错误。
(5) if let表达式,除了match
表达式,Rust还提供了if let
表达式,用于简化只有一种模式匹配的情况。它允许我们在条件为真时执行代码块。
if let Some(value) = some_option {println!("Value is: {}", value);
} else {println!("None");
}
在上面的例子中,如果some_option
是Some
变体,则将执行第一个代码块,并将Some
中的值绑定到value
变量上。如果some_option
是None
,则执行else
块。
3. 项目管理
3.1 包和crate
在Rust中,包(package)和crate是两个重要的概念,它们用于组织和管理代码。
(1) 包(Package):
- 包是Rust中的一个高层次的代码组织单位。它由一个或多个crate组成,并提供了一种结构化的方式来管理相关的功能和模块。
- 一个包由一个
Cargo.toml
文件定义,该文件描述了包的元数据、依赖项和构建配置。 - 包的目录结构通常如下:
my_package/ ├── Cargo.toml └── src/├── lib.rs└── main.rs
src
目录是包的源代码目录。lib.rs
文件定义了包的库crate,而main.rs
文件定义了包的二进制crate(可执行文件)。- 包可以包含多个二进制crate和一个库crate。每个二进制crate都有自己的
main.rs
文件,而库crate则在lib.rs
文件中定义。
(2) Crate:
- Crate是Rust中的编译单元。它可以是一个库(library crate)或一个二进制文件(binary crate)。
- 库crate提供了可重用的代码,可以被其他crate引用和使用。它们通常包含函数、结构体、枚举等,并且没有
main
函数。 - 二进制crate是可执行的程序,它们包含一个
main
函数作为程序的入口点。 - Crate之间可以相互依赖和引用,通过在
Cargo.toml
文件中指定依赖项,可以将其他crate引入到当前的crate中。 - Crate可以通过
use
关键字来引入和使用其他crate中的模块、函数、结构体等。 - 每个crate都有一个根模块,可以在根模块中定义子模块,形成一个模块树结构。
3.2 模块
在Rust中,模块、路径、use
关键字和pub
关键字共同构成了Rust的模块系统,用于组织和封装代码。
(1) 模块(Module):
- 模块是Rust中用于组织和封装代码的基本单位。它允许将相关的功能、结构体、枚举等组合在一起,形成一个逻辑上的代码块。
- 模块可以嵌套,形成一个层次结构。每个模块都有自己的命名空间,可以包含函数、结构体、枚举、常量以及其他模块。
- 模块的定义使用
mod
关键字,后跟模块的名称。mod my_module {// 模块内的代码 }
- 模块可以定义在单独的文件中,也可以内联在其他模块中。当模块定义在单独的文件中时,文件名应与模块名相同。
(2) 路径(Path):
- 路径用于引用模块中的项(如函数、结构体等)。它指定了从当前位置到目标项的路径。
- 路径使用
::
作为分隔符,表示模块的层次结构。例如,std::io::stdin
表示std
模块下的io
模块中的stdin
函数。 - 路径可以是绝对路径或相对路径。绝对路径以crate的根模块开始,而相对路径则从当前模块开始。
(3) use
关键字:
use
关键字用于将模块中的项引入当前作用域,以便可以直接使用它们,而无需每次都指定完整的路径。- 通过
use
关键字,可以创建一个别名或简化访问模块中的项。use std::io; use std::io::stdin;
- 上面的代码将
std::io
模块引入当前作用域,并创建了一个stdin
的别名,可以直接使用stdin()
函数,而不需要写成std::io::stdin()
。 use
关键字还支持通配符*
,用于引入模块中的所有公共项。例如,use std::io::*;
将std::io
模块中的所有公共项引入当前作用域。
(4) pub
关键字:
pub
关键字用于将模块、函数、结构体、枚举等标记为公共的,以便其他模块可以访问和使用它们。- 默认情况下,模块中的项是私有的,只能在当前模块及其子模块中访问。通过在项前面添加
pub
关键字,可以将其标记为公共的。 - 公共项可以被其他模块通过路径或
use
关键字访问和使用。pub mod my_module {pub fn my_function() {// 公共函数的实现} }
- 在上面的代码中,
my_module
模块和my_function
函数都被标记为公共的,可以被其他模块访问。
3.3 use导入模块
(1) 引入模块项的路径:
- 在Rust中,可以使用路径来引用模块中的项(如函数、结构体等),路径指定了从当前位置到目标项的路径。
- 路径使用
::
作为分隔符,表示模块的层次结构,例如,std::io::stdin
表示std
模块下的io
模块中的stdin
函数。 - 如果要引用当前模块中的项,可以直接使用项的名称。如果要引用其他模块中的项,需要使用完整的路径。
- 例如,假设有以下模块结构:
mod my_module {pub fn my_function() {// ...} }
- 在其他模块中引用
my_function
函数时,需要使用完整的路径my_module::my_function()
。
(2) 使用use
关键字引入路径:
use
关键字用于将模块中的项引入当前作用域,以便可以直接使用它们,而无需每次都指定完整的路径。- 通过
use
关键字,可以创建一个别名或简化访问模块中的项。use std::io; use std::io::stdin;
- 上面的代码将
std::io
模块引入当前作用域,并创建了一个stdin
的别名,可以直接使用stdin()
函数,而不需要写成std::io::stdin()
。 use
关键字还支持通配符*
,用于引入模块中的所有公共项。use std::io::*;
将std::io
模块中的所有公共项引入当前作用域。- 通过使用
use
关键字,可以简化代码,减少重复的路径编写。
(3) 将模块拆分为多个文件:
- 当模块变得复杂和庞大时,将其拆分为多个文件可以提高代码的可读性和可维护性。
- 在Rust中,可以使用
mod
关键字来声明一个模块,并指定该模块的内容来自于另一个文件。 - 例如,假设有一个名为
my_module.rs
的文件,其中包含以下代码:pub fn my_function() {// ... }
- 在主文件(如
lib.rs
或main.rs
)中,可以使用以下代码将my_module.rs
文件作为一个模块引入:mod my_module;
- 这样,
my_module.rs
文件中的代码就成为了一个独立的模块,可以通过my_module::my_function()
来访问其中的函数。 - 如果模块包含子模块,可以在模块的目录下创建一个与模块同名的目录,并将子模块的文件放在该目录中。
my_module/ ├── mod.rs └── submodule.rs
- 在
my_module/mod.rs
文件中,可以使用mod submodule;
来声明子模块。
4. 常见集合
4.1 Vector
Vector是Rust标准库中提供的一种动态数组类型,它允许在运行时动态地增加或缩小数组的大小。Vector是一种常用的集合类型,用于存储同一类型的多个值。
(1) 创建Vector:
- 可以使用
Vec::new()
函数创建一个空的Vector。let mut my_vec = Vec::new();
- 也可以使用
vec!
宏来创建一个包含初始值的Vector。let my_vec = vec![1, 2, 3, 4, 5];
(2) 添加元素:
- 可以使用
push()
方法在Vector的末尾添加一个元素。my_vec.push(6);
- 也可以使用
insert()
方法在Vector的指定位置插入一个元素。my_vec.insert(2, 10);
(3) 访问元素:
- 可以使用索引运算符
[]
来访问Vector中的元素。let value = my_vec[3];
- 也可以使用
get()
方法来安全地访问Vector中的元素,它返回一个Option
类型。if let Some(value) = my_vec.get(3) {println!("Value at index 3: {}", value); }
(4) 遍历Vector:
- 可以使用
for
循环来遍历Vector中的元素。for value in &my_vec {println!("{}", value); }
- 如果需要遍历Vector的可变引用,可以使用
for
循环配合&mut
。for value in &mut my_vec {*value += 1; }
(5) 删除元素:
- 可以使用
pop()
方法从Vector的末尾删除一个元素,并返回被删除的元素。if let Some(value) = my_vec.pop() {println!("Popped value: {}", value); }
- 也可以使用
remove()
方法从Vector的指定位置删除一个元素。let removed_value = my_vec.remove(2);
(6) Vector的常用方法:
len()
: 返回Vector的长度。is_empty()
: 检查Vector是否为空。clear()
: 清空Vector中的所有元素。contains()
: 检查Vector是否包含指定的元素。sort()
: 对Vector中的元素进行排序。dedup()
: 去除Vector中的重复元素。
Vector在Rust中被广泛使用,特别是在需要动态调整数组大小的场景下。它提供了方便的方法来添加、删除和访问元素,并支持各种常用的操作。Vector是通过连续的内存布局实现的,因此访问元素的速度非常快。
需要注意的是,当Vector的容量不足时,添加新元素会触发内存重新分配和数据复制,这可能会影响性能。因此,如果事先知道Vector的大小,可以使用with_capacity()
方法预先分配足够的容量,以避免频繁的内存重新分配。
4.2 字符串编码
在Rust中,字符串是一个重要的数据类型,用于存储和操作UTF-8编码的文本。Rust提供了两种主要的字符串类型:字符串切片(&str
)和字符串对象(String
)。
(1) 字符串切片(&str
):
- 字符串切片是一个指向UTF-8编码字符序列的引用。
- 它是一个固定大小的视图,指向字符串的一部分或全部。
- 字符串切片是不可变的,无法修改其内容。
- 可以使用字符串字面量创建字符串切片:
let hello = "Hello, world!";
- 字符串切片的类型标记为
&str
,它是一个借用的引用,不拥有底层的字符串数据。
(2) 字符串对象(String
):
- 字符串对象是一个可增长的、可变的字符串类型,它拥有自己的底层字符串数据。
- 可以使用
String::new()
函数创建一个空的字符串对象:let mut my_string = String::new();
- 也可以使用
String::from()
函数从字符串切片或字符串字面量创建字符串对象:let my_string = String::from("Hello, world!");
- 字符串对象提供了各种方法来操作和修改字符串,如
push_str()
、insert()
、replace()
等。
(3) UTF-8编码:
- Rust的字符串使用UTF-8编码来表示Unicode字符。
- UTF-8是一种可变长度的编码方式,每个Unicode字符可以占用1到4个字节。
- Rust的字符串保证始终是有效的UTF-8序列,不允许包含无效的字节序列。
- 可以使用
chars()
方法遍历字符串中的Unicode字符:for c in my_string.chars() {println!("{}", c); }
(4) 字符串的常用操作:
- 连接字符串:可以使用
+
运算符或format!
宏将多个字符串连接起来。 - 获取字符串长度:可以使用
len()
方法获取字符串的字节长度,使用chars().count()
获取字符串的字符数量。 - 截取子字符串:可以使用切片语法
[start..end]
截取字符串的一部分。 - 搜索和替换:可以使用
contains()
、find()
等方法搜索字符串,使用replace()
方法替换字符串中的子串。 - 转换大小写:可以使用
to_lowercase()
和to_uppercase()
方法将字符串转换为小写或大写。
需要注意的是,由于字符串使用UTF-8编码,字符串的索引操作和切片操作需要小心处理。不能直接使用字节索引来访问字符串中的字符,而应该使用chars()
方法获取字符迭代器,或者使用char_indices()
方法获取字符的字节索引。
4.3 HashMap
Rust中的HashMap是一种键值对的集合类型,它提供了快速的查找、插入和删除操作。HashMap使用哈希表实现,其中每个键都映射到一个唯一的值。
(1) 创建HashMap:
- 可以使用
HashMap::new()
函数创建一个空的HashMap。use std::collections::HashMap;let mut my_map = HashMap::new();
- 也可以使用
HashMap::with_capacity()
函数创建一个指定初始容量的HashMap,以避免频繁的扩容操作。
(2) 插入键值对:
- 可以使用
insert()
方法将一个键值对插入到HashMap中。my_map.insert("key1", 10); my_map.insert("key2", 20);
- 如果插入的键已经存在,
insert()
方法会更新对应的值,并返回旧的值(如果存在)。
(3) 访问值:
- 可以使用
get()
方法根据键来获取对应的值。get()
方法返回一个Option<&V>
类型,如果键存在,则返回Some(&value)
,否则返回None
。if let Some(value) = my_map.get("key1") {println!("Value for key1: {}", value); }
- 也可以使用
[]
运算符来访问值,但如果键不存在,会导致程序崩溃。
(4) 遍历HashMap:
- 可以使用
for
循环来遍历HashMap中的键值对。for (key, value) in &my_map {println!("{}: {}", key, value); }
- 也可以使用
keys()
和values()
方法分别获取HashMap的键和值的迭代器。
(5) 删除键值对:
- 可以使用
remove()
方法根据键来删除对应的键值对,并返回被删除的值(如果存在)。if let Some(value) = my_map.remove("key1") {println!("Removed value: {}", value); }
(6) HashMap的所有权:
- HashMap拥有其存储的键和值的所有权,当HashMap被丢弃时,其内部的键和值也会被释放。
- 如果希望在HashMap中存储引用,可以使用
HashMap<&str, &T>
类型,其中键和值都是引用类型。
(7) HashMap的常用方法:
len()
: 返回HashMap中键值对的数量。is_empty()
: 检查HashMap是否为空。clear()
: 清空HashMap中的所有键值对。contains_key()
: 检查HashMap是否包含指定的键。entry()
: 返回一个Entry枚举,用于检查和修改指定键对应的值。
HashMap在Rust中广泛用于需要快速查找和关联数据的场景。它提供了高效的键值对存储和检索,并支持各种常用的操作。HashMap的键必须实现Eq
和Hash
trait,以确保在哈希表中的正确性和一致性。