Как статический тип значения определяет вызов метода, добавленного только в расширении протокола Sequence?
Если метод объявлен только в расширении протокола Sequence, а не как требование самого протокола, его вызов выбирается по статическому типу. Поэтому значение конкретного типа может вызвать собственную реализацию при обращении напрямую, но через any Sequence или обобщённый параметр будет вызвана реализация из расширения протокола.
Чтобы обеспечить полиморфный вызов, метод нужно объявить требованием протокола. Тогда конкретный тип предоставляет реализацию через таблицу witness-методов, и она сохраняется при передаче значения через протокол.
Протокольные расширения в Swift позволяют добавлять общую реализацию алгоритмов сразу для множества типов. Это особенно важно для Sequence: общие операции обхода и преобразования можно выразить один раз и сделать доступными разным последовательностям.
Однако метод, существующий только в расширении, не является частью полиморфного интерфейса протокола. Swift трактует такой вызов как статически разрешаемый, что уменьшает неоднозначность и позволяет компилятору эффективно специализировать обобщённый код.
Представим библиотечный API, принимающий последовательность через протокол. Конкретная последовательность добавляет метод с тем же именем, что и метод расширения, рассчитывая изменить поведение.
Если этот метод не объявлен требованием протокола, ожидание полиморфизма будет ошибочным. В результате через any Sequence или параметр, ограниченный протоколом, может выполняться не специализированная реализация, а общий метод расширения. Это способно привести к неверной логике, неожиданным результатам и трудно обнаруживаемым ошибкам API.
У метода есть два принципиально разных статуса:
Минимальный пример:
При прямом вызове статический тип — Values, поэтому видна его реализация. В inspect статический тип параметра известен только как T: LabeledSequence; поскольку label не является требованием LabeledSequence, выбирается реализация расширения.
Если добавить func label() -> String в тело протокола, ситуация изменится: реализация Values станет удовлетворением требования, и вызов через обобщённый параметр будет использовать её. Это следует выбирать, когда метод является частью публичного поведения любого соответствующего типа.
Если же метод нужен только как стандартный алгоритм без возможности переопределения, расширение без требования подходит лучше. Такой вариант проще, но не предоставляет динамического полиморфизма.
В библиотеке есть собственный тип последовательности, который возвращает элементы с особой диагностической меткой. Разработчик добавляет метод label в тип, а общий API принимает any LabeledSequence и вызывает label. В отчёте неожиданно появляется стандартная метка расширения.
Рассмотрены два варианта. Оставить метод только в расширении проще и позволяет гарантировать единую реализацию, но специализированное поведение недоступно полиморфно. Добавить метод в требования протокола требует изменить контракт и реализовать его у соответствующих типов, зато поведение корректно сохраняется через existential и обобщённые функции.
Выбран второй вариант, поскольку метка является частью семантики конкретной последовательности, а не просто вспомогательным алгоритмом. После этого вызовы через any LabeledSequence используют реализацию конкретного типа, а общий API остаётся независимым от его внутреннего устройства.
Всегда ли обобщённая функция вызывает метод расширения, если параметр ограничен протоколом?
Нет. Это верно только для метода, который объявлен исключительно в расширении. Если метод указан среди требований протокола, вызов использует witness-таблицу и реализацию конкретного типа. Оптимизации компилятора могут устранить косвенный вызов, но не должны менять наблюдаемую семантику.
Изменит ли добавление перегрузки в расширении поведение вызова у конкретного типа?
Перегрузка выбирается по статическим типам аргументов и контексту вызова. Наличие метода у конкретного типа не превращает автоматически одноимённый метод расширения в динамически переопределяемый. Если требуется единый полиморфный контракт, нужную сигнатуру следует объявить требованием протокола, а не полагаться на совпадение имён.
Почему такой метод нельзя считать обычным виртуальным методом базового класса?
Протокол в Swift не задаёт наследуемую реализацию в том же смысле, что класс. Расширение предоставляет доступный по статическому типу код, а динамический полиморфизм появляется только для требований протокола. Поэтому одинаковая сигнатура в конкретном типе сама по себе не создаёт переопределение метода расширения.