Может ли бросающая реализация удовлетворить требование протокола с невыбрасывающим методом?
Нет. Метод с throws не может реализовать требование протокола, объявленное без throws, потому что такой протокол обещает вызывающему коду отсутствие ошибки. Обратное направление допустимо: невыбрасывающая реализация может удовлетворить бросающее требование.
Явное указание throws — это часть контракта функции и её эффекта выполнения. Такой контракт позволяет компилятору проверить, что потенциальная ошибка либо обработана, либо явно передана выше по стеку вызовов.
Разделение бросающих и невыбрасывающих функций решает проблему неявных исключений: вызывающий код заранее знает, должен ли учитывать сценарий ошибки. Поэтому реализация не может расширить гарантии протокола и внезапно добавить новый способ завершения.
Представим протокол загрузчика с методом без throws. Клиент может вызывать этот метод без try и не иметь do-catch. Если подставить реализацию, способную выбросить ошибку, контракт нарушится: ошибка окажется не обработанной в месте, где её не ожидали.
Обратная подстановка безопасна. Если протокол разрешает ошибку, конкретная реализация может фактически никогда её не выбрасывать, а вызывающий код всё равно обязан работать с более общим контрактом протокола.
throws является частью типа функции в отношении её эффекта. Требование func load() throws -> String допускает как бросающие, так и невыбрасывающие реализации, тогда как func load() -> String допускает только невыбрасывающую реализацию.
CacheLoader корректно соответствует Loader: его метод не выбрасывает ошибку, хотя требование протокола это разрешает. В read всё равно нужна отметка try, поскольку статический тип loader содержит бросающий контракт.
Обратный вариант — реализация func load() throws -> String для протокола с func load() -> String — компилятор отклоняет. Это не вопрос того, выбросит ли ошибка фактически конкретный вызов: важен объявленный контракт метода.
Если часть реализаций действительно может завершиться ошибкой, протокол следует объявить с throws. Если ошибка не должна быть видна клиенту, её нужно обработать внутри реализации и вернуть согласованное невыбрасывающее состояние. Скрывать ошибку только ради соответствия протоколу опасно: можно потерять причину сбоя или выдать некорректный результат.
Есть протокол хранилища, первоначально объявленный без throws, потому что первая реализация работала только с памятью. Позже добавляется сетевое хранилище, для которого возможны тайм-аут, отсутствие сети и отказ сервера.
Вариант с бросающей реализацией для старого протокола не скомпилируется. Обёртка, которая ловит ошибку и возвращает пустое значение, сохранит совместимость, но смешает «данных нет» и «произошёл сбой».
Наиболее корректное решение — изменить контракт протокола на throws, если это контролируемое изменение API. Кэш при этом может остаться невыбрасывающим, а сетевой адаптер — бросающим. Клиенты получают единый честный контракт и могут различать отсутствие данных и ошибку подключения.
Если изменение публичного протокола невозможно, следует создать новый протокол или адаптер с явно документированным поведением. Это лучше, чем молча подавлять ошибки, но увеличивает количество типов и переходный код.
Вопрос: Требуется ли try при вызове невыбрасывающей реализации через тип протокола с throws?
Ответ: Да. Проверка выполняется по статическому типу выражения, а не по фактическому типу объекта. Пока вызывающий код видит значение как Loader, он обязан учитывать возможность ошибки, даже если конкретный объект — CacheLoader.
Вопрос: Может ли адаптер превратить бросающий метод в невыбрасывающий, просто возвращая значение по умолчанию при ошибке?
Ответ: Технически да, если адаптер перехватывает ошибку внутри себя. Однако это меняет семантику API: ошибка заменяется выбранным значением по умолчанию. Такой подход допустим только когда потеря причины сбоя предусмотрена контрактом; иначе лучше сохранить throws или использовать результат, явно содержащий состояние ошибки.
Вопрос: Почему невыбрасывающая реализация безопасна для бросающего требования, хотя вызывающий код всё равно пишет try?
Ответ: Бросающий контракт задаёт верхнюю границу возможностей реализации: ошибка может быть, но не обязана возникать на каждом вызове. Невыбрасывающая реализация удовлетворяет этому контракту, потому что никогда не нарушает обещание обработки ошибок. try остаётся необходимым из-за статического типа и позволяет заменить реализацию на действительно бросающую без изменения вызывающего кода.