Swift представил структурированное управление ошибками в версии 2.0, заменив ошибки указателей Objective-C на собственные семантики throw и catch. Ключевое слово rethrows появилось для решения неприятности, где универсальные функции высшего порядка, такие как map или filter, вынуждали вызывающих использовать try, даже при передаче не выбрасывающих замыканий, создавая ненужную церемонию обработки ошибок.
Проблема связана с полиморфизмом эффектов функций и подтипированием. В системе типов Swift не выбрасывающее замыкание является подтипом выбрасывающего замыкания, так как оно удовлетворяет контракту "может выбрасывать", никогда не выбрасывая. Без rethrows функция, принимающая выбрасывающее замыкание, обязана безусловно распространять выбросы, заставляя все места вызова обрабатывать ошибки независимо от фактического поведения аргумента.
Решение — аннотация rethrows, которая устанавливает условный контракт: функция выбрасывает только если ее параметр замыкания выбрасывает. Компилятор Swift реализует это, отслеживая выбрасывание аргументов замыкания на этапе компиляции. Когда передается не выбрасывающее замыкание, функция рассматривается как не выбрасывающая в месте вызова, устраняя необходимость в try; когда передается выбрасывающее замыкание, функция наследует эффект выброса.
Мы разрабатывали модульный конвейер преобразования данных для приложения iOS, где пользователи могли объединять операции, такие как разбор JSON, изменение размера изображений и криптографическое хеширование. Основная функция pipeline принимала массив преобразований, определенных как (Data) throws -> Data. Сначала мы использовали стандартную аннотацию throws для pipeline, что заставляло каждое место вызова оборачивать даже простые преобразования в блоки do-catch, несмотря на то, что многие операции были чистыми функциями без режимов сбоя.
Наш первый подход дублировал всю функцию: одна версия называлась pipeline для не выбрасывающих преобразований, а другая — pipelineThrowing для выбрасывающих. Это разделение позволяло чистые места вызова, но создавало кошмар обслуживания, где каждое исправление ошибки требовало редактирования двух мест, а поверхность API увеличивалась с каждой новой опцией конфигурации. Кроме того, пользователи должны были знать детали реализации, чтобы выбрать правильный метод, что нарушало принципы инкапсуляции.
Второй подход оставил единую сигнатуру throws, но побуждал использовать try? для подавления предупреждений, фактически игнорируя информацию об ошибках и делая отладку невозможной, когда происходили реальные ошибки. Это нарушало гарантии безопасности и делало код хрупким, так как разработчики могли забыть обработать подлинные случаи ошибок в смешанных конвейерах, содержащих как безопасные, так и небезопасные операции.
В конечном итоге мы приняли решение rethrows, объявив func pipeline(_ transforms: [(Data) throws -> Data]) rethrows -> Data. Это позволило компилятору требовать try только тогда, когда массив замыканий содержал выбрасывающие операции, одновременно разрешая прямые вызовы для чистых вычислений. Результатом стало сокращение кода на 40%, устранение дублирующих сигнатур функций и улучшенная эргономика API, где система типов точно отражала реальные области ошибок конкретных случаев использования.
Почему Swift запрещает выбрасывать ошибки напрямую внутри тела функции rethrows, а не исключительно через параметр замыкания?
Ключевое слово rethrows создает строгий контракт прозрачности, утверждающий, что функция только распространяет ошибки, сгенерированные ее аргументами. Если вы попытаетесь throw CustomError() напрямую в теле функции, компилятор Swift отклонит это, потому что это представляет собой безусловный выброс, нарушая гарантию "только если замыкание выбрасывает". Функция должна либо обрабатывать свои собственные ошибки внутри, используя do-catch, либо преобразовывать их в возвращаемые значения, либо поднять сигнатуру до безусловного throws, что гарантирует, что вызывающие могут безопасно предполагать, что новые области ошибок не возникают от самой функции.
Как rethrows взаимодействует с несколькими параметрами замыкания и каковы последствия для распространения эффектов?
Когда функция имеет несколько параметров замыкания, отмеченных как выбрасывающие, и сама функция помечена как rethrows, функция выбрасывает, если любой из замыкающих выбрасывает, создавая объединение эффектов. Компилятор Swift отслеживает эти эффекты индивидуально через цепочку вызовов, поэтому составление функций rethrows сохраняет условную природу без ручного вмешательства. Однако, если вы трансформируете или оборачиваете замыкания перед их передачей, вы должны сохранить сигнатуру выброса в обертке, иначе компилятор будет считать аргумент не выбрасывающим, в результате чего внешняя функция потеряет свою условную способность выбрасывать.
Какова связь между rethrows и @autoclosure, и почему эта схема появляется в API утверждений?
Сочетание @autoclosure и rethrows позволяет отложенную оценку с условным распространением ошибок, где автозакрытие задерживает оценку до необходимости, а функция выбрасывает только в том случае, если эта задержка оценка выбрасывает. Эта схема является основой функций утверждения и предустановки Swift, позволяя передавать выбрасывающие выражения в утверждения без необходимости помечать вызов утверждения try. Кандидаты часто упускают, что автозакрытие должно явно объявлять () throws -> T, чтобы участвовать в контракте rethrows, и что этот механизм отделяет время оценки (отложенное) от семантики распространения ошибок (условная), что имеет решающее значение для критических путей кода, где утверждения отключены в сборках релиза.