Объясните, зачем в Rust успешный результат операции без полезного значения представлен как Result<(), E>, а не как отдельный тип успеха.
Result<(), E> означает: операция либо успешно завершилась и вернула единственное значение типа unit (), либо завершилась ошибкой типа E. Тип () нужен не для передачи данных, а для явного обозначения успешного завершения.
Такой вариант сохраняет единый контракт Result<T, E>: успешная ветвь всегда находится в Ok, ошибочная — в Err, поэтому с ним работают оператор ?, сопоставление с образцом и стандартные методы Result.
В Rust успешное и ошибочное завершение представлены алгебраическим перечислением Result<T, E>, где T — тип успешного значения, а E — тип ошибки. Но многие операции, например запись файла или изменение состояния объекта, не имеют полезного значения для возврата после успеха.
Для таких случаев нужен корректный тип T. Тип unit () решает эту задачу: у него есть ровно одно значение, также записываемое как (). Это позволяет выразить факт успеха без введения специального искусственного типа.
Операция может завершиться успешно, но не вернуть вызывающему коду данные. При этом ошибка должна передаваться явно, без исключений и без неоднозначного специального значения вроде false или пустой строки.
Если смешать признак успеха с данными, вызывающий код может неверно интерпретировать результат. Например, false может означать как успешную запись значения false, так и сбой операции. Result<(), E> устраняет эту неоднозначность: Ok(()) означает именно успех, а Err(error) — именно ошибку.
В Result<(), E> параметр T равен (). Поэтому возможны только две формы результата: Ok(()) и Err(error). Внутри Ok нет содержательной информации, но сама ветвь сообщает, что операция завершилась успешно.
После успешного fs::write оператор ? не извлекает полезные данные: он продолжает выполнение с единичным значением (). Если запись завершилась ошибкой, ? немедленно возвращает её из save_state.
Result<(), E> лучше, чем Result<bool, E>, когда булево значение не имеет самостоятельного смысла. bool добавил бы лишнее состояние: операция могла бы вернуть Ok(false), хотя отдельного успешного сценария «ложь» не существует.
Возврат Result<E, ()> был бы семантически неверным: он поместил бы ошибку в успешную ветвь Ok, а отсутствие ошибки — в Err. Кроме того, это нарушило бы обычный контракт Result, ожидаемый оператором ? и стандартными комбинаторами.
Ограничение заключается в том, что () подходит только тогда, когда после успеха действительно нечего возвращать. Если вызывающему коду нужно сообщить созданный идентификатор, число изменённых записей или другое значение, следует выбрать соответствующий тип, например Result<Id, E> или Result<usize, E>.
Сервис сохраняет конфигурацию. Вариант с Result<bool, ConfigError> мог бы возвращать true при сохранении и false при отсутствии изменений, но тогда API смешивает факт успеха с отдельной бизнес-семантикой. Если «изменений нет» не является ошибкой и не требуется вызывающему коду, такой bool избыточен.
Вариант с Result<(), ConfigError> проще: Ok(()) означает, что сохранение завершилось успешно, а Err содержит причину сбоя. Если позже понадобится различать «сохранено» и «изменений не было», контракт можно осознанно расширить до перечисления вроде SaveOutcome, не притворяясь, что это просто отсутствие возвращаемого значения.
Выбранное решение — Result<(), ConfigError>, потому что текущей операции нужен только надёжный сигнал успеха или ошибка. Это уменьшает число состояний, упрощает применение ? и не заставляет клиентов обрабатывать неиспользуемое значение.
1. Чем Result<(), E> отличается от Option<()>?
Option<()> различает только наличие и отсутствие значения: Some(()) или None. Оно подходит, когда отсутствие результата — нормальный сценарий, но не объясняет причину отсутствия. Result<(), E> предназначен для модели, где неуспех нужно описать ошибкой и передать её вызывающему коду.
2. Можно ли написать Ok без () в функции, возвращающей Result<(), E>?
Обычно нет: Ok — конструктор с вложенным значением, поэтому корректная форма — Ok(()). Исключением являются контексты, где тип и значение выводятся иначе через конкретный синтаксис языка, но для явного возврата успешного результата следует указывать Ok(()).
3. Почему ? особенно естественно работает с Result<(), E>?
Оператор ? проверяет, является ли результат успешным. При Ok он извлекает значение типа (), которое не требует дальнейшего использования, а при Err выполняет ранний возврат с возможным преобразованием ошибки. Поэтому цепочка процедурных операций может последовательно использовать ?, не создавая искусственные промежуточные значения.
При этом ? не превращает ошибку в panic и не подавляет её: ошибка продолжает распространяться по обычному контракту возвращаемого Result.