Почему этот код Swift не компилируется?
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension Array where Element : P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()
Компилятор сообщает: «Тип P
не соответствует протоколу P
» (или, в более поздних версиях Swift, «Использование« P »в качестве конкретного типа, соответствующего протоколу« P », не поддерживается»).
Почему нет? Это почему-то кажется дырой в языке. Я понимаю, что проблема связана с объявлением массива arr
как массива типа протокола , но разве это неразумно? Я думал, что протоколы существуют именно для того, чтобы помогать структурам с чем-то вроде иерархии типов?
swift
generics
swift-protocols
матовый
источник
источник
let arr
строке, компилятор определяет тип,[S]
и код компилируется. Похоже, что тип протокола нельзя использовать так же, как отношения класс-суперкласс.protocol P : Q { }
, P не соответствует Q.Ответы:
EDIT: еще 18 месяцев работы со Swift, еще один крупный выпуск (который предоставляет новую диагностику) и комментарий от @AyBayBay заставляют меня переписать этот ответ. Новая диагностика:
Это на самом деле делает все это намного понятнее. Это расширение:
не применяется , если
Element == P
такP
не рассматривается конкретное соответствие сP
. (Решение "положить в коробку", приведенное ниже, по-прежнему является наиболее общим.)Старый ответ:
Это еще один случай метатипов. Swift действительно хочет, чтобы вы выбрали конкретный тип для большинства нетривиальных вещей.(Я не думаю, что это на самом деле правда; вы абсолютно можете создать что-то большого размера,[P]
не является конкретным типом (вы не можете выделить блок памяти известного размераP
).P
потому что это делается косвенно .) Я не думаю, что есть какие-либо доказательства того, что это случай «не должен» работать. Это очень похоже на одно из их дел "еще не работает". (К сожалению, практически невозможно заставить Apple подтвердить разницу между этими случаями.) Тот факт, что этоArray<P>
может быть переменный тип (гдеArray
не может) указывает на то, что они уже проделали некоторую работу в этом направлении, но у метатипов Swift есть много острых углов и нереализованных случаев. Я не думаю, что вы получите лучший ответ «почему», чем этот. «Потому что компилятор этого не позволяет». (Я знаю, это неудовлетворительно. Вся моя жизнь в Swift ...)Решение почти всегда - положить вещи в коробку. Строим шрифт-ластик.
Когда Swift позволяет вам делать это напрямую (что я ожидаю в конечном итоге), скорее всего, это будет просто создание этого поля для вас автоматически. Рекурсивные перечисления имели именно такую историю. Вы должны были упаковать их, и это было невероятно раздражающим и ограничивающим, и, наконец, компилятор добавил,
indirect
чтобы сделать то же самое более автоматически.источник
==
в моем примере с массивом, мы получим ошибку: требование того же типа делает общий параметр «Элемент» не универсальным. «Почему использование Томохиро не==
генерирует ту же ошибку?»Почему протоколы не соответствуют себе?
Допускать соответствие протоколов самим себе в общем случае неразумно. Проблема заключается в требованиях статического протокола.
Это включает:
static
методы и свойстваМы можем получить доступ к этим требованиям через общий заполнитель,
T
гдеT : P
- однако мы не можем получить к ним доступ в самом типе протокола, поскольку нет конкретного соответствующего типа для пересылки. Поэтому мы не можем допуститьT
этогоP
.Рассмотрим, что произошло бы в следующем примере, если бы мы разрешили
Array
применять расширение к[P]
:Мы не можем вызвать
appendNew()
a[P]
, потому чтоP
(theElement
) не является конкретным типом и, следовательно, не может быть создан. Он должен вызываться для массива с элементами с конкретным типом, которому соответствует этот типP
.Аналогичная история со статическим методом и требованиями к свойствам:
Мы не можем говорить в терминах
SomeGeneric<P>
. Нам нужны конкретные реализации требований статического протокола (обратите внимание, что нет реализацийfoo()
илиbar
определенных в приведенном выше примере). Хотя мы можем определить реализации этих требований вP
расширении, они определены только для конкретных типов, которым соответствуютP
- вы по-прежнему не можете вызывать их наP
себе.Из-за этого Swift просто полностью запрещает нам использовать протокол как тип, который соответствует самому себе - потому что, когда у этого протокола есть статические требования, это не так.
Требования к протоколу экземпляра не представляют проблемы, поскольку вы должны вызывать их на фактическом экземпляре, который соответствует протоколу (и, следовательно, должен реализовать требования). Таким образом, при вызове требования к экземпляру с типом
P
, мы можем просто перенаправить этот вызов на реализацию этого требования базового конкретного типа.Однако создание специальных исключений для правила в этом случае может привести к неожиданным несоответствиям в том, как протоколы обрабатываются универсальным кодом. Несмотря на это, ситуация не слишком отличается от
associatedtype
требований, которые (в настоящее время) не позволяют вам использовать протокол как тип. Наличие ограничения, которое не позволяет вам использовать протокол как тип, который соответствует самому себе, когда он имеет статические требования, может быть вариантом для будущей версии языка.Изменить: И, как показано ниже, это похоже на то, к чему стремится команда Swift.
@objc
протоколыФактически, именно так язык трактует
@objc
протоколы. Когда у них нет статических требований, они подчиняются себе.Следующие компилируются нормально:
baz
требует, чтоT
соответствуетP
; но мы можем заменить вP
течениеT
потому чтоP
не имеют статические требования. Если мы добавим статическое требование кP
, пример больше не будет компилироваться:Таким образом, одним из способов решения этой проблемы является создание собственного протокола
@objc
. Конечно, во многих случаях это не идеальный обходной путь, поскольку он заставляет ваши соответствующие типы быть классами, а также требует среды выполнения Obj-C, поэтому не делает его жизнеспособным на платформах, отличных от Apple, таких как Linux.Но я подозреваю, что это ограничение является (одной из) основных причин, по которым язык уже реализует «протокол без статических требований, соответствующий сам себе» для
@objc
протоколов. Общий код, написанный на их основе, может быть значительно упрощен компилятором.Зачем? Поскольку
@objc
значения, типизированные для протокола, фактически являются просто ссылками на классы, требования которых отправляются с использованиемobjc_msgSend
. С другой стороны,@objc
значения , не типизированные для протокола, более сложны, поскольку они переносят как таблицы значений, так и таблицы-свидетели, чтобы как управлять памятью их (потенциально косвенно сохраненного) обернутого значения, так и определять, какие реализации вызывать для разных требования соответственно.Из-за этого упрощенного представления для
@objc
протоколов значение такого типа протоколаP
может совместно использовать то же представление в памяти, что и «общее значение» типа некоторого универсального заполнителяT : P
, что , по- видимому, упрощает для команды Swift возможность самосогласования. Однако это не относится к@objc
протоколам, поскольку такие общие значения в настоящее время не содержат таблиц значений или свидетелей протокола.Однако эта функция является преднамеренной и, как мы надеемся, будет развернута для не
@objc
протоколов, что подтвердил член команды Swift Слава Пестов в комментариях к SR-55 в ответ на ваш запрос об этом (вызванный этим вопросом ):Надеюсь, однажды этот язык будет поддерживать и не
@objc
протоколы.Но какие есть текущие решения для непротоколов
@objc
?Реализация расширений с ограничениями протокола
В Swift 3.1, если вам нужно расширение с ограничением, согласно которому данный общий заполнитель или связанный тип должен быть заданным типом протокола (а не только конкретным типом, который соответствует этому протоколу), вы можете просто определить это с помощью
==
ограничения.Например, мы могли бы написать расширение вашего массива как:
Конечно, теперь это не позволяет нам вызывать его в массиве с конкретными элементами типа, которые соответствуют
P
. Мы могли бы решить эту проблему, просто определив дополнительное расширение для whenElement : P
и просто перейдя к== P
расширению:Однако стоит отметить, что это приведет к преобразованию массива в O (n)
[P]
, поскольку каждый элемент должен быть помещен в экзистенциальный контейнер. Если производительность является проблемой, вы можете просто решить эту проблему, повторно реализовав метод расширения. Это не совсем удовлетворительное решение - будем надеяться, что будущая версия языка будет включать способ выражения ограничения типа протокола или соответствия типу протокола.До Swift 3.1 наиболее общий способ добиться этого, как показывает Роб в своем ответе , состоял в том , чтобы просто создать тип оболочки для a
[P]
, на котором вы затем можете определить свои методы расширения.Передача экземпляра, типизированного для протокола, в ограниченный универсальный заполнитель
Рассмотрим следующую (надуманную, но не редкость) ситуацию:
Мы не можем перейти
p
кtakesConcreteP(_:)
, поскольку в настоящее время не можем заменитьP
общий заполнительT : P
. Давайте рассмотрим несколько способов решения этой проблемы.1. Открытие экзистенциального
Вместо того , чтобы пытаться заменить
P
наT : P
, что если бы мы могли копаться в базовый тип бетона , чтоP
введенное значение было обертывание и заменить , что вместо этого? К сожалению, для этого требуется языковая функция, называемая открытием экзистенциальных данных , которая в настоящее время не доступна пользователям напрямую.Однако, Swift делает неявно открытые экзистенциалы (значения протокола типизированными) при доступе пользователей на них (т.е. выкапывает тип выполнения и делает его доступным в виде родового заполнителя). Мы можем использовать этот факт в расширении протокола на
P
:Обратите внимание на неявный универсальный
Self
заполнитель, который принимает метод расширения, который используется для ввода неявногоself
параметра - это происходит за кулисами со всеми членами расширения протокола. При вызове такого метода для значенияP
, типизированного для протокола , Swift выкапывает базовый конкретный тип и использует его для удовлетворенияSelf
универсального заполнителя. Вот почему мы можем назватьtakesConcreteP(_:)
сself
- мы удовлетворяющимT
сSelf
.Это означает, что теперь мы можем сказать:
И
takesConcreteP(_:)
вызывается с его общим заполнителем,T
который удовлетворяется базовым конкретным типом (в данном случаеS
). Обратите внимание, что это не «протоколы, соответствующие самим себе», поскольку мы заменяем конкретный тип, а неP
- попробуйте добавить статическое требование к протоколу и посмотреть, что произойдет, когда вы вызовете его изнутриtakesConcreteP(_:)
.Если Swift продолжит запрещать протоколам соответствовать самим себе, следующей лучшей альтернативой будет неявное открытие экзистенциальных данных при попытке передать их в качестве аргументов параметрам общего типа - эффективно выполняя то же самое, что и наш батут расширения протокола, только без шаблона.
Однако обратите внимание, что открытие экзистенциальных записей не является общим решением проблемы протоколов, не соответствующих самим себе. Он не имеет дело с разнородными коллекциями типизированных для протокола значений, которые могут иметь разные базовые конкретные типы. Например, рассмотрим:
По тем же причинам функция с несколькими
T
параметрами также будет проблематичной, поскольку параметры должны принимать аргументы одного и того же типа - однако, если у нас есть дваP
значения, мы не можем гарантировать во время компиляции, что они оба имеют один и тот же базовый конкретный тип.Чтобы решить эту проблему, мы можем использовать типографский ластик.
2. Создайте типографский ластик.
Как говорит Роб , типовой ластик - это самое общее решение проблемы протоколов, не соответствующих самим себе. Они позволяют нам обернуть типизированный для протокола экземпляр в конкретный тип, который соответствует этому протоколу, путем перенаправления требований экземпляра в базовый экземпляр.
Итак, давайте создадим блок стирания типа, который перенаправляет
P
требования экземпляра на базовый произвольный экземпляр, который соответствуетP
:Теперь мы можем просто говорить в терминах
AnyP
вместоP
:А теперь задумайтесь на мгновение, почему нам пришлось построить этот ящик. Как мы уже обсуждали ранее, Swift нужен конкретный тип для случаев, когда протокол имеет статические требования. Подумайте, есть ли
P
статическое требование - нам нужно было бы реализовать это вAnyP
. Но как это должно было быть реализовано? Мы имеем дело с произвольными экземплярами, которые соответствуютP
здесь - мы не знаем, как их базовые конкретные типы реализуют статические требования, поэтому мы не можем осмысленно выразить это вAnyP
.Следовательно, решение в этом случае действительно полезно только в случае требований протокола экземпляра . В общем случае мы все еще не можем рассматривать
P
как конкретный тип, соответствующийP
.источник
P
) - это нормально, потому что мы можем просто перенаправить вызовы требований экземпляра в базовый экземпляр. Однако для самого типа протокола (т.P.Protocol
Е. Буквально просто типа, который описывает протокол) - нет адепта, поэтому нечего вызывать статические требования, поэтому в приведенном выше примере у нас не может бытьSomeGeneric<P>
(Это отличается отP.Type
(экзистенциального метатипа), который описывает конкретный метатип чего-то, что соответствуетP
- но это уже другая история)P
), так и экзистенциальные метатипы (например,P.Type
метатипы). Проблема в том, что для дженериков мы на самом деле не сравниваем подобное с подобным. КогдаT
естьP
, не существует подстилающего конкретного (мета) типа для пересылки статических требований на (T
этоP.Protocol
, а не аP.Type
) ....Если вы расширяете
CollectionType
протокол вместоArray
и ограничиваете протоколом как конкретный тип, вы можете переписать предыдущий код следующим образом.источник
== P
против: P
. С == исходный пример тоже работает. И потенциальная проблема ( в зависимости от контекста) с == в том , что он исключает суб-протоколы: если я создаюprotocol SubP: P
, а затем определить ,arr
как[SubP]
тогдаarr.test()
больше не будет работать (ошибка: SubP и Р должны быть эквивалентны).