Ситуация: тип соответствует двум протоколам с одинаковым требованием, но каждый протокол предоставляет свою реализацию по умолчанию. Как Swift разрешает такой конфликт?
Если тип не объявляет собственную реализацию требования, каждое соответствие протоколу получает свою реализацию по умолчанию. Вызов через конкретный тип может стать неоднозначным, а вызов через any-значение конкретного протокола использует реализацию, записанную в witness table этого протокола. Надёжный способ устранить неоднозначность — явно реализовать требование в самом типе.
Расширения протоколов позволяют добавлять общую реализацию требований и тем самым переиспользовать поведение без базового класса. Это особенно полезно для стандартных алгоритмов, когда множество типов должно получить одинаковую реализацию.
Однако один тип может соответствовать нескольким протоколам, объявляющим одинаковые требования. Каждый протокол при этом сохраняет собственную модель поведения, поэтому Swift не может автоматически считать две реализации взаимозаменяемыми.
Рассмотрим два протокола с одинаковым методом reset(). Если тип соответствует обоим протоколам, но не объявляет reset() самостоятельно, возникает несколько потенциальных источников реализации.
Нельзя полагаться на порядок объявления протоколов или на то, какая реализация кажется более подходящей. Неявный выбор сделал бы поведение зависимым от контекста вызова и затруднил бы поддержку кода.
Если reset() является требованием обоих протоколов, соответствие каждому протоколу формируется отдельно. Для одного протокола witness table может ссылаться на его default implementation, а для другого — на реализацию из расширения второго протокола.
Минимальный пример:
Через any CacheResettable будет выбрана реализация, связанная с соответствием CacheResettable; через any SessionResettable — реализация SessionResettable. Это следствие статического выбора требования протокола через конкретную таблицу соответствия.
Если Store объявит собственный reset(), эта реализация сможет удовлетворить оба требования и устранит неоднозначность прямого вызова. Важно, чтобы метод был объявлен в самом типе до формирования соответствий; простое добавление одноимённого метода в независимое расширение протокола не превращает его автоматически в универсальное переопределение всех default implementations.
Если одноимённый метод существует только в расширении протокола и не объявлен как требование, он вообще не участвует в witness table. Тогда выбор такого метода зависит от статического типа выражения, а не от динамического типа значения.
В библиотеке есть протоколы CacheResettable и SessionResettable. Оба используют имя reset, но первый должен очищать локальный кэш, а второй — завершать пользовательскую сессию. Тип AccountStore соответствует обоим протоколам.
Вариант с двумя default implementations удобен для переиспользования, но прямой вызов accountStore.reset() неоднозначен. Вызовы через разные протоколы технически различают операции, однако одинаковое имя скрывает важное различие смысла.
Можно переименовать требования, например в clearCache() и endSession(). Это наиболее выразительный вариант, но он требует изменить API и места вызова. Можно оставить имя reset, явно реализовать его в AccountStore и внутри выбрать нужную семантику, но тогда один общий метод должен корректно представлять две разные операции.
Практически предпочтительно переименовать требования, если операции действительно различны по смыслу. Явная реализация оправдана, когда это намеренно единая операция, а два протокола лишь описывают её разные аспекты. Такое решение устраняет неоднозначность и делает контракт видимым в исходном типе.
Нет. Если метод объявлен требованием протокола, existential-вызов использует witness table конкретного соответствия. Если метод существует только в расширении и не является требованием, он выбирается статически по типу выражения; реализация конкретного типа может не участвовать в таком вызове.
Одна реализация типа обычно может быть использована для удовлетворения одинаковых требований обоих протоколов. Это снимает неоднозначность прямого вызова, но не меняет того, что у каждого протокола остаётся отдельное соответствие и отдельный контекст вызова через existential.
Да, если реализация добавляется именно типу, например в расширении Store, а не протоколу. Такая реализация становится методом типа и может использоваться как witness для соответствий. Ограниченное расширение удобно для организации кода, но его условие должно выполняться для конкретного типа; иначе эта реализация не будет доступна в соответствующем контексте.