概述
开始了解rust相关的编程。
![]()
语言特点
Rust是一种无运行时的强类型语言,它具有许多高级特性,如泛型和lambda等。Rust的内存安全机制比C++更完善,因为它采用了独特的所有权模型。Rust的语法与C++相似,但采用了类型后置的风格,即关键字后跟类型。Rust的变量声明使用let关键字,且变量一旦声明后其类型和值都不能改变,除非使用mut关键字显式地声明变量为可变的。
高性能
Rust 速度惊人且内存利用率极高。由于没有运行时和垃圾回收,它能够胜任对性能要求特别高的服务,可以在嵌入式设备上运行,还能轻松和其他语言集成。
可靠性
Rust 丰富的类型系统和所有权模型保证了内存安全和线程安全,让您在编译期就能够消除各种各样的错误。
生产力
Rust 拥有出色的文档、友好的编译器和清晰的错误提示信息, 还集成了一流的工具——包管理器和构建工具, 智能地自动补全和类型检验的多编辑器支持, 以及自动格式化代码等等。
2024-05-23 我现在选择的是 1.78.0 版本。
rust安装
linux/mac机器上
1 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh |
Rust 的升级非常频繁。如果您安装 Rustup 后已有一段时间,那么很可能您的 Rust 版本已经过时了。运行 rustup update 获取最新版本的 Rust。
vscode里面安装两个插件 rust-analyzer、Native Debug。
cargo 教程
Cargo:Rust 的构建工具和包管理器
您在安装 Rustup 时,也会安装 Rust 构建工具和包管理器的最新稳定版,即 Cargo。Cargo 可以做很多事情:
- cargo build 可以构建项目
- cargo run 可以运行项目
- cargo test 可以测试项目
- cargo doc 可以为项目构建文档
- cargo publish 可以将库发布到 crates.io。
要检查您是否安装了 Rust 和 Cargo,可以在终端中运行:
1 | cargo --version |
rust输出命令行
1 | fn main() { |
Rust基本语法
如果要声明变量,需要使用 let 关键字。例如:
1 | let a = 123; |
在语言层面尽量少的让变量的值可以改变。所以 a 的值不可变。但这不意味着 a 不是”变量”(英文中的 variable),官方文档称 a 这种变量为”不可变变量”。
1 | const a: i32 = 123; |
重影(Shadowing)
1 | fn main() { |
rust数据类型
整数型(Integer)
| 位长度 | 有符号 | 无符号 |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
浮点数型(Floating-Point)
1 | fn main() { |
数学运算
1 | fn main() { |
注意:Rust 不支持 ++ 和 –,因为这两个运算符出现在变量的前后会影响代码可读性,减弱了开发者对变量改变的意识能力。
rust注释
1 | /* |
rust函数
1 | fn <函数名> ( <参数> ) <函数体> |
rust条件句子
1 | fn main() { |
rust循环
1 | fn main() { |
rust迭代器
| 方法名 | 描述 | 示例 |
|---|---|---|
| next() | 返回迭代器中的下一个元素。 | let mut iter = (1..5).into_iter(); while let Some(val) = iter.next() { println!(“{}”, val); } |
| size_hint() | 返回迭代器中剩余元素数量的下界和上界。 | let iter = (1..10).into_iter(); println!(“{:?}”, iter.size_hint()); |
| count() | 计算迭代器中的元素数量。 | let count = (1..10).into_iter().count(); |
| nth() | 返回迭代器中第 | n 个元素。 let third = (0..10).into_iter().nth(2); |
| last() | 返回迭代器中的最后一个元素。 | let last = (1..5).into_iter().last(); |
| all() | 如果迭代器中的所有元素都满足某个条件,返回 | true。 let all_positive = (1..=5).into_iter().all( |
| any() | 如果迭代器中的至少一个元素满足某个条件,返回 | true。 let any_negative = (1..5).into_iter().any( |
| find() | 返回迭代器中第一个满足某个条件的元素。 | let first_even = (1..10).into_iter().find( |
| find_map() | 对迭代器中的元素应用一个函数,返回第一个返回 | Some 的结果。 let first_letter = “hello”.chars().find_map( |
| map() | 对迭代器中的每个元素应用一个函数。 | let squares: Vec |
| filter() | 保留迭代器中满足某个条件的元素。 | let evens: Vec |
| filter_map() | 对迭代器中的元素应用一个函数,如果函数返回 | Some,则保留结果。 let chars: Vec |
| map_while() | 对迭代器中的元素应用一个函数,直到函数返回 | None。 let first_three = (1..).into_iter().map_while( |
| take_while() | 从迭代器中取出满足某个条件的元素,直到不满足为止。 | let first_five = (1..10).into_iter().take_while( |
| skip_while() | 跳过迭代器中满足某个条件的元素,直到不满足为止。 | let odds: Vec |
| for_each() | 对迭代器中的每个元素执行某种操作。 | let mut counter = 0; (1..5).into_iter().for_each( |
| fold() | 对迭代器中的元素进行折叠,使用一个累加器。 | let sum: i32 = (1..5).into_iter().fold(0, |
| try_fold() | 对迭代器中的元素进行折叠,可能在遇到错误时提前返回。 | let result: Result = (1..5).into_iter().try_fold(0, |
| scan() | 对迭代器中的元素进行状态化的折叠。 | let sum: Vec |
| take() | 从迭代器中取出最多 | n 个元素。 let first_five = (1..10).into_iter().take(5).collect::<Vec<_>>() |
| skip() | 跳过迭代器中的前 | n 个元素。 let after_five = (1..10).into_iter().skip(5).collect::<Vec<_>>() |
| zip() | 将两个迭代器中的元素打包成元组。 | let zipped = (1..3).zip(&[‘a’, ‘b’, ‘c’]).collect::<Vec<_>>() |
| cycle() | 重复迭代器中的元素,直到无穷。 | let repeated = (1..3).into_iter().cycle().take(7).collect::<Vec<_>>() |
| chain() | 连接多个迭代器。 | let combined = (1..3).chain(4..6).collect::<Vec<_>>() |
| rev() | 反转迭代器中的元素顺序。 | let reversed = (1..4).into_iter().rev().collect::<Vec<_>>() |
| enumerate() | 为迭代器中的每个元素添加索引。 | let enumerated = (1..4).into_iter().enumerate().collect::<Vec<_>>() |
| peeking_take_while() | 取出满足条件的元素,同时保留迭代器的状态,可以继续取出后续元素。 | let (first, rest) = (1..10).into_iter().peeking_take_while( |
| step_by() | 按照指定的步长返回迭代器中的元素。 | let even_numbers = (0..10).into_iter().step_by(2).collect::<Vec<_>>() |
| fuse() | 创建一个额外的迭代器,它在迭代器耗尽后仍然可以调用 | next() 方法。 let mut iter = (1..5).into_iter().fuse(); while iter.next().is_some() {} |
| inspect() | 在取出每个元素时执行一个闭包,但不改变元素。 | let mut counter = 0; (1..5).into_iter().inspect( |
| same_items() | 比较两个迭代器是否产生相同的元素序列。 | let equal = (1..5).into_iter().same_items((1..5).into_iter()); |
在 Rust 中,迭代器(Iterator)是一个允许你逐个访问集合中每个元素的对象。迭代器提供了一种抽象的方法来处理集合,无需显式地管理索引或遍历逻辑。Rust 的迭代器具有惰性,即它们不会在你明确要求时执行任何操作。
迭代器是一个实现了 Iterator trait 的类型,这个 trait 定义了一些方法,其中最重要的是 next 方法,它返回迭代器中的下一个元素。
创建迭代器
Rust 中的集合类型(如数组、向量、哈希表等)都有方法创建迭代器。下面是一些示例:
数组迭代器
1 | let arr = [1, 2, 3, 4, 5]; |
向量迭代器
1 | let vec = vec![1, 2, 3, 4, 5]; |
范围迭代器
1 | let iter = 1..6; // 创建一个范围的迭代器,从1到5 |
使用迭代器
你可以使用 for 循环遍历迭代器:
1 | let vec = vec![1, 2, 3, 4, 5]; |
常用迭代器适配器
Rust 提供了一些强大的迭代器适配器,用于对迭代器进行变换、过滤和组合。
map
map 方法对每个元素应用一个闭包(函数)并返回一个新的迭代器:
1 | let vec = vec![1, 2, 3, 4, 5]; |
filter
filter 方法使用一个闭包对每个元素进行过滤,并返回一个新的迭代器,只保留闭包返回 true 的元素:
1 | let vec = vec![1, 2, 3, 4, 5]; |
collect
collect 方法将迭代器转换为集合类型(如向量、哈希表等):
1 | let vec = vec![1, 2, 3, 4, 5]; |
fold
fold 方法对迭代器中的每个元素应用一个累加器函数,生成一个单一的值:
1 | let vec = vec![1, 2, 3, 4, 5]; |
自定义迭代器
你也可以实现自己的迭代器,只需要实现 Iterator trait。
1 | struct Counter { |
总结
迭代器是 Rust 中处理集合和序列的强大工具,它们提供了一种灵活、简洁和高效的方式来处理数据。通过使用迭代器适配器,你可以链式调用多个操作,实现复杂的数据变换和过滤。
rust闭包
当然!闭包(Closure)是Rust中的一个强大特性。它允许你捕获周围作用域中的变量,然后在以后调用它们。让我们通过几个简单的例子来解释闭包。
基本语法
闭包的基本语法如下:
1 | let closure = |参数| -> 返回类型 { 代码块 }; |
示例
- 一个简单的闭包:
1 | let add_one = |x: i32| -> i32 { |
这里,add_one 是一个闭包,它接收一个整数 x,返回 x + 1。
- 捕获外部变量:
闭包可以自动捕获它们所在环境中的变量:
1 | let y = 5; |
在这个例子中,闭包 add_y 捕获了外部的变量 y。
类型推断
Rust 可以自动推断闭包的参数和返回类型,通常不需要显式声明:
1 | let add_two = |x| x + 2; |
闭包的三种类型
根据捕获变量的方式,闭包分为三种类型:
- 按引用捕获:使用
&,即借用。 - 按值捕获:使用
move,即所有权转移。 - 按可变引用捕获:使用
&mut,即可变借用。
示例:
- 按引用捕获:
1 | let s = String::from("hello"); |
- 按值捕获:
1 | let s = String::from("hello"); |
使用 move 关键字会将变量 s 的所有权移动到闭包内。
- 按可变引用捕获:
1 | let mut s = String::from("hello"); |
在这个例子中,闭包 change 可变地借用了变量 s。
结论
闭包是Rust中非常灵活且强大的工具,允许你编写更简洁、更强大的代码。希望这些简单的例子能帮助你理解闭包的基本用法!
rust所有权
在Rust中,所有权(Ownership)是一个核心概念,确保了内存安全性和无数据竞争。让我们通过一些简单的例子和解释来了解所有权。
所有权的基本规则
Rust 的所有权系统有三条基本规则:
- 每一个值都有一个所有者(Owner)。
- 每一个值同时只能有一个所有者。
- 当所有者超出作用域(Scope)时,值会被丢弃(Dropped)。
示例
1. 变量的所有权
1 | { |
当 s 超出作用域时,它的内存会被自动释放,这个过程被称为 drop。
2. 所有权转移(移动)
1 | let s1 = String::from("hello"); |
在这个例子中,s1 的所有权被移动(Move)给了 s2,所以 s1 不能再被使用。
3. 克隆
如果需要在多个变量中使用同一个值,可以使用 clone 方法:
1 | let s1 = String::from("hello"); |
通过 clone 方法,可以深度复制数据,从而 s1 和 s2 都有效。
引用和借用
Rust 提供了引用(References)来实现借用(Borrowing),这允许你在不转移所有权的情况下使用值。
1. 不可变引用
1 | let s1 = String::from("hello"); |
使用 & 创建引用,函数 calculate_length 借用了 s1 而不获取其所有权。
2. 可变引用
1 | let mut s = String::from("hello"); |
使用 &mut 创建可变引用,可以修改借用的值。注意:同时只能有一个可变引用,防止数据竞争。
悬垂引用(Dangling References)
Rust 不允许悬垂引用,确保引用永远是有效的。
1 | fn dangle() -> &String { |
在这个例子中,返回对局部变量的引用会导致悬垂引用,Rust 会在编译时捕获这个错误。
结论
Rust 的所有权系统通过所有权、借用和引用的规则,确保了内存安全性和无数据竞争。这些规则可能需要一些时间来习惯,但它们提供了强大的安全保证。希望这些示例和解释能帮助你理解Rust的所有权概念!
rust切片
在Rust中,slice是一种引用类型,用于表示一个连续的元素序列的子集。它不拥有数据,只是对数据的一种引用。slice可以帮助我们在不复制数据的情况下对数组或向量进行部分操作。以下是一些关于Rust slice的关键概念:
定义和语法:
- slice是通过一个引用和一个长度来定义的。
- 语法形式为:
&[T]或&mut [T],其中T是元素的类型。 - 不可变slice使用
&符号,可变slice使用&mut符号。
创建和使用:
- 你可以通过引用数组或向量的一部分来创建slice。
- 例如:
let slice = &array[1..4];表示创建一个引用,指向array的第2到第4个元素(不包括第4个元素)。
特性和功能:
- slice具有与数组类似的功能,例如可以使用索引访问元素。
- slice的长度是固定的,不能动态改变。
- slice可以用于函数参数,避免传递整个数组或向量,提升效率。
示例代码:
1
2
3
4
5
6
7
8
9
10
11fn main() {
let arr = [1, 2, 3, 4, 5];
let slice = &arr[1..4]; // 取得数组的第2到第4个元素的slice
println!("{:?}", slice); // 输出:[2, 3, 4]
// 使用slice
for elem in slice.iter() {
println!("{}", elem);
}
}
在这个示例中,slice是对数组arr的第2到第4个元素的引用,通过这种方式,你可以在不复制数据的情况下对这些元素进行操作。
slice在Rust中是一个非常强大的工具,能够让你更高效地处理数据,同时保证内存安全和性能。
这段代码的核心问题在于对String的可变引用和不可变引用之间的冲突。让我们逐行分析代码:
1 | fn main() { |
创建
String变量mut s:1
let mut s = String::from("runoob");
这里创建了一个可变的
String变量s,并初始化为”runoob”。创建不可变的slice:
1
let slice = &s[0..3];
这里创建了一个不可变的slice,引用
s的前3个字符(即”run”)。这个slice是不可变的,并且它引用的是s的部分数据。尝试修改
String:1
s.push_str("yes!");
这行代码试图向
s追加字符串”yes!”。然而,Rust的借用规则规定,在存在一个不可变引用时,不能创建可变引用或修改原数据。这是为了防止数据竞争和保持内存安全。因此,这行代码会导致编译错误。打印slice:
1
println!("slice = {}", slice);
这行代码会打印
slice的内容(即”run”),但由于前一行的错误,程序在编译时就会失败,所以这行代码实际上不会执行。
错误的原因
在Rust中,借用规则如下:
- 一个变量可以有任意多个不可变引用(
&T)。 - 一个变量在任何时候只能有一个可变引用(
&mut T)。 - 如果存在不可变引用,就不能创建可变引用,反之亦然。
在这段代码中,slice是对s的不可变引用,而随后试图对s进行修改(通过push_str方法),这是不允许的。因此,编译器会报错。
解决方法
为了避免这个错误,可以在修改s之前不持有任何不可变引用。下面是修改后的代码:
1 | fn main() { |
在这个版本中,slice的作用域限制在一个新的块中,当块结束时,slice不再引用s。这样,在后面可以安全地修改s。
rust结构体
在Rust中,结构体(struct)是一种自定义数据类型,用于将相关的数据组合在一起。结构体在Rust中有三种主要形式:经典结构体(C-like struct)、元组结构体(tuple struct)和单元结构体(unit-like struct)。下面详细介绍每种结构体并给出相应的示例代码。
1. 经典结构体(C-like Struct)
经典结构体类似于其他编程语言中的结构体或类,具有命名字段。
定义和使用
1 | struct User { |
2. 元组结构体(Tuple Struct)
元组结构体是没有命名字段的结构体,使用方式类似于元组。
定义和使用
1 | struct Color(i32, i32, i32); |
3. 单元结构体(Unit-like Struct)
单元结构体没有任何字段,通常用于实现某种特征或标记。
定义和使用
1 | struct AlwaysEqual; |
结构体的方法
你可以为结构体定义方法和关联函数,使用impl块来实现。
方法和关联函数示例
1 | struct Rectangle { |
结构体的所有权
结构体中的字段可以拥有数据的所有权,也可以引用其他数据。使用引用时,需要考虑生命周期。
生命周期示例
1 | struct User<'a> { |
Rust的结构体功能强大且灵活,结合其所有权和借用系统,可以帮助开发者编写安全且高效的代码。
rust枚举类
在Rust中,枚举(enum)是一种定义可能值的集合的数据类型。枚举允许你将一个值与一组预定义的标签(变体)之一关联起来。每个变体可以携带不同类型和数量的数据。枚举在处理有限集合的可能值时特别有用。
定义和使用枚举
基本枚举
这是一个简单的枚举示例,没有附加数据。
1 | enum Direction { |
带数据的枚举
枚举的每个变体可以携带不同类型和数量的数据。
1 | enum Message { |
使用Option枚举
Rust标准库定义了一个非常常用的枚举类型Option,用于处理可能为空的值。
1 | fn main() { |
使用Result枚举
Rust标准库还定义了一个常用的枚举类型Result,用于处理可能的错误。
1 | fn divide(dividend: i32, divisor: i32) -> Result<i32, String> { |
方法和枚举
你可以为枚举定义方法,类似于为结构体定义方法。
1 | enum Message { |
总结
Rust中的枚举非常强大,允许定义复杂的类型并对其进行模式匹配。这使得代码更加简洁和易读,同时还能确保在编译时就捕获到许多潜在的错误。
rust组织管理
在Rust中,程序的组织方式主要涉及三个概念:crate、package和module。理解这些概念及其相互关系,有助于编写清晰、可维护的Rust代码。以下是对这三个概念的详细解释:
1. Crate
Crate是Rust中的一个最小的编译单元。每个Rust项目都是一个crate,有两种类型的crate:
- Binary Crate:生成一个可执行文件。每个binary crate都有一个
main函数作为入口点。通常,Rust项目的根目录下有一个src/main.rs文件,它定义了一个binary crate。 - Library Crate:生成一个库,可以被其他crate使用。通常,Rust项目的根目录下有一个
src/lib.rs文件,它定义了一个library crate。
示例:
创建一个binary crate的项目结构:
1 | my_project |
创建一个library crate的项目结构:
1 | my_library |
2. Package
Package是包含一个或多个crate的文件夹,提供一个Cargo.toml文件来描述这些crate以及它们之间的依赖关系。一个package可以包含以下内容:
- 至多一个library crate。
- 零个或多个binary crate。
示例:
一个包含一个library crate和一个binary crate的package结构:
1 | my_package |
3. Module
Module是用于在crate内部组织代码的工具。通过模块,可以将代码分割成多个逻辑单元,增加代码的可读性和可维护性。模块可以嵌套,并且可以在文件内或跨文件进行定义。
示例:
1 | // src/lib.rs |
定义和使用模块
你可以在一个文件中定义多个模块,也可以跨多个文件定义模块。
在一个文件中定义模块:
1 | mod network { |
跨多个文件定义模块:
假设有以下项目结构:
1 | my_project |
src/main.rs:
1 | mod network; |
src/network/mod.rs:
1 | pub fn connect() { |
src/network/server.rs:
1 | pub fn start() { |
src/client.rs:
1 | pub fn connect() { |
总结
- Crate:最小的编译单元,包含binary crate和library crate。
- Package:包含一个或多个crate,使用
Cargo.toml文件管理。 - Module:用于在crate内部组织代码,可以嵌套和跨文件。
通过crate、package和module的组合,Rust提供了一种灵活且强大的方式来组织和管理代码,使得代码更易于维护和扩展。
在Rust中,Package 是一个包含一个或多个 crate 的文件夹,并且包含一个 Cargo.toml 文件来管理这些 crate 及其依赖项。可以把 Package 理解为一个独立的工作单元,它提供了一组功能,或者一个应用程序。下面是对 Package 及其相关概念的详细解释:
Package 的核心概念
Cargo.toml 文件:每个 Package 都有一个
Cargo.toml文件。这个文件包含了 Package 的元数据,比如名称、版本、依赖项等。Crate:一个 Package 至少包含一个 Crate(可以是 Library Crate 或 Binary Crate),但可以包含多个 Crate。一个 Package 最多包含一个 Library Crate,但可以包含多个 Binary Crate。
src 目录:通常 Package 会有一个
src目录,其中包含源代码文件。
创建 Package
使用 Cargo 工具来创建一个新的 Package。例如,创建一个名为 my_package 的 Package:
1 | cargo new my_package |
这个命令会生成以下目录结构:
1 | my_package |
这个 Package 包含一个 Binary Crate,因为 src 目录下有一个 main.rs 文件。
Cargo.toml 文件
Cargo.toml 文件是 Package 的配置文件,它包含了 Package 的元数据和依赖项信息。以下是一个简单的 Cargo.toml 文件示例:
1 | [package] |
- [package]:包含 Package 的基本信息,如名称、版本、作者和编译版本。
- [dependencies]:列出 Package 所依赖的库。在这个示例中,
serde是一个依赖项。
包含多个 Crate
一个 Package 可以包含一个 Library Crate 和多个 Binary Crate。可以通过创建多个文件来实现。以下是一个包含一个 Library Crate 和两个 Binary Crate 的 Package 示例:
1 | my_package |
- lib.rs:定义了一个 Library Crate。
- main.rs:定义了第一个 Binary Crate。
- bin/another_binary.rs:定义了第二个 Binary Crate。
使用模块组织代码
你可以使用模块在一个 Package 内组织代码。例如:
src/lib.rs:
1 | pub mod network; |
src/network/mod.rs:
1 | pub fn connect() { |
src/network/server.rs:
1 | pub fn start() { |
src/client.rs:
1 | pub fn connect() { |
总结
- Package 是一个包含一个或多个 Crate 的独立工作单元,由一个
Cargo.toml文件管理。 - 每个 Package 至少包含一个 Crate,可以是 Library Crate 或 Binary Crate。
Cargo.toml文件定义了 Package 的元数据和依赖项。- 通过模块(mod)在 Crate 内组织代码,使得代码结构更清晰、更易维护。
这种组织方式使得 Rust 项目可以很容易地扩展和维护,促进代码的重用和模块化。
rust错误处理
Rust 中的错误处理主要通过两种类型实现:Result 和 Option。其中,Result 用于处理可能失败的操作,而 Option 用于处理可能不存在的值。Rust 的错误处理强调显式处理错误,避免了许多其他编程语言中常见的隐式错误传播问题。
1. 使用 Result 处理错误
Result 枚举用于表示可能的成功或失败结果。它有两个变体:
Ok(T):表示操作成功,包含成功的值。Err(E):表示操作失败,包含错误的信息。
示例
假设我们有一个函数用来读取文件内容:
1 | use std::fs::File; |
? 运算符
? 运算符用于简化错误传播。如果函数返回 Result 类型,? 运算符可以在遇到 Err 时返回错误,而在遇到 Ok 时继续执行。
2. 使用 Option 处理值的存在性
Option 枚举用于表示可能存在或不存在的值。它有两个变体:
Some(T):表示值存在,包含值。None:表示值不存在。
示例
假设我们有一个函数用来从数组中获取元素:
1 | fn get_element(arr: &[i32], index: usize) -> Option<i32> { |
3. 自定义错误类型
你可以定义自己的错误类型来处理特定场景下的错误。
示例
1 | use std::fmt; |
4. 错误传播
Rust 的错误传播通过 ? 运算符进行,当函数返回类型是 Result 或 Option 时,? 可以在遇到错误时自动返回。
总结
- Result:用于处理可能失败的操作,具有
Ok(T)和Err(E)变体。 - Option:用于处理可能不存在的值,具有
Some(T)和None变体。 - ? 运算符:用于简化错误传播。
- 自定义错误类型:可以通过实现
std::fmt::Display和std::error::Error特征来自定义错误类型。 - 错误传播:通过
?运算符自动传播错误。
Rust 强调显式错误处理,通过类型系统和模式匹配提供了一种安全且高效的错误处理方式。
rust中的panic
在Rust中,panic! 宏用于处理不可恢复的错误。当程序遇到致命错误时,panic! 宏会终止当前线程的执行,并开始展开(unwind)过程以清理线程中的资源,或直接中止(abort)程序。下面详细介绍 panic! 的机制和处理方式。
1. panic! 的基本用法
panic! 宏通常用于程序遇到无法继续执行的情况,如数组越界、非法状态等。以下是一个简单的示例:
1 | fn main() { |
这段代码会因为试图访问不存在的元素而触发 panic!。panic! 会打印一个错误信息,并终止程序。
2. 捕获 panic:使用 std::panic::catch_unwind
在某些情况下,你可能希望捕获 panic 而不是让程序崩溃。这可以使用 std::panic::catch_unwind 实现,它允许你捕获 panic 并继续执行。
示例
1 | use std::panic; |
在这个示例中,catch_unwind 捕获了 panic,并使程序能够继续运行。
3. 恢复(unwind)与终止(abort)
Rust 在遇到 panic 时有两种处理策略:
展开(unwind):默认情况下,Rust 会展开栈,清理每一层函数的资源。这种方式允许你在某些情况下捕获并恢复
panic,但会增加一些运行时开销。中止(abort):你可以配置Rust在遇到
panic时直接终止程序,而不进行栈展开。这种方式更高效,但不允许你捕获和恢复panic。
你可以在 Cargo.toml 中配置 panic 策略:
1 | [profile.release] |
4. 自定义 panic 行为:std::panic::set_hook
你可以自定义 panic 时的行为,比如记录日志或打印自定义消息。这可以通过 std::panic::set_hook 实现。
示例
1 | use std::panic; |
在这个示例中,set_hook 设置了一个自定义的 panic 钩子函数,当 panic 发生时,会执行这个函数。
5. 避免 panic
尽量在代码中避免使用 panic!,特别是在库代码中。可以通过返回 Result 或 Option 类型来处理可能的错误,从而让调用者决定如何处理错误。
示例:使用 Result 代替 panic
1 | fn divide(a: f64, b: f64) -> Result<f64, String> { |
在这个示例中,divide 函数返回一个 Result 类型,而不是在遇到除零错误时调用 panic!。
总结
panic!:用于处理不可恢复的错误,会终止当前线程的执行。- 捕获
panic:使用std::panic::catch_unwind可以捕获panic并继续执行。 - 恢复与终止:Rust 默认使用展开(unwind)策略,你可以配置为中止(abort)策略。
- 自定义
panic行为:使用std::panic::set_hook可以自定义panic时的行为。 - 避免
panic:尽量使用Result或Option处理可能的错误,避免在库代码中使用panic!。
通过这些机制,Rust 提供了灵活且安全的错误处理方式,使得程序更健壮、更易于维护。
rust泛型特征
Rust 中的泛型(Generics)
泛型允许你编写适用于多种数据类型的代码,而无需重复相同的逻辑。它可以用于函数、结构体、枚举和方法。泛型通过类型参数来实现,这些类型参数通常以尖括号包围。
示例:泛型函数
1 | fn largest<T: PartialOrd>(list: &[T]) -> &T { |
在这个示例中,largest 函数使用泛型 T 来表示它可以处理任何实现了 PartialOrd 特征的类型。
示例:泛型结构体
1 | struct Point<T> { |
在这个示例中,Point 结构体使用泛型 T 来表示 x 和 y 可以是任何类型。
Rust 中的特征(Traits)
特征定义了一组方法签名,任何实现该特征的类型都必须实现这些方法。特征类似于其他语言中的接口。
示例:定义和实现特征
1 | trait Printable { |
在这个示例中,Printable 特征定义了一个 print 方法。Point 结构体实现了这个特征,并提供了 print 方法的具体实现。
示例:带有特征约束的泛型函数
1 | trait Printable { |
在这个示例中,print_item 函数使用泛型 T 并要求 T 实现 Printable 特征。
总结
- 泛型:使代码适用于多种数据类型,通过类型参数表示。
- 用法:函数、结构体、枚举和方法。
- 特征:定义共享行为,类似于接口。
- 用法:定义方法签名,并由具体类型实现。
通过这两个概念,Rust 提供了强大的抽象能力,使得代码更加灵活和可重用。
rust生命周期
在 Rust 中,生命周期(lifetime)是一个重要的概念,用于管理引用的有效范围,确保引用在使用时是有效的。Rust 的生命周期系统是其内存安全的重要保证之一,防止悬垂引用和数据竞争问题。下面是对生命周期的简要说明,以及它与其他语言的不同之处。
Rust 中的生命周期
基本概念:
- 生命周期用来描述引用的有效范围。
- Rust 编译器通过生命周期来检查引用的有效性,确保引用在使用时是有效的。
显式生命周期标注:
- 当函数涉及多个引用参数时,可能需要显式标注生命周期。
- 生命周期参数以单引号开头,如
'a,并放在引用类型的前面。
示例:简单的生命周期标注
1 | fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { |
在这个例子中,'a 是生命周期参数,表示 x、y 和返回值的生命周期是相同的,即它们都必须在调用者的上下文中有效。
与其他语言的不同之处
编译时检查:
- Rust 的生命周期是在编译时检查的,而不像其他一些语言(如 C/C++)在运行时才会出现悬垂指针问题。
- 通过编译时检查,Rust 保证了内存安全,避免了悬垂引用和数据竞争。
显式生命周期注解:
- 在 Rust 中,有时需要显式地标注生命周期,特别是在函数签名中。这在其他语言中通常是隐式处理的。
- 这种显式性增强了代码的可读性和可维护性,明确了引用的关系和作用域。
借用检查器:
- Rust 的借用检查器(borrow checker)会在编译时确保所有引用在其生命周期内有效,防止数据竞争和悬垂指针。
- 这与手动管理内存的语言(如 C/C++)形成鲜明对比,在这些语言中,程序员需要自行保证引用的有效性。
生命周期消除
在许多情况下,Rust 编译器可以通过生命周期消除(lifetime elision)推断生命周期参数,减少显式标注的需要。例如:
1 | fn first_word(s: &str) -> &str { |
总结
- 生命周期:用于管理引用的有效范围,防止悬垂引用和数据竞争。
- 显式标注:需要在函数签名中显式标注生命周期参数,以描述引用之间的关系。
- 编译时检查:Rust 的生命周期检查在编译时进行,确保内存安全。
- 与其他语言不同:Rust 的显式生命周期和编译时检查机制,与其他手动管理内存的语言形成对比,提高了内存安全性和代码可读性。
通过生命周期系统,Rust 在保证内存安全的同时,保持了高效的性能和灵活性。
面向对象
在 Rust 中,虽然没有像 C++ 或 Java 那样直接的面向对象(OO)语法,但它通过结构体、特征(traits)和方法实现了面向对象的核心概念:封装、继承和多态。
封装(Encapsulation)
封装是通过定义结构体(struct)和实现方法(impl)来实现的。
示例:封装
1 | struct Rectangle { |
继承(Inheritance)
Rust 没有传统的继承,但可以通过特征(traits)实现类似的行为。通过特征定义共享的行为,并在结构体中实现这些特征。
示例:继承(通过特征)
1 | trait Shape { |
多态(Polymorphism)
多态通过特征对象(trait objects)和动态分发(dynamic dispatch)实现。可以在运行时通过特征对象调用不同类型的实现。
示例:多态
1 | trait Shape { |
在这个示例中,print_area 函数接受一个实现了 Shape 特征的引用。无论传递的是 Circle 还是 Square,都会调用对应的 area 方法,这就是多态的实现。
总结
- 封装:使用结构体(struct)和方法(impl)实现,将数据和行为封装在一起。
- 继承:通过特征(traits)实现类似继承的行为,共享方法定义。
- 多态:使用特征对象(trait objects)和动态分发(dynamic dispatch)实现多态。
通过这些机制,Rust 提供了强大的抽象能力,同时保持了内存安全和高性能。
rust并发编程
- 并发:在同一时间段内处理多个任务,通过任务切换交替执行。(Concurrency)
- 并行:在同一时刻同时执行多个任务,通过多个处理单元同时执行。(Parallelism)
Rust 的并发编程提供了几种方式来实现线程和任务并行执行,重点在于安全性和效率。以下是一些最常用的方法,用最简单的方式来说明。
1. 使用 std::thread
Rust 的标准库提供了线程支持,可以使用 std::thread 创建和管理线程。
示例:创建一个简单的线程
1 | use std::thread; |
在这个示例中,主线程和新创建的线程同时执行各自的任务。
2. 使用 std::sync::mpsc 进行线程间通信
Rust 提供了 mpsc(multiple producer, single consumer)通道,用于在线程之间传递消息。
示例:在线程之间传递消息
1 | use std::sync::mpsc; |
3. 使用 std::sync::Mutex 进行共享状态的并发访问
Rust 提供了 Mutex 来确保只有一个线程可以访问共享数据。
示例:使用 Mutex 进行共享数据访问
1 | use std::sync::{Arc, Mutex}; |
在这个示例中,Arc(原子引用计数)用于在多个线程之间共享 Mutex,Mutex 确保每次只有一个线程能够修改 counter。
4. 使用 async 和 await 进行异步编程
Rust 的异步编程通过 async 和 await 关键字来实现,通常与异步运行时库(如 Tokio)一起使用。
示例:异步函数和任务
1 | use tokio::time::{sleep, Duration}; |
在这个示例中,两个异步任务 task1 和 task2 被并发执行,并使用 tokio::join! 来等待它们完成。
总结
std::thread:用于创建和管理线程。std::sync::mpsc:用于线程间的消息传递。std::sync::Mutex:用于共享状态的安全访问。async和await:用于异步编程,通常与异步运行时库(如 Tokio)一起使用。
Rust 的并发编程通过这些工具和概念,提供了安全高效的并发执行方式。
好的,在 Rust 中,除了 std::sync::mpsc 提供的通道(channel),还有其他库提供的通道,比如 crossbeam,它提供了更强大和灵活的并发工具。下面我们结合 std::sync::mpsc 和 crossbeam,重新讲解 Rust 中的并发编程。
1. 使用 std::thread
Rust 的标准库提供了线程支持,可以使用 std::thread 创建和管理线程。
示例:创建一个简单的线程
1 | use std::thread; |
2. 使用 std::sync::mpsc 进行线程间通信
Rust 提供了 mpsc(multiple producer, single consumer)通道,用于在线程之间传递消息。
示例:在线程之间传递消息
1 | use std::sync::mpsc; |
3. 使用 std::sync::Mutex 进行共享状态的并发访问
Rust 提供了 Mutex 来确保只有一个线程可以访问共享数据。
示例:使用 Mutex 进行共享数据访问
1 | use std::sync::{Arc, Mutex}; |
在这个示例中,Arc(原子引用计数)用于在多个线程之间共享 Mutex,Mutex 确保每次只有一个线程能够修改 counter。
4. 使用 crossbeam 的通道
crossbeam 是一个更强大和灵活的并发库,提供了多种并发原语,包括通道。
示例:使用 crossbeam 的通道
1 | use crossbeam::channel; |
在这个示例中,crossbeam::channel::unbounded 创建了一个无界通道,允许任意多的消息发送和接收。
5. 使用 async 和 await 进行异步编程
Rust 的异步编程通过 async 和 await 关键字来实现,通常与异步运行时库(如 Tokio)一起使用。
示例:异步函数和任务
1 | use tokio::time::{sleep, Duration}; |
在这个示例中,两个异步任务 task1 和 task2 被并发执行,并使用 tokio::join! 来等待它们完成。
总结
std::thread:用于创建和管理线程。std::sync::mpsc:用于线程间的消息传递,支持多个生产者单个消费者。std::sync::Mutex:用于共享状态的安全访问。crossbeam::channel:提供了更灵活的通道,支持多种并发模式。async和await:用于异步编程,通常与异步运行时库(如 Tokio)一起使用。
Rust 的并发编程通过这些工具和概念,提供了安全高效的并发执行方式,适用于不同的并发场景。
Rust 智能指针
在 Rust 中,智能指针(Smart Pointers)是一种具有指针行为的数据结构,不仅能够指向某个值,还包含额外的元数据和功能。Rust 的智能指针提供了更高的安全性和便捷性,通过所有权和借用规则来管理内存,避免了手动管理内存的常见问题。以下是 Rust 中常用的几种智能指针及其用途。
1. Box<T>
Box<T> 是最简单的智能指针,用于在堆上分配值。它提供了对数据的所有权,但不会引入额外的开销。
示例
1 | fn main() { |
在这个示例中,Box::new 在堆上分配了一个值 5,并返回一个指向该值的 Box<T>。
2. Rc<T>
Rc<T> 是一个引用计数智能指针,用于在多个所有者之间共享数据。它允许多个变量持有同一个值的所有权,但只能用于单线程环境。
示例
1 | use std::rc::Rc; |
在这个示例中,Rc::clone 创建了指向同一值的多个引用,并增加了引用计数。
3. Arc<T>
Arc<T> 是一个原子引用计数智能指针,类似于 Rc<T>,但可以在线程之间安全地共享数据。它用于需要跨线程共享所有权的场景。
示例
1 | use std::sync::Arc; |
在这个示例中,Arc::clone 创建了跨线程共享的引用。
4. RefCell<T>
RefCell<T> 允许在运行时检查借用规则,而不是在编译时。它提供了内部可变性,即使在不可变环境中也可以修改数据。RefCell 仅用于单线程环境。
示例
1 | use std::cell::RefCell; |
在这个示例中,RefCell::borrow_mut 和 RefCell::borrow 用于可变和不可变借用,分别在运行时检查借用规则。
5. Mutex<T>
Mutex<T> 提供互斥锁机制,用于在线程之间安全地共享数据。它确保一次只有一个线程可以访问数据。
示例
1 | use std::sync::{Arc, Mutex}; |
在这个示例中,Mutex::lock 获取互斥锁,确保一次只有一个线程可以访问和修改 counter。
总结
Box<T>:用于在堆上分配值,提供简单的所有权。Rc<T>:用于单线程环境下的引用计数共享所有权。Arc<T>:用于多线程环境下的原子引用计数共享所有权。RefCell<T>:提供运行时的内部可变性检查,适用于单线程环境。Mutex<T>:提供互斥锁机制,确保线程安全的共享访问。
Rust 的智能指针通过所有权、借用和引用计数等机制,提供了安全高效的内存管理,避免了手动管理内存的常见问题。
rust异步编程
Rust 的异步编程(asynchronous programming)提供了一种处理并发任务的方式,避免了传统多线程编程中的复杂性和开销。通过使用 async 和 await 关键字,Rust 能够以异步的方式执行代码,这对于 IO 密集型任务非常有效,比如网络请求、文件操作等。
基本概念
- async/await:Rust 使用
async关键字定义异步函数,使用await关键字等待异步操作完成。 - Future:异步函数返回一个
Future,它代表一个在某个时间点将要完成的值。 - Executor:执行异步任务的运行时环境,常用的有
Tokio和async-std。
async 和 await
使用 async 和 await 关键字可以让你以同步的方式编写异步代码。
示例:基本的 async/await
1 | use tokio::time::{sleep, Duration}; |
在这个示例中,两个异步任务 task1 和 task2 并发执行,并使用 tokio::join! 等待它们完成。
使用 Tokio 运行时
Tokio 是 Rust 中常用的异步运行时,它提供了执行异步任务的环境。
示例:使用 Tokio 进行异步操作
1 | use tokio::time::{sleep, Duration}; |
在这个示例中,perform_task 是一个异步函数,接受任务名称和持续时间。main 函数使用 tokio::join! 并发执行两个任务。
异步函数和 Future
异步函数返回一个 Future,表示一个尚未完成的计算。你可以手动创建和操作 Future。
示例:手动创建 Future
1 | use std::future::Future; |
在这个示例中,SimpleFuture 是一个自定义的 Future,它在第一次调用 poll 时返回 Poll::Pending,在第二次调用时返回 Poll::Ready。
异步编程的优势
- 高效 IO:异步编程可以高效地处理 IO 操作,不需要为每个任务创建一个线程,从而节省资源。
- 提高响应性:通过异步编程,可以在等待某个任务完成时执行其他任务,提高程序的响应性。
使用 async-std
除了 Tokio,还有另一个常用的异步运行时库 async-std。
示例:使用 async-std
1 | use async_std::task; |
在这个示例中,使用 async-std 运行时来执行异步任务。
总结
- async/await:Rust 提供的关键字,用于定义和执行异步函数。
- Future:表示一个尚未完成的计算,异步函数返回
Future。 - Executor:执行异步任务的运行时环境,如
Tokio和async-std。 - 高效 IO 和响应性:异步编程能够高效处理 IO 操作并提高程序的响应性。
通过以上示例和概念,您可以开始在 Rust 中编写和执行异步代码,提高程序的性能和效率。
vim 里面安装 rust
1 | set nocompatible " 必须 |