Из-за чего метод в ограниченном расширении generic-типа недоступен в коде, где соответствующее ограничение не доказано?
Метод из ограниченного расширения generic-типа доступен только там, где компилятор доказал выполнение условия расширения. Сам факт, что конкретный экземпляр обычно использует подходящий тип, недостаточен: для generic-параметра ограничение должно быть указано явно.
Расширения позволяют отделять части реализации типа и добавлять API без изменения основного объявления. Для generic-типов этого недостаточно: некоторые операции корректны только для части возможных параметров, например сравнение значения возможно лишь при Equatable.
Ограниченные расширения решают эту проблему статически. Они позволяют описать специализированный API только для тех специализаций generic-типа, где необходимые требования гарантированы компилятором.
Рассмотрим generic-контейнер Cache<Value>. Если Value не ограничен, Swift не может предположить, что его значения сравнимы. Поэтому метод, использующий ==, нельзя безусловно добавить в основной тип.
Если ограничение известно только в конкретном месте использования, но не отражено в сигнатуре generic-кода, вызов специализированного метода недопустим. Иначе компилятор не мог бы гарантировать корректность для всех возможных типов.
Условие в extension Cache where Value: Equatable становится частью доступности объявленных в нём членов. Оно не меняет исходное объявление Cache и не делает Value соответствующим Equatable автоматически.
В check вызов разрешён, потому что ограничение Value: Equatable доказано сигнатурой функции. В generic-функции без этого ограничения вызов был бы отклонён, даже если фактический аргумент при конкретном вызове окажется, например, Int.
Проверка выполняется на этапе компиляции, а не во время выполнения. Это даёт статическую безопасность и позволяет использовать специализированные операции без приведений типов, но требует повторять ограничения в каждом generic-контексте, где они нужны.
Важно отличать это от условной конформности: ограниченное расширение добавляет доступные члены, но само по себе не объявляет соответствие протоколу. Также ограничение расширения не сужает множество допустимых экземпляров исходного generic-типа — оно лишь определяет, какой дополнительный API виден для конкретной специализации.
В библиотеке есть Cache<Value>, а операция сравнения нужна только для кэшей со сравнимыми значениями. Первый вариант — поместить matches в основной тип. Он не компилируется без ограничения на весь тип, а глобальное ограничение Value: Equatable неоправданно запретило бы использовать кэш с несравнимыми значениями.
Второй вариант — выполнять приведение Value к any Equatable во время выполнения. Это усложняет код, не даёт корректного универсального сравнения и переносит ошибку из компилятора в runtime.
Выбранное решение — ограниченное расширение. Оно сохраняет общий Cache<Value>, предоставляет matches только для сравнимых значений и требует от generic-функций явно заявлять соответствующее ограничение. В результате API остаётся точным, а некорректные вызовы отбрасываются на этапе компиляции.
Нет. Если значение передано в функцию как Cache<T>, компилятор располагает только ограничениями, указанными для T в сигнатуре этой функции. Факт, что вызывающий код передал Cache<Int>, не позволяет телу функции использовать операции, требующие T: Equatable, если это требование не объявлено явно.
Нет. Cache<Value> остаётся допустимым для любого Value. Ограниченное расширение лишь добавляет набор членов для специализаций, удовлетворяющих условию. Поэтому несоответствующий тип не становится ошибкой сам по себе — ошибка возникает только при попытке обратиться к члену, доступность которого не доказана.
Нет. Это не runtime-диспетчеризация по фактическому типу параметра. Swift проверяет ограничение статически и формирует доступ к методу на основании известного generic-контекста. Если нужно поведение, выбираемое во время выполнения, потребуется другой механизм, например полиморфизм через протокол или явное приведение, но это уже иная модель с другими компромиссами.