Option
: работа с отсутствием значения
Result
: обработка ошибок
Паника: panic!
, unwrap
, expect
Пользовательские ошибки с enum
Примеры: обработка файлов с Result
Упражнение: Написать функцию с обработкой ошибок
Проверочное задание
Заключение
Добро пожаловать в десятую лекцию нашего курса по Rust! Сегодня мы погрузимся в одну из ключевых тем языка программирования Rust — обработку ошибок. Rust предлагает мощные и безопасные инструменты для работы с ситуациями, когда что-то может пойти не так: от отсутствия значения до критических сбоев. Эта лекция будет подробной, самодостаточной и подойдёт как новичкам, так и тем, кто хочет углубить свои знания. Мы разберём всё шаг за шагом: от базовых концепций до практических примеров и упражнений.
В программировании ошибки неизбежны. Файл может не открыться, пользователь может ввести неверные данные, сеть может быть недоступна. Rust делает обработку ошибок явной и безопасной, избегая таких проблем, как "null pointer exceptions" в других языках.
"Null pointer exception" (исключение нулевого указателя) — это ошибка в программировании, которая возникает, когда программа пытается использовать объект или переменную, которая имеет значение null
(то есть не указывает на реальный объект в памяти). Например, если вы попытаетесь вызвать метод у объекта, который не был инициализирован, компилятор или среда выполнения выдаст эту ошибку. Это распространённая проблема в языках вроде Java, C++ и других, где требуется явное управление памятью или ссылками.
Вместо этого Rust использует два основных типа для работы с ошибками:
Кроме того, мы рассмотрим ситуации, когда программа "паникует" (прерывается), и научимся создавать свои собственные ошибки. К концу лекции вы сможете уверенно обрабатывать ошибки в своих программах.
Option
— это перечисление (enum), которое используется, когда значение может быть либо "чем-то" (Some
), либо "ничем" (None
). Это замена привычным null
или nil
из других языков, но с важным отличием: в Rust вы обязаны явно обработать возможность отсутствия значения.
Определение Option
в стандартной библиотеке выглядит так:
enum Option<T> {
Some(T),
None,
}
T
— это любой тип данных (например, i32
, String
и т.д.).Some(T)
— значение есть, и оно равно T
.None
— значения нет.Представьте функцию, которая ищет элемент в массиве по индексу. Если индекс выходит за пределы массива, функция возвращает None
:
fn get_element(arr: &[i32], index: usize) -> Option<i32> {
if index < arr.len() {
Some(arr[index])
} else {
None
}
}
fn main() {
let numbers = [1, 2, 3];
let result = get_element(&numbers, 1);
println!("Результат: {:?}", result); // Some(2)
let result = get_element(&numbers, 5);
println!("Результат: {:?}", result); // None
}
Здесь мы видим, что Option
заставляет нас задуматься: а что делать, если элемента нет?
Чтобы извлечь значение из Option
, нужно обработать оба варианта: Some
и None
. Вот основные способы:
match
fn main() {
let numbers = [1, 2, 3];
let result = get_element(&numbers, 1);
match result {
Some(value) => println!("Найдено значение: {}", value),
None => println!("Значение не найдено"),
}
}
match
— это мощный инструмент, который мы подробно разберём в других лекциях. Он гарантирует, что вы обработаете все возможные случаи.
unwrap
(осторожно!)Метод unwrap
извлекает значение из Option
или Result
(например из Some
), но вызывает панику (прерывание программы), если встречается None
или Err
let value = get_element(&numbers, 1).unwrap(); // 2
let value = get_element(&numbers, 5).unwrap(); // Паника!
Используйте unwrap
только если вы уверены, что значение есть.
unwrap_or
Метод unwrap
извлекает значение из Option
или Result
, а если там None
или Err
, возвращает запасное значение, которое ты указал, без паники.
Например, если вы хотите указать значение по умолчанию для случая None
:
let value = get_element(&numbers, 5).unwrap_or(0); // 0
is_some
и is_none
Проверка состояния:
if get_element(&numbers, 1).is_some() {
println!("Элемент найден!");
}
unwrap
— "дай мне значение или паника" (если None
или Err
, программа крашится).unwrap_or
— "дай мне значение или что-то другое" (если None
или Err
, возвращает запасное значение, которое ты указал).expect
— "дай мне значение или паника с моим текстом" (как unwrap
, но с твоим сообщением при краше).is_some
— "скажи, есть ли там значение?" (возвращает true
, если Some
в Option
, и false
, если None
).is_none
— "скажи, пусто ли там?" (возвращает true
, если None
в Option
, и false
, если Some
).Если коротко: unwrap
, unwrap_or
и expect
пытаются достать значение и что-то с ним сделать, а is_some
и is_none
просто проверяют, что внутри, не трогая само значение.
Option
учит вас думать о "пустых" случаях заранее. Это делает код надёжнее, чем в языках с null
.
Result
— это ещё одно перечисление, которое используется для операций, которые могут завершиться успехом (Ok
) или неудачей (Err
):
enum Result<T, E> {
Ok(T), // Успех с результатом типа T
Err(E), // Ошибка с типом E
}
T
— тип возвращаемого значения при успехе.E
— тип ошибки при неудаче.Представьте функцию, которая делит два числа, но возвращает ошибку, если делитель равен нулю:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Деление на ноль!"))
} else {
Ok(a / b)
}
}
fn main() {
let result = divide(10, 2);
println!("Результат: {:?}", result); // Ok(5)
let result = divide(10, 0);
println!("Результат: {:?}", result); // Err("Деление на ноль!")
}
Как и с Option
, вы должны явно обработать оба варианта.
match
fn main() {
match divide(10, 2) {
Ok(value) => println!("Результат деления: {}", value),
Err(error) => println!("Ошибка: {}", error),
}
}
unwrap
и expect
unwrap
— извлекает значение из Ok
, но паникует при Err
.expect
— то же самое, но позволяет указать сообщение об ошибке:let value = divide(10, 2).unwrap(); // 5
let value = divide(10, 0).expect("Не удалось разделить числа"); // Паника с сообщением
unwrap_or
Значение по умолчанию при ошибке:
let value = divide(10, 0).unwrap_or(0); // 0
?
Оператор ?
используется в функциях, возвращающих Result
. Он автоматически возвращает Err
из функции, если результат — Err
, или извлекает значение из Ok
:
fn safe_division(a: i32, b: i32) -> Result<i32, String> {
let result = divide(a, b)?;
Ok(result * 2) // Удваиваем результат
}
fn main() {
println!("{:?}", safe_division(10, 0)); // Err("Деление на ноль!")
println!("{:?}", safe_division(10, 2)); // Ok(10)
}
Result
— ваш лучший друг для обработки ошибок. Используйте ?
, чтобы сократить код, но не злоупотребляйте unwrap
.
Паника — это аварийное завершение программы, когда ошибка настолько серьёзна, что продолжение невозможно. В Rust панику можно вызвать вручную с помощью макроса panic!
:
fn main() {
panic!("Всё сломалось!");
}
При выполнении этого кода программа завершится с сообщением "Всё сломалось!".
panic!
подходит для случаев, которые не должны происходить в нормальной работе программы (например, баг в логике). Для ожидаемых ошибок используйте Result
.
Как мы видели, unwrap
и expect
вызывают панику, если Option
— None
или Result
— Err
. Это удобно для быстрого прототипирования, но в продакшен-коде лучше заменять их на явную обработку.
Иногда стандартных типов ошибок (например, String
) недостаточно. Вы можете создать свои собственные ошибки с помощью enum
:
enum MathError {
DivisionByZero,
NegativeNumber,
}
fn divide_with_custom_error(a: i32, b: i32) -> Result<i32, MathError> {
if b == 0 {
Err(MathError::DivisionByZero)
} else if a < 0 || b < 0 {
Err(MathError::NegativeNumber)
} else {
Ok(a / b)
}
}
fn main() {
match divide_with_custom_error(10, 0) {
Ok(value) => println!("Результат: {}", value),
Err(MathError::DivisionByZero) => println!("Ошибка: деление на ноль"),
Err(MathError::NegativeNumber) => println!("Ошибка: отрицательное число"),
}
}
enum
).Давайте разберём реальный пример — чтение файла:
use std::fs::File;
use std::io::{self, Read};
fn read_file(path: &str) -> Result<String, io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
fn main() {
match read_file("example.txt") {
Ok(contents) => println!("Содержимое файла: {}", contents),
Err(e) => println!("Ошибка: {}", e),
}
}
File::open
возвращает Result<File, io::Error>
.read_to_string
тоже возвращает Result
.?
передаёт ошибки наверх.Если файла нет, программа выведет ошибку вроде "No such file or directory".
Напишите функцию parse_number
, которая принимает строку и возвращает Result<i32, String>
. Функция должна:
Err
, если строка пустая или содержит нечисловые символы.fn parse_number(input: &str) -> Result<i32, String> {
if input.is_empty() {
return Err(String::from("Строка пустая"));
}
match input.parse::<i32>() {
Ok(number) => Ok(number),
Err(_) => Err(String::from("Невозможно преобразовать в число")),
}
}
fn main() {
println!("{:?}", parse_number("42")); // Ok(42)
println!("{:?}", parse_number("")); // Err("Строка пустая")
println!("{:?}", parse_number("abc")); // Err("Невозможно преобразовать в число")
}
is_empty()
проверяет, пуста ли строка.parse()
пытается преобразовать строку в i32
и возвращает Result
.match
для обработки результата parse
.divide_and_add
, которая принимает два числа, делит их с помощью divide
(из примера выше), прибавляет 5 к результату и возвращает Result<i32, String>
.main
с помощью match
.Попробуйте сами, а затем проверьте с решением ниже:
fn divide(a: i32, b: i32) -> Result<i32, String> {
if b == 0 {
Err(String::from("Деление на ноль!"))
} else {
Ok(a / b)
}
}
fn divide_and_add(a: i32, b: i32) -> Result<i32, String> {
let result = divide(a, b)?;
Ok(result + 5)
}
fn main() {
match divide_and_add(10, 2) {
Ok(value) => println!("Результат: {}", value), // 10
Err(e) => println!("Ошибка: {}", e),
}
match divide_and_add(10, 0) {
Ok(value) => println!("Результат: {}", value),
Err(e) => println!("Ошибка: {}", e), // "Деление на ноль!"
}
}
Обработка ошибок в Rust — это мощный инструмент, который делает ваш код безопасным и предсказуемым. Option
и Result
заставляют вас заранее думать о возможных проблемах, а оператор ?
упрощает работу с цепочками операций. Паника — это крайний случай, а пользовательские ошибки дают гибкость.
Практикуйтесь, экспериментируйте с примерами и переходите к следующей лекции с уверенностью! Если что-то непонятно, перечитайте разделы или попробуйте изменить примеры кода. Удачи в изучении Rust!