Объясните механизм: почему условное соответствие generic-типа протоколу нельзя определить обычной проверкой во время выполнения?
Условное соответствие протоколу является частью статической системы типов: оно существует только тогда, когда компилятор доказал выполнение всех ограничений generic-параметров. Обычная проверка во время выполнения может выбрать ветку поведения, но не добавляет типу новое соответствие и не меняет набор операций, доступных generic-коду.
Условные соответствия появились, чтобы стандартные и пользовательские generic-типы могли получать протокольные возможности только при подходящих параметрах. Например, контейнер можно сделать Equatable, если его элементы сами поддерживают Equatable, не реализуя отдельное соответствие для каждой специализации.
Такой подход решает проблему дублирования реализаций и сохраняет статические гарантии Swift. Компилятор проверяет условие заранее, а не полагается на возможный сценарий выполнения программы.
Рассмотрим generic-контейнер, который должен соответствовать протоколу только при соответствии его параметра некоторому другому протоколу. Если попытаться определить это соответствие через проверку значения во время выполнения, возникает конфликт: generic-код должен знать о соответствии уже на этапе компиляции.
Без статически известного соответствия нельзя безопасно передать значение в функцию с ограничением протоколом, выбрать witness table или гарантировать наличие требуемых методов. Кроме того, один и тот же экземпляр generic-типа не может менять свой статический тип в зависимости от ветки выполнения.
Условное соответствие объявляется с ограничением, связанным с параметром generic-типа. Компилятор рассматривает его как правило: для конкретной специализации соответствие существует, если условие доказано. Если условие не выполняется или неизвестно статически, это соответствие недоступно.
Для Box<Int> компилятор знает, что Int соответствует Equatable, поэтому может использовать соответствие Box<Int>: Marked. Для Box<SomeType>, где SomeType не имеет доказанного соответствия Equatable, вызов такой функции недопустим.
Проверка во время выполнения, например приведение к existential-протоколу, может проверить, доступно ли уже существующее соответствие для конкретного динамического типа. Однако она не создаёт новое соответствие и не позволяет текущему generic-коду внезапно получить статически неизвестные методы.
Условное соответствие также должно быть однозначным. Нельзя объявить несколько пересекающихся правил, которые дают одному и тому же типу разные реализации одного протокола: компилятор должен однозначно выбрать соответствие и его witness table.
Главный компромисс — статическая проверяемость достигается ценой невозможности выразить произвольное runtime-условие как часть соответствия. Если условие действительно динамическое, его обычно моделируют обычной веткой, type erasure, existential-значением или отдельным методом, возвращающим результат проверки.
В библиотеке есть универсальный контейнер Box<Value>. Разработчик хочет передавать его в подсистему маркировки только тогда, когда значение внутри можно сравнивать.
Первый вариант — проверять тип значения во время выполнения. Он гибок, но generic-функция, принимающая T: Marked, всё равно не сможет принять произвольный Box<Value> без дополнительного runtime-протокола и ручной маршрутизации.
Второй вариант — объявить условное соответствие Box: Marked where Value: Equatable. Это решение выбрано: для каждой специализации условие проверяется статически, вызовы проверяются компилятором, а реализация не дублируется. Если в будущем потребуется поддержать динамические значения, для этого добавляют отдельный API с runtime-проверкой, не смешивая его со статическим соответствием.
1. Можно ли передать generic-тип с неизвестным параметром в функцию, требующую условное соответствие?
Нет, если ограничение не доказано в месте вызова. Например, generic-функция с параметром T, не ограниченным Equatable, не может автоматически считать Box<T> соответствующим Marked, даже если некоторые конкретные типы T это соответствие имеют. Для вызова нужно добавить соответствующее ограничение.
2. Создаёт ли runtime-приведение к протоколу условное соответствие?
Нет. Приведение проверяет наличие соответствия, которое уже сформировано правилами типа и его ограничениями. Оно может успешно сработать для конкретного динамического типа, но результат этой проверки не становится новым статическим ограничением для исходного generic-параметра.
3. Что произойдёт при двух условных соответствиях, подходящих для одной специализации?
Swift не должен выбирать реализацию по случайному приоритету. Если правила пересекаются и для одной специализации дают неоднозначный результат, программа не компилируется либо объявление соответствия отклоняется. Условия нужно переписать так, чтобы области применимости не конфликтовали, или объединить логику в одно однозначное соответствие.