Veröffentlicht · wird verbessert
Rust-Anleitung · 5/6
Dieses Kapitel ist vorerst nur auf Englisch verfügbar.
Rust has no exceptions. Instead it separates two kinds of failure: bugs you cannot recover from stop the program with panic!, while operations that can legitimately fail return a Result that the caller must handle. The second half of this chapter covers the testing tools built into Cargo.
A panic! says "reaching this point means the program has a bug". Indexing past the end of a slice or calling unwrap() on None also panic. For expected failures, like a missing file or malformed input, return Result<T, E>.
use std::fs;
use std::io;
fn read_config(path: &str) -> Result<String, io::Error> {
let text = fs::read_to_string(path)?; // on failure, return the error
Ok(text.trim().to_string())
}
fn main() {
match read_config("config.txt") {
Ok(text) => println!("config: {text}"),
Err(e) => eprintln!("could not read config: {e}"),
}
}unwrap() and expect("reason") panic on error. Keep them for examples, tests, and places where failure truly cannot happen, and even then prefer expect so the panic message explains your assumption.
? unwraps an Ok value or returns the Err from the current function immediately. If the error types differ, it converts via From into the function's error type. Inside a function that returns Option, it works on Option values as well.
use std::error::Error;
use std::num::ParseIntError;
fn sum_strs(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x: i32 = a.trim().parse()?;
let y: i32 = b.trim().parse()?;
Ok(x + y)
}
fn first_char_upper(s: &str) -> Option<char> {
let c = s.chars().next()?; // None for an empty string
Some(c.to_ascii_uppercase())
}
// main can return a Result; an Err is printed and the exit code signals failure
fn main() -> Result<(), Box<dyn Error>> {
println!("{}", sum_strs("2", " 40 ")?); // 42
println!("{:?}", first_char_upper("rust")); // Some('R')
println!("{:?}", sum_strs("2", "x")); // Err(ParseIntError { .. })
Ok(())
}Box<dyn Error> can hold any error type, which makes it a convenient return type for main in small programs.
Libraries usually model their failure modes as an enum so callers can match on them. Implement Display and std::error::Error, and add From impls so ? converts underlying errors automatically.
use std::fmt;
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
Negative(i64),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Parse(e) => write!(f, "not a number: {e}"),
AppError::Negative(n) => write!(f, "negative values are not allowed: {n}"),
}
}
}
impl std::error::Error for AppError {}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self {
AppError::Parse(e)
}
}
fn parse_positive(s: &str) -> Result<i64, AppError> {
let n: i64 = s.parse()?; // ParseIntError becomes AppError
if n < 0 {
return Err(AppError::Negative(n));
}
Ok(n)
}
fn main() {
for input in ["42", "-7", "abc"] {
match parse_positive(input) {
Ok(n) => println!("ok: {n}"),
Err(e) => println!("error: {e}"),
}
}
}Two popular crates remove most of this boilerplate: thiserror derives error enums for libraries, and anyhow gives applications a single catch-all error type with easy context.
A test is any function marked #[test]. By convention unit tests sit at the bottom of the same file in a #[cfg(test)] module, so they are compiled only when testing. Check results with assert!, assert_eq! and assert_ne!.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("divide by zero");
}
a / b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn adds_two_numbers() {
assert_eq!(add(2, 3), 5);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn panics_on_zero() {
divide(1, 0);
}
#[test]
fn parses_number() -> Result<(), std::num::ParseIntError> {
let n: i32 = "10".parse()?; // tests may return Result too
assert_eq!(n, 10);
Ok(())
}
}cargo test runs unit tests, integration tests in tests/, and code examples inside documentation comments.
cargo test # everything
cargo test adds # only tests whose name contains "adds"
cargo test -- --nocapture # show println! output
cargo test -- --ignored # run slow tests marked #[ignore]
cargo test --doc # doc tests onlyCode blocks inside /// comments become documentation and tests at the same time, so examples cannot drift out of date. A package named my-crate is referred to as my_crate in code.
/// Adds two numbers.
///
/// ```
/// let sum = my_crate::add(2, 3);
/// assert_eq!(sum, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
a + b
}panic! for bugs and Result for failures the caller should handle.? propagates errors early and converts them through From.Display, Error and From makes a pleasant API.#[test] plus cargo test covers unit, integration and doc tests in one command.
0 Kommentare
Anmelden · Melde dich an, um einen Kommentar zu schreiben.
Schreib den ersten Kommentar.