В прошлый раз мы научили сумму возвращать Result: теперь parse_amount не молчит, а объясняет, что пошло не так. Но чтобы воспользоваться ответом, приходится писать match на каждом шагу, а в parse_command таких шагов несколько. Сегодня мы избавимся от лишнего шума: в Rust для этого есть один знак — ?.
Проблема: лестница из match
Представьте, что вы собираете посылку по списку: коробка, товар, чек, скотч. Если коробки нет, вы не станете вкладывать чек — остановитесь и скажете, чего не хватило. Так же должен работать разбор команды: первая же ошибка прекращает работу и уходит вызвавшему.
Пока мы умеем делать это только через match. Вот ветка «добавить», записанная отдельной функцией parse_add (она получает остаток строки после слова команды):
fn parse_add(rest: &str) -> Result<Command, String> {
match parse_amount(first_word(rest)) {
Ok(amount) => {
let rest = rest_after_first(rest);
match Category::from_word(first_word(rest)) {
Some(category) => {
let note = rest_after_first(rest);
Ok(Command::Add(Expense::new(amount, category, note)))
}
None => Err("Не знаю категорию".to_string()),
}
}
Err(message) => Err(message),
}
}
Разбор по строкам
match parse_amount(first_word(rest))— разбираем первое слово как сумму; результат —Result, поэтому вmatchдве ветки:OkиErr.Ok(amount) => { ... }— сумма в порядке, идём дальше — и внутри ветки открываем второйmatch, уже по категории.None => Err("Не знаю категорию".to_string())— категории нет: возвращаем ошибку-строку. Методto_string()превращает текст вString.Err(message) => Err(message)— самая скучная строка: ошибка пришла, и мы просто передаём её дальше без изменений.
Всего два шага, а код уже уехал вправо. Добавьте ещё проверку — и получится лесенка, в которой тяжело найти, что где закрывается. А строка Err(message) => Err(message) ничего не решает: она только пересылает ошибку. Такой код Rust умеет писать за нас.
Оператор ?: «если ошибка, верни её сразу»
Простыми словами: знак вопроса после вызова говорит «получилось — бери результат и работай дальше, не получилось — немедленно выходи из функции и отдай эту ошибку тому, кто тебя вызвал». Это как сотрудник, который при проблеме сразу докладывает начальнику.
Официально это оператор ? (его ещё называют оператором распространения ошибки). Он стоит сразу после выражения с Result. Вот как он работает, если расписать его руками:
let amount = match parse_amount(word) {
Ok(n) => n,
Err(message) => return Err(message),
};
Если внутри Ok — значение достаётся и попадает в amount. Если внутри Err — срабатывает return, и функция заканчивается, возвращая эту же ошибку. А теперь то же самое одной строкой:
let amount = parse_amount(word)?;
Обе записи делают одно и то же. Так что ? — не магия, а сокращение для match с return.
Где можно ставить ?
Есть правило: ? умеет делать return только из функции, которая сама возвращает Result (или Option, об этом дальше). В нашей parse_add тип результата — Result<Command, String>, так что всё сходится. Перепишем её:
fn parse_add(rest: &str) -> Result<Command, String> {
let amount = parse_amount(first_word(rest))?;
let rest = rest_after_first(rest);
let category = match Category::from_word(first_word(rest)) {
Some(category) => category,
None => return Err("Не знаю категорию".to_string()),
};
let note = rest_after_first(rest);
Ok(Command::Add(Expense::new(amount, category, note)))
}
Разбор по строкам
let amount = parse_amount(first_word(rest))?;— если сумма плохая,parse_addтут же вернётErrс её текстом, а следующие строки даже не начнутся.let rest = rest_after_first(rest);— затенение, как и раньше: имяrestтеперь указывает на остаток без суммы.None => return Err(...)— дляOptionпока остаётсяmatch— ниже научимся и с ним обходиться без него.Ok(Command::Add(...))— в самом конце оборачиваем готовую команду вOk: функция возвращаетResult, а не простоCommand.
? работает и с Option
Знак вопроса дружит не только с Result. С Option он поступает так же, только по-своему: Some(значение) — достаёт значение, None — сразу возвращает None из функции. Условие то же: функция сама должна возвращать Option.
Допустим, нужно достать третье слово строки «добавить 350 еда»:
fn category_word(line: &str) -> Option<&str> {
let mut parts = line.split_whitespace();
let _command = parts.next()?;
let _amount = parts.next()?;
let category = parts.next()?;
Some(category)
}
Разбор по строкам
let mut parts = line.split_whitespace();— набор слов строки. Онmut, потому что каждый вызовnext()сдвигает нас на слово дальше.let _command = parts.next()?;—next()возвращаетOption<&str>: есть слово —Some, слова кончились —None. Если слов нет, функция сразу вернётNone. Подчёркивание в имени говорит, что значение нам не нужно: мы его только пропускаем.let category = parts.next()?;— третье слово. Если строка короче,?прервёт функцию.Some(category)— дошли до конца — все три слова на месте, возвращаем третье.
Три «может не оказаться» уложились в три ровные строки.
Превращаем ошибки: ok_or и map_err
Теперь вернёмся к Category::from_word. Она возвращает Option, а parse_command — Result. Поставить ? прямо на Option внутри функции с Result нельзя: у None нет текста ошибки, и Rust не знает, что вернуть в Err. Нужен переходник, который скажет: «если None, считай ошибкой вот это». Такой переходник — метод ok_or:
let category = Category::from_word(word)
.ok_or(format!("Не знаю категорию «{word}»"))?;
ok_or превращает Option<T> в Result<T, E>: Some(x) становится Ok(x), а None — Err(то, что вы передали). Дальше уже обычный ?.
Второй переходник нужен, когда ошибка уже есть, но не того вида. Помните, как parse::<u32>() отвечает на «abc»? Он возвращает Err(ParseIntError), а наша функция вернула бы Err(String). Метод map_err меняет только ошибку, оставляя Ok нетронутым:
fn parse_amount(text: &str) -> Result<u32, String> {
text.parse::<u32>()
.map_err(|_| format!("Не число: {text}"))
}
Разбор по строкам
text.parse::<u32>()— пробуем превратить текст в число; получаемResult<u32, ParseIntError>..map_err(|_| ...)— если внутриErr— подставляем свою ошибку. Запись|_| ...— маленькая функция-замыкание: подчёркивание значит «старая ошибка нам не нужна».format!("Не число: {text}")— новая ошибка — уже строка, как требует типResult<u32, String>.
Это упрощённый пример: проверок на пустую строку и ноль в нём нет, поэтому в проект мы его не переносим. В Копилке остаётся полная parse_amount из шага 1 ниже.
Тип ошибки обязан совпасть
? не умеет превращать любую ошибку во что угодно. Если функция возвращает Result<..., String>, то под каждым ? должна быть ошибка типа String. Для чужих типов — ParseIntError и подобных — сначала зовём map_err, для Option — ok_or.
А почему в main не ? ?
Раз знак вопроса так удобен, хочется поставить его и в main. Можно, если объявить fn main() -> Result<(), String>: тогда ошибка из ? завершит программу с сообщением. Обычная main возвращает «ничего» (()), и ? в ней не скомпилируется.
Но нам это и не нужно. У «Копилки» в main цикл: пользователь ошибся — программа должна объяснить, что не так, и ждать следующую команду, а не закрыться. Поэтому наверху ошибки разбираем через match: ветка Err(message) печатает текст, и цикл идёт дальше. А в функциях «по дороге» (parse_command, parse_amount) ? на своём месте. Про функции, которые возвращают ошибки ввода-вывода (io::Result), поговорим в последнем модуле, когда дойдём до файлов.
Практика: parse_command на вопросительных знаках
Переводим «Копилку» на Result. Откройте проект kopilka и файл src/main.rs. Работать будем в трёх местах, всё остальное (структуры, Category, print_status, first_word и остальные помощники) остаётся как есть.
Шаг 1. parse_amount остаётся прежним
Функция parse_amount из прошлого урока должна возвращать Result<u32, String> и стоять выше parse_command. Если ваша версия работает, оставьте её; для сверки вот она целиком:
fn parse_amount(text: &str) -> Result<u32, String> {
let text = text.trim();
if text.is_empty() {
return Err("Не указана сумма".to_string());
}
match text.parse::<u32>() {
Ok(0) => Err("Сумма должна быть больше нуля".to_string()),
Ok(amount) => Ok(amount),
Err(_) => Err(format!("Не число: {text}")),
}
}
Шаг 2. Новый parse_command
Замените прежнюю функцию parse_command (ту, что возвращала Option) на эту:
fn parse_command(line: &str) -> Result<Command, String> {
match first_word(line) {
"добавить" => {
let rest = rest_after_first(line);
let amount = parse_amount(first_word(rest))?;
let rest = rest_after_first(rest);
let word = first_word(rest);
if word.is_empty() {
return Err("Не указана категория".to_string());
}
let category = Category::from_word(word)
.ok_or(format!("Не знаю категорию «{word}»"))?;
let note = rest_after_first(rest);
let note = if note.is_empty() { "без заметки" } else { note };
Ok(Command::Add(Expense::new(amount, category, note)))
}
"список" => Ok(Command::List),
"отчёт" | "отчет" => Ok(Command::Report),
"помощь" => Ok(Command::Help),
"выход" => Ok(Command::Quit),
"" => Err("Пустая команда".to_string()),
other => Err(format!("Не знаю команду «{other}»")),
}
}
Разбор по строкам
Result<Command, String>— теперь функция либо отдаёт команду, либо текст ошибки.parse_amount(first_word(rest))?— лестница изlet ... elseзаменилась одним знаком вопроса: плохая сумма сразу прерывает функцию.if word.is_empty() { return Err(...) }— для строки «добавить 350» слова-категории нет. Без этой проверки мы получили бы странное «Не знаю категорию «»», поэтому пустое слово отлавливаем отдельно.Category::from_word(word).ok_or(...)?— переходник изOptionвResultи сразу?."" => Err(...)— пустая строка тоже получает свой понятный ответ.other => Err(format!(...))— имяotherполучает любое слово, которое мы не знаем, и мы подставляем его в сообщение. Раньше здесь был безликийNone.
Шаг 3. Цикл в main печатает ошибки
В main разбор команды вызывается в цикле через match parse_command(&line). Раньше ветки были Some(Command::...) и None, теперь результат — Result, поэтому каждое Some(...) становится Ok(...), а последняя ветка None заменяется на Err(message). Тела веток не меняются. Целиком match выглядит так:
match parse_command(&line) {
Ok(Command::Add(expense)) => {
println!("Записано: {}", expense.describe());
expenses.push(expense);
let spent = total(&expenses);
println!("Потрачено: {spent} руб. из {MONTH_LIMIT} руб.");
print_status(spent, MONTH_LIMIT);
}
Ok(Command::List) => print_list(&expenses),
Ok(Command::Report) => print_report(&expenses),
Ok(Command::Help) => print_help(),
Ok(Command::Quit) => break,
Err(message) => println!("Ошибка: {message}"),
}
Проверка if line.trim().is_empty() { continue; } выше match пока остаётся, поэтому ветка "" => Err(...) в parse_command сейчас запасная: пустую строку main отсеивает раньше. В следующем уроке проверку уберём, и пустая строка получит своё объяснение.
Запустите проект: cargo run — и вводите команды. Строки, которые набираете вы, ниже отмечены знаком > (на самом деле его на экране нет):
> добавить 350 еда обед в столовой
Записано: 350 руб., еда, «обед в столовой»
Потрачено: 350 руб. из 30000 руб.
Статус: в норме
> добавить abc еда
Ошибка: Не число: abc
> добавить 0 еда
Ошибка: Сумма должна быть больше нуля
> добавить 350
Ошибка: Не указана категория
> добавить 350 сон
Ошибка: Не знаю категорию «сон»
> танцы
Ошибка: Не знаю команду «танцы»
> выход
Итого потрачено: 350 руб. из 30000 руб.
Теперь «Копилка» не молчит и не падает: на любую неудачу она называет причину. Если вывод не совпал, проверьте, что все Some(...) в match стали Ok(...).
Проверьте себя
- Что напечатает программа на «добавить 100 сон»? А на «добавить -5 еда»?
- Что случится, если в
parse_commandзаменить?послеparse_amountна.unwrap()? Почему так делать нельзя? - Попробуйте вернуть
.ok_or(...)без знака вопроса. Какой тип получит переменнаяcategoryи почему программа не соберётся? - Добавьте ещё одну проверку: заметка не длиннее 40 символов. Что нужно вернуть при нарушении, чтобы главный цикл ничего не менял?
Частые ошибки
1. ? в функции, которая ничего не возвращает
Вы написали let n = parse_amount("5")?; внутри функции без ->. Компилятор ругается:
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:5:30
|
4 | fn show() {
| --------- this function should return `Result` or `Option` to accept `?`
5 | let n = parse_amount("5")?;
| ^ cannot use the `?` operator in a function that returns `()`
Почему: ? должен куда-то вернуть Err, а функция возвращает «пустоту». Как исправить: либо поменять тип результата функции на Result<..., String> и добавить Ok(...) в конце, либо там, где ошибку надо обработать на месте (например, в main), вернуться к match.
2. ? на Option внутри функции с Result
error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
--> src/main.rs:4:35
|
3 | fn pick(w: &str) -> Result<Category, String> {
| -------------------------------------------- this function returns a `Result`
4 | let c = Category::from_word(w)?;
| ^ use `.ok_or(...)?` to provide an error compatible with `Result<Category, String>`
Почему: у None нет текста ошибки, а функции нужен Err(String). Компилятор сам подсказывает выход: допишите .ok_or(...) с нужным текстом — Category::from_word(w).ok_or("Нет такой категории".to_string())?.
3. Другой тип ошибки под знаком вопроса
error[E0277]: `?` couldn't convert the error to `String`
--> src/main.rs:2:32
|
1 | fn amount(text: &str) -> Result<u32, String> {
| ------------------- expected `String` because of this
2 | let n = text.parse::<u32>()?;
| --------------^ the trait `From<ParseIntError>` is not implemented for `String`
Почему: parse возвращает ParseIntError, а функция обещает String. Как исправить: text.parse::<u32>().map_err(|_| format!("Не число: {text}"))?.
4. Потерянный текст ошибки
Код компилируется, но программа отвечает невнятно:
let amount = text.parse::<u32>().map_err(|_| "Ошибка".to_string())?;
Любой плохой ввод теперь даёт просто «Ошибка» — пользователь не узнает ни что именно не так, ни какое слово мы не поняли. Почему так вышло: в map_err(|_| ...) мы выбросили всё, что знали. Как исправить: вставляйте в сообщение то, что мы знаем: само слово ({text}) и причину («не число», «не указана категория»). Так же и в main: печатайте {message}, а не фиксированную фразу.
Что важно запомнить
выражение?— «получилось: продолжай, не получилось: немедленно верни ошибку из функции». Это сокращение дляmatchсreturn.?можно использовать только в функции, которая возвращаетResultилиOption.- Над
Optionв функции сResultсначала ставим.ok_or(текст_ошибки); чужую ошибку приводим к нужной через.map_err(|_| ...). - Тип ошибки под
?должен совпадать с типом ошибки функции (у нас пока этоString). - На самом верху, в
main, ошибки разбираем черезmatch: программа печатает причину и продолжает работу. - В тексте ошибки сохраняйте то, что поможет человеку: какое слово не поняли и почему.
Теперь вопрос на будущее: String в роли ошибки приятен тем, что его легко написать, но неточен — по типу не отличить «плохая сумма» от «неизвестной категории». В следующем уроке мы сделаем для ошибок собственное перечисление.