Практическая ситуация: замыкание может выбросить ошибку, но API принимает только невыбрасывающую функцию. Какое ограничение Swift проявится при передаче такого замыкания?
Выбрасывающее замыкание нельзя передать туда, где ожидается невыбрасывающая функция. В Swift признак throws входит в тип функции: тип () throws -> Int не совместим с типом () -> Int. Это ограничение не позволяет скрыть потенциальную ошибку от вызывающего кода.
Swift изначально проектировал обработку ошибок как явный механизм: функция сообщает о возможности сбоя через throws, а вызывающий код обязан использовать try, обработать ошибку или передать её выше. Различие типов выбрасывающих и невыбрасывающих функций сохраняет эту информацию при передаче функций и замыканий как значений.
Такой подход предотвращает неявное превращение операции с ошибкой в обычный вызов. API не может случайно принять замыкание, которое способно завершиться ошибкой, если его контракт не предусматривает такой сценарий.
Предположим, API принимает callback типа () -> Int и вызывает его без try. Если передать туда операцию типа () throws -> Int, возникает принципиальная проблема: API не знает, как обработать возможную ошибку и не может корректно продолжить обычный поток выполнения.
Компилятор останавливает такую передачу на этапе проверки типов. Обход ограничения через принудительное подавление ошибки или скрытое состояние сделал бы контракт API менее надёжным и усложнил бы поиск сбоев.
Если операция действительно может завершиться ошибкой, параметр должен иметь выбрасывающий тип функции. Обычно функция-обёртка объявляется как rethrows: это означает, что она сама передаёт ошибку от полученного замыкания, но не создаёт независимые ошибки.
Здесь read имеет тип () throws -> Int, а apply принимает именно такой тип. Вызов apply(read) требует try, потому что ошибка может пройти через apply к её вызывающему коду.
Если API по смыслу не умеет работать с ошибками, есть только два корректных варианта: передать ему уже невыбрасывающую функцию или изменить контракт API. Например, вызывающий код может заранее обработать ошибку и вернуть запасное значение, но тогда информация о причине сбоя будет потеряна или преобразована в это значение.
Невыбрасывающую функцию можно передать параметру, который ожидает выбрасывающую функцию: отсутствие возможности ошибки безопаснее, чем её наличие. Обратное преобразование невозможно без явного решения, что делать с ошибкой.
Сервис аналитики принимает callback для вычисления значения и поначалу объявлен с параметром () -> Int. Позже вычисление стало зависеть от чтения локального хранилища, которое может завершиться ошибкой.
Вариант с обработкой ошибки до передачи callback сохраняет старый API и прост для вызывающей стороны, но требует заранее выбрать запасное значение. Это может скрыть различие между отсутствующими данными, повреждённым хранилищем и временным сбоем.
Вариант с изменением параметра на () throws -> Int делает контракт честным, но требует обновить места вызова. Если сервис только вызывает callback и не добавляет собственных ошибок, выбранным решением будет rethrows: ошибка сохраняет исходную причину и обрабатывается на уровне, где действительно доступен подходящий контекст.
Можно ли передать невыбрасывающее замыкание параметру типа () throws -> Int?
Да. Невыбрасывающая функция не нарушает контракт выбрасывающего параметра: она просто никогда не использует разрешённый канал ошибки. Это полезно для обобщённых API, которым иногда передают операции с ошибками, а иногда — гарантированно успешные операции.
Может ли функция с rethrows выбросить собственную ошибку независимо от замыкания?
Нет, rethrows предназначен для распространения ошибок от переданных выбрасывающих параметров. Если функции нужно самостоятельно выбрасывать ошибку, не связанную с таким параметром, её следует объявить как throws. Иначе контракт вводил бы в заблуждение: вызывающий код ожидал бы только ошибки, возникающие из переданной операции.
Что изменится, если API обработает ошибку внутри callback и вернёт обычное значение?
Замыкание можно адаптировать к типу () -> Int, если внутри него выполнить do-catch и преобразовать ошибку, например в значение по умолчанию. Компиляторное ограничение будет удовлетворено, но информация о причине сбоя исчезнет для внешнего API; поэтому такой вариант подходит только тогда, когда потеря детализации является частью осознанного контракта.