在Ubuntu上用Rust编写第一个程序
从你的输出可以看出,Rust已经成功安装在你的Ubuntu系统上了。现在我们来编写并运行第一个Rust程序,整个过程需要几个简单的步骤:
一、配置Shell环境(如果需要)
虽然安装提示可能需要重启Shell,但你也可以直接在当前会话中配置环境:
# 对于bash/zsh等shell
. "$HOME/.cargo/env"
# 如果你使用fish shell
source "$HOME/.cargo/env.fish"
# 验证Cargo是否可用
cargo --version
二、创建第一个Rust项目
Rust通过Cargo工具管理项目,它会自动生成基础项目结构:
# 创建一个新的Rust项目,项目名为"hello_rust"
cargo new hello_rust --bin
# 进入项目目录
cd hello_rust
# 查看项目结构
tree -L 2
执行后会看到以下结构:
hello_rust/
├── Cargo.toml # 项目配置文件
└── src
└── main.rs # 主程序文件
三、编写第一个Rust程序
Rust的默认模板已经生成了一个"Hello, world!"程序,我们可以直接使用或修改它:
# 编辑main.rs文件
nano src/main.rs
文件内容默认是:
fn main() {
println!("Hello, world!");
}
你也可以修改为更有意义的内容,例如:
fn main() {
println!("这是我的第一个Rust程序!");
println!("当前时间: {}", chrono::Local::now());
}
四、添加依赖(如果需要)
上面的例子使用了chrono
库来获取时间,需要在Cargo.toml
中添加依赖:
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"
# 依赖部分
[dependencies]
chrono = "0.4"
五、编译并运行程序
Rust有两种运行方式:直接运行或编译后运行:
# 方式1:直接编译并运行(推荐)
cargo run
# 方式2:仅编译(生成可执行文件在target/debug目录)
cargo build
# 运行编译后的可执行文件
./target/debug/hello_rust
六、运行结果
如果一切正常,你将看到类似以下输出:
这是我的第一个Rust程序!
当前时间: 2025-06-25 10:30:45.123456 +08:00
进阶提示
Rust文档:查看官方文档了解更多语法和特性
rustup doc # 本地查看文档
项目发布:如果需要发布项目,使用release模式编译
cargo build --release # 生成优化后的可执行文件
包管理:使用
cargo add
命令快速添加依赖cargo add chrono # 自动添加依赖到Cargo.toml
通过这个简单的例子,你已经掌握了Rust开发的基本流程。Rust以内存安全和高性能著称,接下来可以尝试学习更多特性,比如所有权系统、泛型和异步编程等。
在Rust中,函数是组织代码的基本单元,它允许你将代码逻辑封装成可复用的模块。以下是关于Rust函数的详细介绍:
一、函数定义
Rust使用fn
关键字定义函数,遵循以下语法:
fn 函数名(参数1: 类型, 参数2: 类型) -> 返回类型 {
// 函数体
// 最后一行(无分号)作为返回值
}
示例1:无参数、无返回值
fn say_hello() {
println!("Hello, Rust!");
}
示例2:带参数、无返回值
fn greet(name: String) {
println!("Hello, {}!", name);
}
示例3:带参数和返回值
fn add(a: i32, b: i32) -> i32 {
a + b // 无分号,表示返回值
}
// 等价于(使用return语句)
fn add_with_return(a: i32, b: i32) -> i32 {
return a + b;
}
二、函数参数
Rust要求参数必须显式声明类型,支持以下参数类型:
1. 值传递(Copy类型)
fn square(x: i32) -> i32 {
x * x
}
2. 引用传递
fn append_world(s: &mut String) {
s.push_str(" world");
}
3. 解构参数
fn print_coordinates((x, y): (i32, i32)) {
println!("坐标: ({}, {})", x, y);
}
三、函数返回值
Rust通过-> 类型
声明返回值类型,支持以下方式:
1. 单值返回
fn get_length(s: String) -> usize {
s.len()
}
2. 元组返回(多值)
fn calculate(a: i32, b: i32) -> (i32, i32, i32) {
(a + b, a - b, a * b)
}
3. 无返回值(单元类型()
)
fn log_error(message: &str) -> () {
eprintln!("错误: {}", message);
}
// 等价于(可省略返回类型)
fn log_error_simplified(message: &str) {
eprintln!("错误: {}", message);
}
四、函数调用
通过函数名和参数列表调用函数:
fn main() {
// 调用无参数函数
say_hello(); // 输出: Hello, Rust!
// 调用带参数函数
greet("Alice".to_string()); // 输出: Hello, Alice!
// 调用带返回值函数
let result = add(3, 5);
println!("3 + 5 = {}", result); // 输出: 8
// 解构多返回值
let (sum, diff, prod) = calculate(4, 2);
println!("和: {}, 差: {}, 积: {}", sum, diff, prod); // 输出: 6, 2, 8
// 引用传递示例
let mut msg = String::from("Hello");
append_world(&mut msg);
println!("{}", msg); // 输出: Hello world
}
五、函数作为参数和返回值
Rust支持高阶函数,允许函数作为参数或返回值:
1. 函数作为参数
fn apply_twice(f: fn(i32) -> i32, x: i32) -> i32 {
f(f(x))
}
fn double(n: i32) -> i32 {
n * 2
}
fn main() {
let result = apply_twice(double, 3);
println!("3翻倍两次: {}", result); // 输出: 12
}
2. 函数作为返回值
fn get_adder() -> fn(i32) -> i32 {
fn add_five(x: i32) -> i32 {
x + 5
}
add_five
}
fn main() {
let adder = get_adder();
println!("2 + 5 = {}", adder(2)); // 输出: 7
}
六、闭包(匿名函数)
Rust的闭包是轻量级的匿名函数,可捕获环境变量:
fn main() {
let x = 10;
// 闭包捕获x
let add_x = |a| a + x;
println!("5 + 10 = {}", add_x(5)); // 输出: 15
// 作为参数传递闭包
let numbers = vec![1, 2, 3];
let sum: i32 = numbers.iter().fold(0, |acc, x| acc + x);
println!("总和: {}", sum); // 输出: 6
}
七、方法(关联函数)
方法是定义在结构体或枚举上的函数,使用impl
块:
struct Rectangle {
width: u32,
height: u32,
}
impl Rectangle {
// 实例方法
fn area(&self) -> u32 {
self.width * self.height
}
// 关联函数(类似构造器)
fn square(size: u32) -> Rectangle {
Rectangle {
width: size,
height: size,
}
}
}
fn main() {
let rect = Rectangle { width: 3, height: 4 };
println!("面积: {}", rect.area()); // 输出: 12
let sq = Rectangle::square(5);
println!("正方形面积: {}", sq.area()); // 输出: 25
}
总结
Rust的函数设计强调类型安全和所有权,通过fn
关键字定义,支持参数类型声明、显式返回值、高阶函数和闭包。掌握函数是学习Rust的基础,后续可以结合结构体、枚举和trait进一步构建复杂系统。
在Rust中,错误处理是一个核心设计,它通过类型系统强制你显式处理可能的错误情况,避免隐藏的崩溃。Rust主要使用两种方式处理错误:可恢复错误(Result<T, E>
)和不可恢复错误(panic!
)。
一、不可恢复错误:panic!
当程序遇到无法继续执行的严重问题时,使用panic!
宏终止程序。
1. 主动触发panic
fn main() {
panic!("发生了严重错误!"); // 程序崩溃并打印错误信息
}
2. 自动触发panic
Rust在运行时检测到严重错误(如越界访问)时会自动panic:
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[100]); // 越界访问,触发panic
}
二、可恢复错误:Result<T, E>
对于可能失败但可以恢复的操作,Rust使用Result<T, E>
枚举:
enum Result<T, E> {
Ok(T), // 成功,包含结果值
Err(E), // 失败,包含错误信息
}
1. 基本用法
use std::fs::File;
fn main() {
let file = File::open("hello.txt"); // 返回Result<File, Error>
match file {
Ok(file) => println!("文件打开成功"),
Err(error) => println!("打开文件失败: {}", error),
}
}
2. 传播错误(Error Propagation)
使用?
操作符将错误快速返回给调用者:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> io::Result<String> {
let mut file = File::open("hello.txt")?; // 失败时直接返回错误
let mut username = String::new();
file.read_to_string(&mut username)?; // 失败时直接返回错误
Ok(username)
}
// 简化版本(链式调用)
fn read_username_from_file_shorter() -> io::Result<String> {
let mut username = String::new();
File::open("hello.txt")?.read_to_string(&mut username)?;
Ok(username)
}
三、处理错误的常用方法
1. unwrap()
和 expect()
快速获取Ok
值,失败时panic:
fn main() {
let file = File::open("hello.txt").unwrap(); // 失败时panic并显示默认错误
let file = File::open("hello.txt").expect("无法打开文件"); // 自定义错误信息
}
2. match
模式匹配
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("除数不能为零".to_string())
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("结果: {}", result),
Err(msg) => println!("错误: {}", msg),
}
}
3. if let
和 while let
fn main() {
// 只处理成功情况,忽略错误
if let Ok(file) = File::open("hello.txt") {
println!("文件打开成功");
}
// 循环处理迭代器中的Result
let results = vec![Ok(1), Err("错误"), Ok(3)];
for result in results {
if let Err(e) = result {
println!("处理错误: {}", e);
break;
}
}
}
4. map
和 and_then
链式处理Result
:
fn parse_number(s: &str) -> Result<i32, String> {
s.parse().map_err(|_| "不是有效数字".to_string())
}
fn main() {
let result = parse_number("42")
.map(|n| n * 2)
.and_then(|n| Ok(n + 10));
println!("结果: {:?}", result); // Ok(94)
}
四、自定义错误类型
通过实现std::error::Error
trait创建自定义错误:
use std::error::Error;
use std::fmt;
// 定义自定义错误类型
#[derive(Debug)]
enum MyError {
InvalidInput(String),
CalculationError,
}
// 实现Error trait
impl Error for MyError {}
// 实现Display trait
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::InvalidInput(msg) => write!(f, "无效输入: {}", msg),
MyError::CalculationError => write!(f, "计算错误"),
}
}
}
// 使用自定义错误的函数
fn calculate(x: i32, y: i32) -> Result<i32, MyError> {
if y == 0 {
Err(MyError::InvalidInput("除数不能为零".to_string()))
} else if x < 0 || y < 0 {
Err(MyError::CalculationError)
} else {
Ok(x / y)
}
}
五、panic!
vs Result
的选择原则
使用
panic!
的场景:- 示例代码、原型开发
- 测试环境
- 程序处于无法恢复的状态(如配置文件缺失)
使用
Result
的场景:- 函数可能因为外部因素失败(如文件不存在)
- 函数的调用者需要处理不同的错误情况
- 错误是业务逻辑的一部分(如用户输入验证)
六、实战示例
use std::fs::File;
use std::io::{self, Read};
fn read_and_parse_file() -> Result<i32, io::Error> {
let mut file = File::open("config.txt")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
// 解析整数(简化示例,实际应处理解析错误)
let number = content.trim().parse::<i32>()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
Ok(number)
}
fn main() {
match read_and_parse_file() {
Ok(num) => println!("配置值: {}", num),
Err(e) => println!("读取配置失败: {}", e),
}
}
总结
Rust的错误处理设计迫使你在代码中显式处理可能的错误,通过Result<T, E>
和panic!
分别处理可恢复和不可恢复的错误。这种设计使程序更加健壮,减少了运行时崩溃的可能性。掌握错误处理是编写高质量Rust代码的关键。
在Rust中,自定义错误类型是构建健壮应用程序的重要部分。通过创建特定于领域的错误类型,可以清晰地表达可能出现的问题,并提供友好的错误处理体验。以下是自定义错误类型的几种常见方法:
一、使用enum
定义简单错误类型
最常见的方式是用enum
枚举不同的错误变体,然后实现必要的trait。
1. 基础实现
use std::fmt;
// 定义错误类型
#[derive(Debug)]
enum MyError {
InvalidInput(String),
DatabaseError(String),
NetworkFailure,
}
// 实现Display trait(必需)
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::InvalidInput(msg) => write!(f, "无效输入: {}", msg),
MyError::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
MyError::NetworkFailure => write!(f, "网络连接失败"),
}
}
}
// 实现Error trait(可选,但推荐)
impl std::error::Error for MyError {}
2. 使用错误类型
fn connect_to_database(url: &str) -> Result<(), MyError> {
if url.is_empty() {
Err(MyError::InvalidInput("数据库URL不能为空".to_string()))
} else if !url.starts_with("postgres://") {
Err(MyError::InvalidInput("不支持的数据库类型".to_string()))
} else {
// 模拟连接成功
println!("连接到数据库: {}", url);
Ok(())
}
}
fn main() {
match connect_to_database("") {
Ok(_) => println!("连接成功"),
Err(e) => println!("错误: {}", e), // 输出: 错误: 无效输入: 数据库URL不能为空
}
}
二、包装外部错误(Error Wrapping)
当需要处理多个外部错误类型时,可以使用thiserror
crate简化实现。
1. 添加依赖
# Cargo.toml
[dependencies]
thiserror = "1.0"
anyhow = "1.0" # 可选,用于简化错误处理
2. 使用thiserror
定义错误类型
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("解析配置失败: {0}")]
ParseError(#[from] std::num::ParseIntError), // 自动转换
#[error("I/O操作失败: {0}")]
IoError(#[from] std::io::Error), // 自动转换
#[error("网络请求失败: {status_code} - {message}")]
NetworkError {
status_code: u16,
message: String,
},
}
3. 使用包装的错误类型
use std::fs::File;
use std::io::Read;
fn read_config() -> Result<i32, AppError> {
let mut file = File::open("config.txt")?; // 自动转换为AppError::IoError
let mut content = String::new();
file.read_to_string(&mut content)?;
content.trim().parse::<i32>() // 自动转换为AppError::ParseError
.map(|num| num * 2)
}
fn fetch_data(url: &str) -> Result<String, AppError> {
// 模拟网络请求
if url.is_empty() {
Err(AppError::NetworkError {
status_code: 400,
message: "无效URL".to_string(),
})
} else {
Ok("模拟数据".to_string())
}
}
三、实现错误转换(From Trait)
手动实现From
trait可以将其他错误类型转换为自定义错误。
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
struct MathError {
details: String,
}
impl MathError {
fn new(msg: &str) -> MathError {
MathError { details: msg.to_string() }
}
}
impl fmt::Display for MathError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "数学错误: {}", self.details)
}
}
impl std::error::Error for MathError {}
// 实现从ParseIntError到MathError的转换
impl From<ParseIntError> for MathError {
fn from(err: ParseIntError) -> Self {
MathError::new(&err.to_string())
}
}
// 使用?操作符自动转换错误
fn calculate(input: &str) -> Result<i32, MathError> {
let num = input.parse::<i32>()?; // 自动转换ParseIntError
if num < 0 {
Err(MathError::new("输入不能为负数"))
} else {
Ok(num * 2)
}
}
四、使用anyhow
进行快速原型开发
anyhow
crate提供了灵活的错误类型anyhow::Error
,适合快速开发:
use anyhow::{anyhow, Result};
use std::fs::File;
use std::io::Read;
fn read_config() -> Result<String> {
let mut file = File::open("config.toml")
.map_err(|e| anyhow!("打开配置文件失败: {}", e))?;
let mut content = String::new();
file.read_to_string(&mut content)?;
if content.is_empty() {
Err(anyhow!("配置文件为空"))
} else {
Ok(content)
}
}
fn main() -> Result<()> {
let config = read_config()?;
println!("配置内容: {}", config);
Ok(())
}
五、错误处理最佳实践
- 保持错误类型简洁:只包含必要的信息。
- 提供上下文:在错误中包含足够的上下文信息,便于调试。
- 使用
thiserror
:对于复杂应用,使用thiserror
减少样板代码。 - 避免过度包装:只在需要抽象错误细节时进行包装。
- 考虑错误范围:
- 模块内部使用具体错误类型
- 对外API使用更通用的错误类型(如
anyhow::Error
)
总结
Rust提供了多种方式自定义错误类型,从简单的enum
到使用thiserror
和anyhow
等工具库。选择合适的方式取决于项目的复杂度和需求:
- 简单项目:直接使用
enum
和手动实现trait - 复杂项目:使用
thiserror
简化错误定义 - 快速原型:使用
anyhow
提供灵活的错误处理
通过清晰定义的错误类型,你的代码将更加健壮,错误处理也会更加优雅。
在Rust中,跨函数传递和处理自定义错误主要通过**错误传播(Error Propagation)和错误转换(Error Conversion)**实现。以下是具体方法和最佳实践:
一、错误传播:使用Result
和?
操作符
最基本的方式是通过Result<T, E>
返回错误,并使用?
操作符自动传播错误。
1. 自定义错误类型
use std::fmt;
#[derive(Debug)]
enum MyError {
InvalidInput(String),
DatabaseError(String),
NetworkFailure,
}
impl fmt::Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
MyError::InvalidInput(msg) => write!(f, "无效输入: {}", msg),
MyError::DatabaseError(msg) => write!(f, "数据库错误: {}", msg),
MyError::NetworkFailure => write!(f, "网络失败"),
}
}
}
impl std::error::Error for MyError {}
2. 多层函数传递错误
fn validate_input(input: &str) -> Result<String, MyError> {
if input.is_empty() {
Err(MyError::InvalidInput("输入不能为空".to_string()))
} else {
Ok(input.to_string())
}
}
fn query_database(query: &str) -> Result<String, MyError> {
// 模拟数据库查询
if query.contains("DROP") {
Err(MyError::DatabaseError("危险查询".to_string()))
} else {
Ok("查询结果".to_string())
}
}
fn fetch_data(input: &str) -> Result<String, MyError> {
let validated = validate_input(input)?; // 传播错误
let result = query_database(&validated)?; // 传播错误
Ok(result)
}
fn main() {
match fetch_data("SELECT * FROM users") {
Ok(data) => println!("数据: {}", data),
Err(e) => println!("错误: {}", e),
}
}
二、错误转换:处理不同来源的错误
当函数可能返回多种错误类型时,需要将它们转换为统一的错误类型。
1. 使用thiserror
自动转换
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("解析错误: {0}")]
ParseError(#[from] std::num::ParseIntError),
#[error("IO错误: {0}")]
IoError(#[from] std::io::Error),
#[error("自定义错误: {0}")]
Custom(String),
}
fn parse_number(s: &str) -> Result<i32, AppError> {
s.parse::<i32>() // 自动转换ParseIntError
.map(|n| n * 2)
}
fn read_file() -> Result<String, AppError> {
let mut file = std::fs::File::open("data.txt")?; // 自动转换IoError
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
fn process_input(s: &str) -> Result<i32, AppError> {
if s.len() > 10 {
Err(AppError::Custom("输入太长".to_string()))
} else {
parse_number(s)
}
}
2. 手动实现From
trait
impl From<std::io::Error> for MyError {
fn from(err: std::io::Error) -> Self {
MyError::DatabaseError(format!("IO操作失败: {}", err))
}
}
fn read_config() -> Result<String, MyError> {
let mut file = std::fs::File::open("config.txt")?; // 转换IoError
let mut content = String::new();
file.read_to_string(&mut content)?;
Ok(content)
}
三、组合多个错误类型
当模块需要处理多种错误时,可以创建一个统一的错误类型。
1. 使用enum
组合错误
#[derive(Error, Debug)]
enum ServiceError {
#[error("验证失败: {0}")]
Validation(String),
#[error("数据库错误: {0}")]
Database(#[from] sqlx::Error),
#[error("API错误: {0}")]
Api(#[from] reqwest::Error),
}
async fn authenticate_user(email: &str, password: &str) -> Result<User, ServiceError> {
if !email.contains('@') {
return Err(ServiceError::Validation("无效邮箱".to_string()));
}
let user = sqlx::query_as!(User, "SELECT * FROM users WHERE email = ?", email)
.fetch_one(&db_pool)
.await?; // 转换sqlx::Error
if !verify_password(&user.hashed_password, password)? {
return Err(ServiceError::Validation("密码错误".to_string()));
}
Ok(user)
}
四、全局错误处理策略
在应用程序顶层统一处理错误。
1. 主函数处理错误
fn main() -> Result<(), AppError> {
let config = read_config()?;
let number = parse_number(&config)?;
println!("处理结果: {}", number);
Ok(())
}
2. Web服务中的错误处理
use actix_web::{web, App, HttpResponse, HttpServer, Result};
async fn health_check() -> Result<HttpResponse> {
// 业务逻辑...
Ok(HttpResponse::Ok().finish())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/health", web::get().to(health_check))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
五、错误处理最佳实践
错误类型设计:
- 保持错误类型层次清晰
- 为每个错误变体提供足够的上下文信息
错误转换:
- 使用
thiserror
自动实现From
trait - 避免深层嵌套的错误类型
- 使用
错误处理:
- 在边界层处理错误(如API端点、CLI入口)
- 使用
map_err()
添加额外上下文
fn load_config() -> Result<Config, AppError> { std::fs::read_to_string("config.toml") .map_err(|e| AppError::ConfigError(format!("读取配置失败: {}", e)))? .parse() .map_err(|e| AppError::ConfigError(format!("解析配置失败: {}", e))) }
日志记录:
- 在处理错误时记录错误信息
fn main() { if let Err(e) = run_app() { eprintln!("应用程序错误: {}", e); std::process::exit(1); } }
总结
Rust的错误处理机制通过Result
、?
操作符和From
trait提供了强大而类型安全的错误传递方式:
- 使用自定义
enum
定义特定领域的错误 - 通过
?
操作符自动传播错误 - 实现
From
trait转换不同来源的错误 - 在顶层统一处理错误,提供友好的用户反馈
这种设计确保错误不会被忽略,同时保持代码的简洁性和可维护性。
在Rust中,不可恢复的错误通常指程序无法继续运行的严重问题。Rust提供了多种机制来处理这类错误,以下是详细介绍:
一、使用panic!
宏
panic!
是处理不可恢复错误的主要方式,它会导致程序立即终止并展开调用栈。
1. 主动触发panic
fn main() {
// 手动触发panic并输出错误信息
panic!("程序遇到致命错误,无法继续运行!");
}
2. 自动触发panic
Rust在运行时检测到严重错误(如越界访问)时会自动panic:
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[100]); // 越界访问,触发panic
}
二、使用unwrap()
和expect()
当你确信某个操作不会失败,但编译器无法推导时,可以使用unwrap()
或expect()
快速获取结果,失败时触发panic。
1. unwrap()
fn main() {
let num = "42".parse::<i32>().unwrap(); // 成功时返回42
let invalid = "abc".parse::<i32>().unwrap(); // 失败时panic,显示默认错误
}
2. expect()
fn main() {
let file = std::fs::File::open("config.txt")
.expect("无法打开配置文件"); // 自定义错误信息
}
三、使用unreachable!()
当代码执行到逻辑上不可能到达的位置时,使用unreachable!()
触发panic:
fn get_day_name(day: u8) -> &'static str {
match day {
1 => "Monday",
2 => "Tuesday",
3 => "Wednesday",
4 => "Thursday",
5 => "Friday",
6 => "Saturday",
7 => "Sunday",
_ => unreachable!("Invalid day: {}", day), // 理论上不会执行
}
}
四、自定义panic行为
通过std::panic::set_hook
可以自定义panic时的行为,例如记录日志:
use std::panic;
fn main() {
// 设置panic钩子
panic::set_hook(Box::new(|info| {
let location = info.location().unwrap();
let msg = match info.payload().downcast_ref::<&'static str>() {
Some(s) => *s,
None => match info.payload().downcast_ref::<String>() {
Some(s) => &s[..],
None => "未知panic信息",
},
};
eprintln!("在 {}:{} 处发生panic: {}", location.file(), location.line(), msg);
}));
panic!("测试自定义panic处理");
}
五、在测试中使用panic
测试中常用panic验证预期行为:
#[test]
#[should_panic(expected = "除数不能为零")]
fn test_divide_by_zero() {
divide(10, 0);
}
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("除数不能为零");
}
a / b
}
六、不可恢复错误的适用场景
- 示例代码和原型开发:快速验证概念时,无需处理所有可能的错误。
- 测试环境:确保测试失败时明确指出问题。
- 不可恢复的状态:
- 关键配置文件缺失
- 数据库连接失败
- 资源耗尽(如内存不足)
- 编程错误:
- 无效的函数参数
- 越界访问
- 空指针解引用
七、与可恢复错误的对比
场景 | 不可恢复错误(panic!) | 可恢复错误(Result) |
---|---|---|
错误类型 | 编程错误或不可恢复的系统状态 | 预期可能发生的错误 |
处理方式 | 程序终止 | 函数返回Result,由调用者处理 |
使用时机 | 示例代码、测试、关键错误 | 外部资源操作(文件、网络等) |
代码示例 | panic!("数据库连接失败"); |
return Err(DbError::ConnectFailed); |
总结
Rust通过panic!
机制强制你明确区分可恢复和不可恢复的错误:
- 可恢复错误使用
Result<T, E>
处理 - 不可恢复错误使用
panic!
终止程序
合理使用panic!
可以让代码更健壮,因为它避免了在错误状态下继续运行可能导致的更严重问题。但应谨慎使用,确保只在真正需要终止程序的情况下触发panic。
在Rust中,Result
枚举是处理可能失败操作的核心工具。它强制你显式处理错误情况,从而使代码更健壮。以下是关于Result
的详细用法和最佳实践:
一、Result
枚举基础
Result
是标准库中的枚举类型,定义如下:
enum Result<T, E> {
Ok(T), // 操作成功,包含结果值
Err(E), // 操作失败,包含错误信息
}
T
:成功时返回的数据类型E
:失败时返回的错误类型
二、基本用法示例
1. 文件操作
use std::fs::File;
fn main() {
let file = File::open("hello.txt"); // 返回Result<File, Error>
match file {
Ok(file) => println!("文件打开成功"),
Err(error) => println!("打开文件失败: {}", error),
}
}
2. 自定义函数返回Result
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err("除数不能为零".to_string())
} else {
Ok(a / b)
}
}
fn main() {
match divide(10.0, 2.0) {
Ok(result) => println!("结果: {}", result), // 输出: 5.0
Err(msg) => println!("错误: {}", msg),
}
}
三、错误处理技巧
1. 使用?
操作符传播错误
?
操作符可自动将Err
返回给调用者:
use std::fs::File;
use std::io::{self, Read};
fn read_username_from_file() -> io::Result<String> {
let mut file = File::open("hello.txt")?; // 失败时直接返回错误
let mut username = String::new();
file.read_to_string(&mut username)?; // 失败时直接返回错误
Ok(username)
}
2. unwrap()
和expect()
快速获取值
fn main() {
let num = "42".parse::<i32>().unwrap(); // 成功时返回42,失败时panic
let file = File::open("config.txt").expect("无法打开配置文件"); // 自定义错误信息
}
3. match
模式匹配
fn process_input(input: &str) -> Result<i32, String> {
let num = input.parse::<i32>()
.map_err(|_| "输入不是有效整数".to_string())?; // map_err转换错误类型
if num < 0 {
Err("输入不能为负数".to_string())
} else {
Ok(num * 2)
}
}
fn main() {
match process_input("5") {
Ok(result) => println!("处理结果: {}", result),
Err(msg) => eprintln!("错误: {}", msg),
}
}
4. if let
和while let
简化处理
fn main() {
// 只处理成功情况,忽略错误
if let Ok(file) = File::open("hello.txt") {
println!("文件打开成功");
}
// 循环处理迭代器中的Result
let results = vec![Ok(1), Err("错误"), Ok(3)];
for result in results {
if let Err(e) = result {
println!("处理错误: {}", e);
break;
}
}
}
四、链式处理方法
1. map()
:转换成功值
fn main() {
let result = "42".parse::<i32>()
.map(|num| num * 2); // 将Ok(42)转换为Ok(84)
println!("结果: {:?}", result); // 输出: Ok(84)
}
2. and_then()
:链式调用依赖操作
fn parse_and_validate(s: &str) -> Result<i32, String> {
s.parse::<i32>()
.map_err(|_| "解析失败".to_string())
.and_then(|num| {
if num > 0 {
Ok(num)
} else {
Err("数字必须为正数".to_string())
}
})
}
3. or_else()
:处理错误并提供替代方案
fn main() {
let result = Err("错误信息")
.or_else(|e| Ok(format!("默认值: {}", e))); // 将Err转换为Ok
println!("结果: {:?}", result); // 输出: Ok("默认值: 错误信息")
}
五、自定义错误类型
使用thiserror
crate简化错误定义:
use thiserror::Error;
#[derive(Error, Debug)]
enum CalculatorError {
#[error("除法错误: 除数不能为零")]
DivisionByZero,
#[error("解析错误: {0}")]
ParseError(#[from] std::num::ParseIntError), // 自动转换
}
fn calculate(input: &str) -> Result<i32, CalculatorError> {
let parts: Vec<&str> = input.split(' ').collect();
if parts.len() != 3 {
return Err(CalculatorError::DivisionByZero);
}
let a = parts[0].parse::<i32>()?;
let op = parts[1];
let b = parts[2].parse::<i32>()?;
match op {
"+" => Ok(a + b),
"-" => Ok(a - b),
"*" => Ok(a * b),
"/" => {
if b == 0 {
Err(CalculatorError::DivisionByZero)
} else {
Ok(a / b)
}
}
_ => Err(CalculatorError::DivisionByZero),
}
}
六、实战示例
use std::fs::File;
use std::io::{self, Read};
fn read_and_parse_config() -> Result<i32, io::Error> {
// 读取配置文件
let mut file = File::open("config.txt")?;
let mut content = String::new();
file.read_to_string(&mut content)?;
// 解析整数值
content.trim().parse::<i32>()
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
fn main() {
match read_and_parse_config() {
Ok(value) => println!("配置值: {}", value),
Err(e) => eprintln!("读取配置失败: {}", e),
}
}
七、最佳实践
- 显式处理错误:避免过度使用
unwrap()
,尤其是在生产代码中。 - 错误转换:使用
map_err()
转换错误类型或添加上下文。 - 分层处理:
- 底层函数返回具体错误类型
- 高层函数使用
Box<dyn Error>
或anyhow::Error
抽象错误细节
- 提供友好错误信息:自定义错误类型时实现
Display
trait。 - 错误传播:优先使用
?
操作符简化错误处理逻辑。
总结
Rust的Result
枚举通过类型系统强制你处理可能的错误情况,使代码更健壮。掌握以下关键点:
- 使用
Result<T, E>
表示可能失败的操作 - 通过
match
、if let
、?
等处理错误 - 结合
map()
、and_then()
等方法链式处理结果 - 使用自定义错误类型提供清晰的错误信息
这种设计使错误处理成为代码的显式部分,减少了运行时崩溃的可能性。
根据你提供的目录结构,编译好的可执行文件是 hello_rust
,位于:
/home/yuanhaoli/hello_rust/target/release/hello_rust
如何运行
在当前目录(release/
)下,直接执行:
./hello_rust
如果需要从任意位置运行,可以指定完整路径:
/home/yuanhaoli/hello_rust/target/release/hello_rust
文件说明
target/release/
:Rust 通过cargo build --release
生成的优化版本可执行文件目录hello_rust
(带*
标记):可执行文件,包含编译后的二进制程序
其他编译选项
- 调试版本:通过
cargo build
生成,位于target/debug/hello_rust
- 重新编译:使用
cargo build --release
命令更新可执行文件
最佳实践
建议在项目根目录(hello_rust/
)下直接运行:
cargo run --release # 自动编译并运行优化版本
这会自动处理依赖并执行最新的可执行文件。