Что определяет границу раннего возврата оператора ? внутри замыкания Rust?
Оператор ? возвращает ошибку из ближайшей функции или замыкания, в котором он находится, если возвращаемый тип этого конструкта поддерживает соответствующий остаток ошибки. Поэтому внутри замыкания ? завершает именно замыкание, а не внешнюю функцию.
Чтобы ошибка продолжила распространяться наружу, внешняя функция должна обработать Result, возвращённый замыканием, например применить к вызову замыкания собственный оператор ?.
Rust распространяет ошибки явно через возвращаемые значения, а не через исключения, невидимо пересекающие границы вызовов. Оператор ? был введён как компактный способ сохранить этот контроль потока без постоянного написания полного сопоставления с Result.
Замыкание в Rust является отдельным вызываемым объектом со своим возвращаемым типом. Такое разделение необходимо, чтобы замыкание можно было передавать в другие функции, сохранять, вызывать позже и использовать в итераторах независимо от внешней функции.
Если ошибочно считать замыкание частью тела внешней функции, можно ожидать, что ? внутри него немедленно вернёт ошибку вызывающему коду внешней функции. На практике замыкание сначала должно завершиться своим собственным Result или Option.
Это особенно важно в цепочках итераторов. Замыкание в map может вернуть ошибку, но сам map лишь создаёт элементы Result; решение о прекращении обработки и итоговом возврате принимает последующая операция, например сборка в Result.
При применении к Result оператор ? проверяет значение. При Ok он извлекает успешное значение и продолжает выполнение, а при Err немедленно возвращает ошибку из текущего конструкта с совместимым типом возврата.
Для замыкания текущим конструктом является само замыкание. Поэтому его возвращаемый тип должен быть, например, Result<T, E>. Внешний Result не превращает автоматически возвращаемый тип замыкания в тип внешней функции.
Первый ? внутри замыкания возвращает Err из parse, поэтому замыкание завершается ошибкой. Второй ?, применённый к результату вызова parse("x"), уже находится во внешней функции и возвращает ошибку из read_pair.
Если замыкание должно возвращать обычное значение, использовать ? в нём нельзя: его возвращаемый тип не предоставляет механизма для раннего возврата ошибки. В таком случае ошибку нужно обработать внутри замыкания либо изменить контракт замыкания на Result или Option.
В итераторах типичный дизайн выглядит так: замыкание возвращает Result, а collect преобразует последовательность таких элементов в один Result. Обработка прекращается на первой ошибке, но это поведение задаётся операцией агрегации, а не самим map.
Допустим, сервис преобразует набор строковых идентификаторов в числа. Вариант с for_each вынуждает обрабатывать ошибку внутри замыкания: её можно записать в внешнее состояние, вывести в журнал или проигнорировать, но такой подход усложняет владение и контроль результата.
Вариант с map и последующим collect позволяет сделать замыкание возвращающим Result. Его плюс — типобезопасное распространение первой ошибки и отсутствие изменяемого внешнего состояния. Минус — все успешные значения обычно накапливаются в коллекции; для потоковой или частично успешной обработки это может быть нежелательно.
Явный цикл даёт максимальный контроль: можно остановиться, пропустить ошибочный элемент, собрать несколько ошибок или добавить контекст. Его недостаток — больше кода. Для обычного преобразования с остановкой на первой ошибке разумно оставить замыкание возвращающим Result и завершить внешнюю функцию через collect или через ? на результате отдельного вызова.
1. Может ли замыкание с ? вернуть ошибку напрямую из внешней функции?
Нет. Оператор действует в ближайшем контексте, поддерживающем соответствующий механизм возврата. Сначала замыкание возвращает собственный Err, а внешний код должен явно обработать результат его вызова.
2. Что произойдёт, если функция, принимающая замыкание, ожидает обычное значение, а тело замыкания использует ??
Такое замыкание не сможет соответствовать требуемому типу. Если функция ожидает замыкание, возвращающее T, а тело должно досрочно завершаться ошибкой, контракт нужно изменить на возвращающий Result<T, E> либо обработать ошибку до выхода из замыкания.
3. Почему map сам по себе не прекращает внешний вызов при ошибке из замыкания?
map не знает, что значение Err означает необходимость остановки: для него Result<T, E> — обычный элемент итерации. Прекращение на первой ошибке появляется только у операции, которая специально агрегирует Result, например collect::<Result<Vec<_>, _>>(), либо при явной проверке результата в цикле.