一、 编译与运行
在 Rust 中,编译和运行代码的常用命令是使用 cargo
,这是 Rust 的包管理和构建工具。以下是使用 cargo
和 rustc
(Rust 编译器)的具体命令:
1. 使用 cargo
工具
cargo
是 Rust 的推荐工具,因为它可以处理依赖管理、构建、测试等多个功能。
-
编译和运行项目:
bashcargo run
该命令将编译项目并运行可执行文件。
cargo run
是一个方便的命令,适用于开发过程中快速测试代码。 -
仅编译项目:
bashcargo build
该命令只会编译项目,而不运行。编译后的可执行文件通常位于
target/debug/
目录下。 -
编译发布版本:
bashcargo build --release
使用
--release
选项来编译一个优化的发布版本。编译后的可执行文件通常位于target/release/
目录下。
2. 使用 rustc
命令
如果你想直接使用 Rust 编译器 rustc
,而不使用 cargo
,可以使用以下命令:
-
编译文件:
bashrustc main.rs
这将使用
rustc
编译器直接编译main.rs
文件,并生成一个名为main
(Windows 下为main.exe
)的可执行文件。 -
运行可执行文件:
编译完成后,运行生成的可执行文件:
bash./main # 在 Unix 或 Linux 系统上 main.exe # 在 Windows 系统上
二、如何显式的处理数字类型的溢出
相关教程章节:https://doc.rust-lang.org/book/ch03-02-data-types.html
Rust 提供了多种方法来处理整数溢出,这些方法可以帮助你根据需要选择适当的策略来应对可能的溢出。让我们逐个分析这些方法,并举例说明它们的用法。
1. wrapping_*
方法
- 描述 :当发生溢出时,
wrapping_*
方法会绕回到类型的最小值(或最大值),即在所有模式下(包括debug
和release
模式)进行"环绕"(wrap around)处理。 - 使用场景:适用于希望溢出后从类型的边界"环绕"回来的情况,比如在循环计数时。
示例:
rust
fn main() {
let max = u8::MAX; // u8 的最大值是 255
let wrapped = max.wrapping_add(1); // 255 + 1 应该是 256,但会"环绕"回 0
println!("Wrapped value: {}", wrapped); // 输出: Wrapped value: 0
}
- 解释:
u8
类型的最大值是 255。wrapping_add(1)
将会"环绕"回 0,因为u8
只能存储 0 到 255 之间的值。
2. checked_*
方法
- 描述 :
checked_*
方法在发生溢出时返回None
,否则返回Some(value)
。 - 使用场景 :适用于需要检测溢出并希望在溢出时采取一些特定措施的情况(例如返回一个
None
值表示错误)。
示例
rust
fn main() {
let max = u8::MAX; // u8 的最大值是 255
let checked = max.checked_add(1); // 255 + 1 会溢出
match checked {
Some(value) => println!("Checked value: {}", value),
None => println!("Overflow occurred!"), // 输出: Overflow occurred!
}
}
- 解释:
checked_add(1)
会检测到溢出,因为 255 + 1 超出了u8
类型的范围,因此返回None
。
3. overflowing_*
方法
- 描述 :
overflowing_*
方法返回一个元组(value, bool)
,其中value
是操作结果,bool
表示是否发生了溢出(true
表示发生溢出)。 - 使用场景:适用于需要知道溢出发生与否,同时还需要获取操作结果的情况。
示例
rust
fn main() {
let max = u8::MAX; // u8 的最大值是 255
let (overflowed_value, did_overflow) = max.overflowing_add(1); // 255 + 1 会溢出
println!("Overflowing value: {}", overflowed_value); // 输出: Overflowing value: 0
println!("Did overflow: {}", did_overflow); // 输出: Did overflow: true
}
- 解释:
overflowing_add(1)
返回一个元组,其中第一个值是溢出后的结果(即环绕后的值 0),第二个值是一个布尔值,表示是否发生了溢出(true
表示发生了溢出)。
4. saturating_*
方法
- 描述 :
saturating_*
方法在溢出时返回类型的最小值或最大值,而不是产生环绕效果。 - 使用场景:适用于希望结果在类型的范围内"饱和"(即限制在最大值或最小值)而不发生环绕的情况,比如计算结果不能超过一个特定的上限或下限。
示例
rust
fn main() {
let max = u8::MAX; // u8 的最大值是 255
let saturated = max.saturating_add(1); // 255 + 1 会溢出
println!("Saturating value: {}", saturated); // 输出: Saturating value: 255
}
- 解释:
saturating_add(1)
在发生溢出时不会绕回到 0,而是返回u8
类型的最大值(255),这是"饱和"效果的表现。
总结
wrapping_*
方法:当发生溢出时,结果"环绕"回类型的边界值。checked_*
方法 :当发生溢出时,返回None
,否则返回Some(value)
。overflowing_*
方法 :返回一个元组(value, bool)
,表示结果和是否发生溢出。saturating_*
方法:当发生溢出时,返回类型的最小值或最大值(不发生环绕)。
三、控制台打印中{}
和{:?}
的区别
在 Rust 中,{:?}
和 {}
都是格式化占位符,但它们的用途和作用有所不同。
{:?}
和 {}
的区别
-
{:?}
:表示调试格式化 (debug formatting)。它用于打印那些实现了Debug
trait 的类型的值。它的输出通常更详细,适用于开发和调试用途。使用{:?}
时,Rust 不仅会打印变量的值,还会尽可能地显示它们的内部结构。 -
{}
:表示显示格式化 (display formatting)。它用于打印那些实现了Display
trait 的类型的值。Display
trait 更像是用户友好的输出格式,而不是用于调试的信息。并不是所有类型都实现了Display
trait,例如,常见的 Rust 标准类型(如Option
或Result
)默认没有实现Display
。
为什么用 {:?}
而不是 {}
在 Rust 中,大多数基本数据类型(如 bool
、i32
、f64
等)都同时实现了 Debug
和 Display
trait,因此你可以使用 {:?}
或 {}
来打印它们的值。但是,使用 {:?}
更通用,它适用于更广泛的类型。
例子:
rust
println!("a = {:?}, b = {:?}", a, b);
这是用来打印布尔值 a
和 b
。虽然 bool
类型同时实现了 Debug
和 Display
,但一般情况下:
-
使用
{:?}
是一种好习惯 ,因为它可以打印更复杂的数据结构,例如元组、数组、向量、枚举等。在这些情况下,{:?}
可以打印更多细节而无需更改代码。 -
{:?}
更通用 ,它能确保你在需要打印复杂数据结构时不出错,而{}
只能用于实现了Display
的类型。
示例:{:?}
vs {}
让我们看一个例子来更好地理解这两个占位符的区别。
使用 {:?}
调试格式化
rust
fn main() {
let tuple = (42, "hello", vec![1, 2, 3]);
// 使用调试格式化打印
println!("{:?}", tuple);
}
输出:
(42, "hello", [1, 2, 3])
- 这里使用
{:?}
成功打印了元组的所有内容,包含整数、字符串和向量的详细信息。
使用 {}
显示格式化
rust
fn main() {
let tuple = (42, "hello", vec![1, 2, 3]);
// 使用显示格式化打印
println!("{}", tuple); // 编译错误
}
输出:
error[E0277]: `({integer}, &str, std::vec::Vec<{integer}>)` doesn't implement `std::fmt::Display`
- 上述代码会产生编译错误,因为元组
(42, "hello", vec![1, 2, 3])
没有实现Display
trait,因此不能使用{}
来打印它。
总结
{:?}
:用于调试格式化,适用于所有实现了Debug
trait 的类型。更通用,更适合打印复杂的数据结构和调试输出。{}
:用于显示格式化,适用于实现了Display
trait 的类型。输出更简洁,但不适用于所有类型。
四、BACKTRACE
在 Rust 中,当程序发生 panic(恐慌)时,会打印一个错误消息,描述导致 panic 的原因。
note: run with RUST_BACKTRACE=1 environment variable to display a backtrace
的含义
这条提示信息的意思是:如果你希望看到导致 panic 的完整调用栈(backtrace),可以运行程序时设置环境变量 RUST_BACKTRACE=1
。
什么是 Backtrace?
- Backtrace 是一种调试信息,它显示了程序在发生 panic 时的调用堆栈(函数调用的路径)。
- 通过查看 backtrace,可以了解程序是如何到达 panic 点的,这对调试程序非常有用。
- 在 Rust 中,默认情况下,当程序发生 panic 时,Rust 只会显示一个简短的错误信息(如你看到的那样),不会显示完整的 backtrace。
如何启用 Backtrace?
要启用 backtrace,可以在运行程序时设置 RUST_BACKTRACE
环境变量为 1
。这样 Rust 会在 panic 发生时打印出更详细的调用堆栈。
示例:在不同平台上启用 Backtrace
-
Linux/macOS:
在终端中运行以下命令:
bashRUST_BACKTRACE=1 cargo run
或者如果你直接使用编译后的可执行文件,可以这样运行:
bashRUST_BACKTRACE=1 ./your_program_name
-
Windows:
在命令提示符中运行以下命令:
bashset RUST_BACKTRACE=1 cargo run
或者,如果你直接运行可执行文件,可以这样运行:
bashset RUST_BACKTRACE=1 your_program_name.exe
启用 Backtrace 后的输出
启用 backtrace 后,运行程序时,你会看到类似以下的输出:
plaintext
thread 'main' panicked at 'index out of bounds: the len is 5 but the index is 10', src/main.rs:19:19
stack backtrace:
0: std::panicking::begin_panic
1: core::panicking::panic_bounds_check
2: <your_function_name>
...
8: main
9: __libc_start_main
10: _start
解释
- 这些信息显示了程序中发生 panic 的函数调用栈。每一行对应一个函数调用。
- 通过查看这些信息,你可以追踪导致 panic 的路径,找到出错的代码段并进行修复。
五、解构式赋值
这段教程介绍了 Rust 1.59 版本后支持的一种新的赋值方式------解构式赋值 。这种方式允许在赋值语句中使用元组、切片和结构体的模式,从而直接给多个变量赋值或更新变量的值。它类似于 let
语句的解构绑定,但不同的是,let
语句用于变量的初次绑定,而解构式赋值用于已经绑定的变量的再赋值。
什么是解构式赋值?
解构式赋值 是一种赋值方法,它允许你使用模式匹配的方式将值"解构"并赋给多个变量。在 Rust 1.59 之前,这种操作只能在 let
语句中使用,而在 Rust 1.59 之后,它被扩展到普通的赋值语句中,使得代码更加简洁和易于理解。
解释代码示例
我们来看一下你的代码示例,并逐步分析每一行:
rust
struct Struct {
e: i32
}
fn main() {
let (a, b, c, d, e);
(a, b) = (1, 2);
// _ 代表匹配一个值,但是我们不关心具体的值是什么,因此没有使用一个变量名而是使用了 _
[c, .., d, _] = [1, 2, 3, 4, 5];
Struct { e, .. } = Struct { e: 5 };
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
}
1. 结构体定义
rust
struct Struct {
e: i32
}
- 定义了一个简单的结构体
Struct
,它只有一个字段e
,类型是i32
。
2. 解构式赋值的例子
rust
fn main() {
let (a, b, c, d, e);
- 这里用
let
声明了五个变量a
,b
,c
,d
, 和e
,但是没有为它们赋值。
rust
(a, b) = (1, 2);
- 解构式赋值 :将元组
(1, 2)
中的值解构并分别赋给a
和b
。结果是a = 1
,b = 2
。
rust
[c, .., d, _] = [1, 2, 3, 4, 5];
- 这里我们对数组
[1, 2, 3, 4, 5]
使用了解构赋值。c
取得第一个元素1
。..
表示忽略中间的元素(这里是[2, 3]
)。d
取得倒数第二个元素4
。_
匹配最后一个元素5
,但我们不关心它的值,因此使用_
忽略掉。- 结果是
c = 1
,d = 4
。
rust
Struct { e, .. } = Struct { e: 5 };
- 对结构体进行解构赋值。
Struct { e, .. }
是一个模式,匹配Struct
结构体类型。..
表示忽略结构体中的其他字段(如果有的话)。e
取得结构体中e
字段的值(这里是5
)。- 结果是
e = 5
。
3. 断言
rust
assert_eq!([1, 2, 1, 4, 5], [a, b, c, d, e]);
assert_eq!
宏用于断言两个值相等。- 它比较数组
[1, 2, 1, 4, 5]
和变量的值[a, b, c, d, e]
是否相等。 - 由于前面的解构式赋值,我们知道
a = 1
,b = 2
,c = 1
,d = 4
,e = 5
,因此断言成功,程序继续执行。
解构式赋值 vs. let
绑定
-
let
绑定 :在初次声明变量时使用。let
语句会新建变量,并绑定一个值或解构多个值。rustlet (x, y) = (10, 20); // 新建变量 x 和 y,并赋值
-
解构式赋值:在已经声明的变量上使用。解构式赋值仅仅是对已存在变量的再赋值。
rustlet x; x = 5; // 给已声明的变量 x 赋值
注意事项
-
使用
+=
等运算符进行赋值时,还不支持解构式赋值。这意味着你不能对解构后的变量直接使用+=
等操作符,例如:rust(x, y) += (1, 2); // 这是不允许的,编译时会报错
总结
- 解构式赋值 是一种灵活的赋值方式,它允许你在赋值语句中使用元组、切片和结构体模式进行解构。
- 它保持了与
let
语句的使用一致性,但用于已经声明的变量,而不是创建新的变量。 - 使用这种方式可以简化对多值赋值的操作,使代码更加简洁和易读。