Обработка ошибок

В Rust ошибки группируются на две основные категории: исправимые (recoverable) и неисправимые (unrecoverable).

В Rust нет исключений. Вместо этого он имеет тип Result<T, E> для обрабатываемых (исправимых) ошибок и макрос panic!, который останавливает выполнение, когда программа встречает необрабатываемую (неисправимую) ошибку.

Когда происходит паника, программа начинает процесс раскрутки стека, означающий в Rust проход обратно по стеку вызовов и очистку данных для каждой обнаруженной функции.

Warning

Если в вашем проекте нужно насколько это возможно сделать маленьким исполняемый файл, вы можете переключиться с варианта раскрутки стека на вариант прерывания при панике, добавьте panic = 'abort' в раздел [profile] вашего Cargo.toml файла.

[profile.release]
panic = 'abort'

При добавление в код макрос panic!

Программа прервется

Используем обратную трассировку

~/data/rs/panic [101] $ RUST_BACKTRACE=1 cargo run
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/panic`

thread 'main' panicked at src/main.rs:5:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
   0: __rustc::rust_begin_unwind
             at /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/std/src/panicking.rs:697:5
   1: core::panicking::panic_fmt
             at /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/panicking.rs:75:14
   2: core::panicking::panic_bounds_check
             at /rustc/1159e78c4747b02ef996e55082b704c09b970588/library/core/src/panicking.rs:280:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at /home/edge/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at /home/edge/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/slice/index.rs:18:15
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at /home/edge/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3571:9
   6: panic::main
             at ./src/main.rs:5:6
   7: core::ops::function::FnOnce::call_once
             at /home/edge/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library/core/src/ops/function.rs:253:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
~/data/rs/panic [101] $ 

Исправление ошибок с помощью Result

#![allow(unused)]
fn main() {
enum Result<T, E> {
    Ok(T),
    Err(E),
}

}

T представляет тип значения, которое будет возвращено в случае успеха внутри варианта Ok, а E представляет тип ошибки, которая будет возвращена при сбое внутри варианта Err.

use std::{fs::File, io::ErrorKind};

fn main() {
    let greeting_file_result = File::open("hello.txt");
    let _greeting_file = match greeting_file_result {
	Ok(file) => file,
	Err(error) => match error.kind() {
	    ErrorKind::NotFound => match File::create("hello.txt") {
		Ok(fc) => fc,
		Err(e) => panic!("Какая-то проблема: {e:?}"),
	    },
	    _ => {
		panic!("Проблема открытия файла: {error:?}");
	    }
	},
    };
}

Но есть еще вариант решить эту проблему через замыкания

use std::fs::File;
use std::io::ErrorKind;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap_or_else(|error| {
        if error.kind() == ErrorKind::NotFound {
            File::create("hello.txt").unwrap_or_else(|error| {
                panic!("Problem creating the file: {:?}", error);
            })
        } else {
            panic!("Problem opening the file: {:?}", error);
        }
    });
}

Обработка ошибок с помощью unwrap и expect

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt").unwrap();
}

unwrap выполняет туже работу что match:

  • возвращает результат OK или
  • вызывает panic! с описанием ошибки

expect позволяет указать сообщение для макроса panic!

use std::fs::File;

fn main() {
    let greeting_file = File::open("hello.txt")
        .expect("hello.txt should be included in this project");
}

Проброс ошибок

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let username_file_result = File::open("hello.txt");

    let mut username_file = match username_file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };

    let mut username = String::new();

    match username_file.read_to_string(&mut username) {
        Ok(_) => Ok(username),
        Err(e) => Err(e),
    }
}
}

Оператор ? для проброса ошибок

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

}

определим OurError. Если мы также определим impl From<io::Error> for OurError для создания экземпляра OurError из io::Error, то оператор ?, вызываемый в теле read_username_from_file, вызовет from и преобразует типы ошибок без необходимости добавления дополнительного кода в функцию.

Как итог:

#![allow(unused)]
fn main() {
use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut username = String::new();

    File::open("hello.txt")?.read_to_string(&mut username)?;

    Ok(username)
}

}

Или записать весь файл в строку

#![allow(unused)]
fn main() {
use std::fs;
use std::io;

fn read_username_from_file() -> Result<String, io::Error> {
    fs::read_to_string("hello.txt")
}

}

? можно использовать и со значениями Option<T>

#![allow(unused)]
fn main() {
fn last_char_of_first_line(text: &str) -> Option<char> {
    text.lines().next()?.chars().last()
}

}

main тоже может возвращать Result

use std::error::Error;
use std::fs::File;

fn main() -> Result<(), Box<dyn Error>> {
    let greeting_file = File::open("hello.txt")?;

    Ok(())
}

panic! или не panic!

При панике код не имеет возможности восстановить своё выполнение.

В ситуациях как примеры, прототипы и тесты, более уместно писать код, который паникует вместо возвращения Result.

Когда код должен паниковать:

  • Некорректное состояние — это что-то неожиданное, отличается от того, что может происходить время от времени, например, когда пользователь вводит данные в неправильном формате.
  • Ваш код после этой точки должен полагаться на то, что он не находится в некорректном состоянии, вместо проверок наличия проблемы на каждом этапе.
  • Нет хорошего способа закодировать данную информацию в типах, которые вы используете.

Создание своего типа

#![allow(unused)]
fn main() {
pub struct Guess {
    value: i32,
}

impl Guess {
    pub fn new(value: i32) -> Guess {
        if value < 1 || value > 100 {
            panic!("Guess value must be between 1 and 100, got {value}.");
        }

        Guess { value }
    }

    pub fn value(&self) -> i32 {
        self.value
    }
}
}