Представьте generic-функцию, принимающую тип, который должен соответствовать двум протоколам одновременно. Какой доступ к требованиям этих протоколов получает её параметр?
Ограничение параметра композицией протоколов означает, что переданный тип должен соответствовать каждому протоколу из композиции. Внутри generic-кода параметр сохраняет конкретный тип и получает доступ ко всем требованиям обоих протоколов.
Такая композиция не создаёт новый именованный протокол и не добавляет типу новую конформность. Она лишь выражает пересечение требований: подходит любой тип, который уже соответствует всем указанным протоколам.
Протоколы-композиции нужны, когда функции требуется несколько независимых возможностей объекта. Без композиции пришлось бы объявлять отдельный протокол-наследник только ради объединения требований или дублировать ограничения в разных местах.
Этот подход отделяет набор необходимых возможностей от конкретной иерархии протоколов. Тип не обязан объявлять соответствие специально созданному составному протоколу, если он уже удовлетворяет каждому компоненту композиции.
Допустим, функция должна получить объект с идентификатором и возможностью преобразовать себя в текст. Ограничение только одним из протоколов позволит передать неподходящий тип, а требование конкретного класса снизит переиспользуемость функции.
Важно также не путать generic-ограничение с existential-типом. В generic-функции Swift знает, что параметр имеет некоторый конкретный тип, соответствующий всем протоколам; при хранении значения как existential часть статической информации может быть скрыта.
В generic-ограничении композиция записывается через &. Она означает логическое «и»: тип обязан удовлетворять протоколу слева и протоколу справа. Поэтому компилятор разрешает обращаться к требованиям обоих протоколов без дополнительных проверок.
Параметр value имеет статический тип T, а не просто один из протоколов. Поэтому вызовы value.id и value.render() проверяются на этапе компиляции. Конкретный тип T выводится из аргумента, а условие композиции проверяется для этого типа.
Композиция не является номинальным типом в том же смысле, что объявленный протокол. Нельзя рассчитывать на автоматически созданную конформность вида «тип соответствует новому составному протоколу»; это только ограничение или форма представления значения.
Если у протоколов есть одинаковые требования с совместимыми сигнатурами, одна реализация типа может удовлетворить оба требования. Если требования несовместимы или конфликтуют реализации по умолчанию, потребуется явная реализация в самом типе либо иное проектирование протоколов.
Композиция особенно полезна для алгоритмов, которым нужны независимые свойства объекта. Если один и тот же набор требований является устойчивой частью публичной модели, именованный протокол-наследник может быть понятнее и лучше документировать намерение.
В модуле аналитики функция формирует запись для объекта, который должен иметь стабильный идентификатор и уметь предоставлять диагностическое представление. Разработчик может ограничить параметр конкретным базовым классом, но это исключит структуры и классы из других иерархий.
Первый вариант — объявить новый протокол, объединяющий оба требования. Его плюс — понятное доменное имя и единая точка расширения. Минус — каждый тип должен явно объявить соответствие новому протоколу, а сам протокол становится дополнительным элементом API.
Второй вариант — использовать композицию двух существующих протоколов. Он не требует специальной конформности и принимает любой подходящий тип, поэтому для локальной generic-функции это обычно лучший выбор.
Выбранная композиция сохраняет слабую связанность: аналитика зависит только от реально используемых возможностей. Если такое объединение станет повторяться во многих публичных API и получит самостоятельный смысл, его можно заменить именованным протоколом-наследником.
Нет. P & Q не объявляет новый номинальный протокол и не меняет список конформностей исходного типа. Это выражение пересечения требований, используемое в generic-ограничениях, параметрах или типах значений.
Следствие: композиция удобна для потребителя возможностей, но не заменяет именованный протокол, если другим частям системы нужно ссылаться на устойчивое понятие предметной области.
Generic-параметр сохраняет конкретный тип во время компиляции. Благодаря этому Swift может статически проверить требования композиции и выбрать специализированную реализацию generic-кода.
Existential-значение хранит экземпляр некоторого неизвестного типа, соответствующего композиции. Это удобнее для гетерогенного хранения, но операции, зависящие от Self или связанных типов, могут быть недоступны без дополнительных условий или type erasure.
Если требования совместимы по сигнатуре, одна реализация типа обычно удовлетворяет им обоим. Компилятор рассматривает её как реализацию одинакового требования, а не как два автоматически созданных метода.
Если сигнатуры различаются, тип должен реализовать оба требования отдельно, если это вообще возможно. Конфликтующие реализации по умолчанию также не превращаются в однозначный выбор: явная реализация в типе устраняет неоднозначность и фиксирует нужное поведение.