Разберите, почему реализация метода из расширения протокола может вызываться в generic-функции, несмотря на одноимённый метод конкретного типа.
Если метод объявлен только в расширении протокола, а не в самом протоколе, его вызов выбирается статически по известному типу. В generic-функции компилятор видит лишь ограничение T: Протокол, поэтому выбирает реализацию из расширения, даже если фактический тип имеет одноимённый метод.
Чтобы вызов проходил через таблицу требований протокола и использовал реализацию конкретного типа, метод нужно объявить обязательным требованием протокола. Тогда соответствие фиксируется в witness table, а вызов становится полиморфным.
Расширения протоколов появились как способ добавлять общую реализацию поведения непосредственно к протоколу. Это позволяет писать алгоритмы один раз и автоматически предоставлять реализацию всем подходящим типам.
Однако расширение может содержать два разных вида методов: реализацию требования протокола и дополнительный метод, которого среди требований нет. Эти случаи выглядят похоже в исходном коде, но имеют разную модель диспетчеризации.
Рассмотрим тип, соответствующий протоколу и объявляющий метод с именем метода из расширения. При вызове через конкретный тип разработчик обычно ожидает получить реализацию конкретного типа.
Но generic-функция может знать только протокол, а не конкретный тип. Если метод не является требованием протокола, у неё нет динамического контракта, по которому можно выбрать реализацию. Неверное ожидание приводит к неожиданному результату и особенно опасно для логирования, сериализации, форматирования и бизнес-правил.
Вызов метода, который существует только в расширении протокола, разрешается на этапе компиляции. Выбор зависит от статического типа выражения: для параметра T, ограниченного протоколом, доступна реализация расширения протокола.
У Card действительно есть метод render, поэтому вызов через конкретный тип выбирает его. Но render не входит в Renderable; внутри generic-функции это лишь метод, найденный в расширении, а не требование с динамическим соответствием.
Если объявить render требованием протокола, реализация Card станет частью его соответствия. Тогда вызов через T: Renderable будет использовать запись из witness table, связанную с конкретным типом, и вернёт "card".
Главное ограничение — нельзя полагаться на наличие одноимённого метода в типе, если он не объявлен в протоколе. Также не следует рассчитывать, что компилятор всегда заменит generic-вызов специализированной реализацией: оптимизация специализации не должна менять наблюдаемую семантику диспетчеризации.
Практическое правило простое: если метод должен быть переопределяемой частью полиморфного контракта, объявляйте его в протоколе. Если это только удобная общая операция для пользователей протокола, размещайте его в расширении как дополнительный метод.
В библиотеке есть протокол форматирования моделей. Команда добавила метод formattedValue только в расширение протокола, а отдельная модель реализовала собственное форматирование. При вызове через конкретный тип всё выглядело правильно, но общий слой обработки моделей вызывал стандартную реализацию.
Рассматривались варианты:
formattedValue требованием протокола и сохранить стандартную реализацию в его расширении. Это требует обновить соответствия типов, зато задаёт единый полиморфный контракт.Выбран третий вариант. Общий слой стал вызывать реализацию конкретной модели через witness table, а типы, которым подходит стандартное поведение, продолжили получать реализацию из расширения. Это устранило расхождение между вызовами через конкретный тип и через generic-абстракцию.
Что изменится, если метод из расширения добавить в требования протокола, сохранив стандартную реализацию?
Тогда метод станет частью протокольного контракта. Расширение будет предоставлять значение по умолчанию, а реализация конкретного типа будет записана в его witness table. Вызовы через generic-параметр или existential-протокол смогут использовать реализацию соответствующего типа.
Важно, чтобы тип явно формировал соответствие в контексте, где видит требование протокола. Простое совпадение имён само по себе не превращает произвольный метод расширения в динамическое требование.
Почему вызов через конкретный тип и через generic-параметр может отличаться, хотя фактический объект один?
Потому что компилятор использует статический тип в месте вызова. Для переменной типа Card он видит метод Card.render. Для параметра T: Renderable он видит только члены, доступные через Renderable; если render не является требованием, выбирается метод расширения.
Фактическое значение не меняется, но меняется доступная компилятору информация о его типе. Это пример статической диспетчеризации, а не ошибки хранения или копирования значения.
Поможет ли перегрузка метода в ограниченном расширении протокола устранить проблему?
Не обязательно. Ограниченное расширение, например доступное только при дополнительном generic-условии, влияет на статический выбор метода и доступность реализации. Оно не превращает метод в динамическое требование протокола.
Если поведение должно зависеть от конкретного соответствующего типа, надёжнее объявить метод требованием протокола. Ограниченные расширения полезны для предоставления алгоритмов только типам, удовлетворяющим дополнительным условиям, но не заменяют witness-based dispatch.