Зачем в механизме оператора ? Rust разделены успешное значение и residual при досрочном выходе?
Residual — это специальное представление досрочного выхода, отделённое от успешного значения. Благодаря этому оператор ? может быть обобщён не только для Result и Option: он продолжает вычисление при успешной ветви, а при раннем выходе передаёт residual во внешний тип через механизм FromResidual.
Оператор ? создавался как краткая форма распространения ошибок, но в Rust существует несколько типов, управляющих досрочным завершением вычисления. Result передаёт ошибку, Option — отсутствие значения, а другие типы могут использовать собственную модель управления потоком.
Разделение успешного результата и residual позволило описывать общий механизм такого поведения без привязки оператора исключительно к паре Ok и Err. При этом публичная реализация собственных Try-типов остаётся ограниченной стабильностью соответствующих трейтов.
Если считать результатом только единый тип вроде Result<T, E>, оператору пришлось бы заранее знать конкретную форму ошибки или отсутствия значения. Это затруднило бы поддержку разных типов досрочного выхода и сделало бы механизм менее обобщаемым.
Кроме того, успешное значение и сигнал раннего выхода имеют разную семантику. Успешное значение нужно передать следующей операции, а residual — вернуть из текущего контекста или преобразовать в формат, который ожидает внешний вызов.
Внутренне механизм ? можно рассматривать как взаимодействие двух операций:
Try::branch разделяет вычисление на продолжение с успешным значением и ранний выход с residual;FromResidual преобразует residual в тип, который возвращает текущая функция.Для Result<T, E> успешная ветвь содержит T, а residual представляет ошибочную ветвь. Для Option<T> успешная ветвь содержит T, а residual обозначает None. Поэтому residual не является обычным значением результата: это переносимый сигнал о необходимости завершить текущий контекст раньше.
Такое разделение объясняет, почему оператор ? способен работать с разными типами, сохраняя единый алгоритм: проверить ветвь, продолжить вычисление при успехе или выполнить ранний выход. Для Result преобразование ошибки другого типа обычно выполняется реализацией FromResidual, которая использует From для преобразования ошибки.
Главное ограничение состоит в том, что совместимость определяется не сходством названий типов, а наличием подходящей реализации FromResidual. Нельзя произвольно ожидать, что любой тип с успешным значением автоматически совместим с оператором ?.
Внутренний код сервиса использует несколько типов операций: парсер возвращает Result, поиск необязательной настройки — Option, а управляющий слой должен единообразно поддерживать ранний выход. Попытка механически смешать эти типы приводит к неочевидным ограничениям: разные residual требуют разных правил преобразования, а отсутствие значения не всегда является ошибкой.
Варианты решения:
Практически выбирают третий вариант: Option преобразуют в Result там, где отсутствие значения действительно является ошибкой предметной области, а Result передают через ? внутри функции с согласованным типом возврата. Это делает ранний выход предсказуемым и не маскирует отсутствие данных под технический сбой.
Вопрос: Чем residual отличается от ошибки E в Result<T, E>?
Ответ: Ошибка E — это полезное содержимое ошибочной ветви Result. Residual — более абстрактное представление досрочного выхода, включающее необходимую информацию для передачи управления наружу. Для Result residual содержит ошибочную ветвь в специальной форме, но концептуально не обязан совпадать с самим E.
Вопрос: Почему оператор ? нельзя считать просто сокращением сопоставления с Ok и Err?
Ответ: Такое сопоставление описывает только частный случай Result. Реальный механизм учитывает общий протокол Try и преобразование через FromResidual, поэтому применим к нескольким моделям досрочного выхода. Кроме того, он определяет, как результат ранней ветви адаптируется к возвращаемому типу текущей функции.
Вопрос: Что произойдёт, если для residual нет подходящей реализации FromResidual?
Ответ: Код не скомпилируется. Rust не будет автоматически угадывать, как преобразовать досрочный выход в тип результата функции: такое преобразование должно быть предусмотрено системой типов. Это защищает API от скрытого превращения, например, отсутствующего значения в ошибку или наоборот.