Программирование RustОбработка ошибокРазработчик системного ПО на Rust

Разберите последствия вызова функции для разных входных данных: что произойдёт при передаче строки "http" и...

Разберите последствия вызова функции для разных входных данных: что произойдёт при передаче строки "http" и почему возвращаемый тип не защищает от аварийного завершения?

fn parse_port(text: &str) -> Result<u16, String> {
    let port = text.parse::<u16>().unwrap();
    Ok(port)
}

fn main() {
    println!("{:?}", parse_port("8080"));
    println!("{:?}", parse_port("http"));
}
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

Для строки "http" вызов parse вернёт Err, но unwrap() немедленно вызовет panic. Поэтому функция не вернёт Err(String): её выполнение аварийно завершится до достижения Ok(port).

Тип Result сам по себе не делает функцию безопасной для любых входных данных. Безопасность зависит от того, как обрабатывается полученный Result; в данном случае ошибка принудительно превращается в panic.

Исторический контекст

В Rust разделены два разных класса проблем. Result предназначен для ожидаемых отказов, которые вызывающий код может обработать: неверный ввод, отсутствие доступа к файлу, ошибка сети. panic! предназначен прежде всего для нарушения предположений программы или состояния, продолжение работы при котором небезопасно или бессмысленно.

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

Постановка проблемы

Парсинг внешнего текста — например, значения из конфигурации или HTTP-запроса — по определению может завершиться ошибкой. Если библиотечная функция вызывает unwrap, некорректные данные превращают ожидаемую ситуацию в panic.

Последствия зависят от контекста: процесс может аварийно завершить текущую операцию, вывести сообщение об ошибке и стек вызовов, а при стратегии abort — завершиться без раскрутки стека. В сервере это может привести к недоступности процесса или потере текущей задачи вместо контролируемого ответа клиенту.

Подробное решение

text.parse::<u16>() возвращает Result<u16, ParseIntError>. Для строки "8080" это Ok(8080), а для "http"Err(...).

Метод unwrap() извлекает значение из Ok. Если встречает Err, он вызывает panic. Эквивалентная логика выглядит концептуально так:

fn parse_port(text: &str) -> Result<u16, String> { let parsed = text.parse::<u16>(); match parsed { Ok(port) => Ok(port), Err(error) => Err(error.to_string()), } }

Практичнее сохранить конкретный тип ошибки и использовать ?:

use std::num::ParseIntError; fn parse_port(text: &str) -> Result<u16, ParseIntError> { let port = text.parse::<u16>()?; Ok(port) } fn main() { match parse_port("http") { Ok(port) => println!("порт: {port}"), Err(error) => eprintln!("некорректный порт: {error}"), } }

Здесь ? при Err досрочно возвращает ошибку из parse_port, не вызывая panic. Тип ошибки остаётся информативным и может быть преобразован в более общий тип на границе слоя приложения.

unwrap допустим только при доказанном инварианте, например когда значение создаётся непосредственно перед вызовом и его корректность гарантирована логикой программы. Для внешнего ввода, файлов, сети и конфигурации это обычно неверный выбор. expect не меняет поведение, а лишь добавляет осмысленное сообщение к panic, поэтому не заменяет обработку ожидаемой ошибки.

Ситуация из практики

Сервис читает порт из переменной окружения. Рассматривались три варианта:

  • unwrap: короткий код, но опечатка в конфигурации вызывает panic и не даёт приложению сформировать понятное сообщение или корректно выбрать стратегию завершения;
  • catch_unwind вокруг вызова: позволяет перехватить некоторые panic, но усложняет поток управления и не превращает обычную ошибку в хороший контракт; это не замена Result;
  • возврат типизированной ошибки через Result: требует явно обработать отказ, зато позволяет на верхнем уровне вывести причину, добавить контекст и выбрать код завершения.

Выбирается третий вариант: функция разбора возвращает Result<u16, ConfigError>, а слой запуска приложения преобразует ошибку в сообщение вроде «параметр PORT имеет недопустимое значение». В результате ошибка конфигурации становится управляемой, а panic остаётся для действительно нарушенных внутренних инвариантов.

Что кандидаты часто упускают

  1. Всегда ли panic завершает весь процесс?

    Нет. При стандартной стратегии unwind panic запускает раскрутку стека и может быть перехвачен через std::panic::catch_unwind, если граница и типы кода это позволяют. При стратегии abort раскрутки стека нет, и процесс завершается. Однако catch_unwind не является обычным способом обработки ошибок: panic может означать нарушение инварианта, а не штатный отказ операции.

  2. Чем expect принципиально отличается от unwrap?

    По механизму почти ничем: оба извлекают значение из Result или вызывают panic при Err. expect принимает сообщение разработчика, поэтому обычно даёт более полезное описание нарушенного предположения. Он улучшает диагностику, но не меняет контракт и не делает обработку входных данных безопасной.

  3. Почему возврат String часто хуже, чем конкретный тип ошибки?

    String сохраняет текст, но теряет структуру ошибки: вызывающему коду трудно надёжно отличить ошибку диапазона от ошибки формата или преобразовать её в разные пользовательские сообщения. Конкретный тип, например ParseIntError, позволяет сохранять источник причины и использовать преобразования через From или thiserror. Компромисс состоит в том, что тип ошибки нужно спроектировать, особенно если публичный API должен скрывать детали внутренних библиотек.