Протокол не соответствует самому себе?
Почему этот код 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
как массив типа протокола, но это неразумно? Я думал, что протоколы существуют именно для того, чтобы помочь структурам с чем-то вроде иерархии типов?
3 ответа
РЕДАКТИРОВАТЬ: еще восемнадцать месяцев работы с Swift, еще одним важным выпуском (который предоставляет новую диагностику), и комментарий от @AyBayBay заставляет меня хотеть переписать этот ответ. Новая диагностика:
"Использование" P "в качестве конкретного типа, соответствующего протоколу" P ", не поддерживается".
Это на самом деле делает все это намного яснее. Это расширение:
extension Array where Element : P {
не применяется, когда Element == P
поскольку P
не считается конкретным соответствием P
, (Приведенное ниже решение "положить его в коробку" по-прежнему является наиболее общим решением.)
Старый ответ:
Это еще один случай метатипов. Свифт действительно хочет, чтобы вы подошли к конкретному типу для большинства нетривиальных вещей. [P]
не является конкретным типом (вы не можете выделить блок памяти известного размера для P
). (Я не думаю, что это действительно так; вы можете абсолютно создать что-то размером P
потому что это сделано через косвенное обращение.) Я не думаю, что есть какие-либо доказательства того, что это случай "не должен" работать. Это очень похоже на один из их случаев "еще не работает". (К сожалению, почти невозможно заставить Apple подтвердить разницу между этими случаями.) Тот факт, что Array<P>
может быть типом переменной (где Array
не может) означает, что они уже проделали определенную работу в этом направлении, но метатипы Swift имеют много острых краев и невыполненных случаев. Я не думаю, что вы получите лучший ответ "почему", чем это. "Потому что компилятор не позволяет этого". (Неудовлетворительно, я знаю. Вся моя жизнь Свифта…)
Решение почти всегда состоит в том, чтобы положить вещи в коробку. Мы строим ластик.
protocol P { }
struct S: P { }
struct AnyPArray {
var array: [P]
init(_ array:[P]) { self.array = array }
}
extension AnyPArray {
func test<T>() -> [T] {
return []
}
}
let arr = AnyPArray([S()])
let result: [S] = arr.test()
Когда Swift позволяет вам сделать это напрямую (что я ожидаю в конечном итоге), это, скорее всего, будет сделано путем автоматического создания этого поля для вас. У рекурсивных перечислений была именно эта история. Вы должны были положить их в коробку, и это было невероятно раздражающим и ограничивающим, а затем, наконец, компилятор добавил indirect
сделать то же самое более автоматически.
Почему протоколы не соответствуют самим себе?
Разрешение протоколов соответствовать себе в общем случае нецелесообразно. Проблема заключается в требованиях статического протокола.
Они включают:
static
методы и свойства- инициализаторов
- Связанные типы (хотя в настоящее время они не позволяют использовать протокол в качестве фактического типа)
Мы можем получить доступ к этим требованиям на родовом заполнителе T
где T : P
- однако мы не можем получить к ним доступ по самому типу протокола, поскольку нет конкретного соответствующего типа для переадресации. Поэтому мы не можем позволить T
быть P
,
Рассмотрим, что произойдет в следующем примере, если мы позволим Array
расширение должно быть применимо к [P]
:
protocol P {
init()
}
struct S : P {}
struct S1 : P {}
extension Array where Element : P {
mutating func appendNew() {
// If Element is P, we cannot possibly construct a new instance of it, as you cannot
// construct an instance of a protocol.
append(Element())
}
}
var arr: [P] = [S(), S1()]
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
arr.appendNew()
Мы не можем позвонить appendNew()
на [P]
, так как P
(Element
) не является конкретным типом и, следовательно, не может быть создан. Он должен вызываться в массиве с элементами конкретного типа, где этот тип соответствует P
,
Это похожая история со статическим методом и требованиями к свойствам:
protocol P {
static func foo()
static var bar: Int { get }
}
struct SomeGeneric<T : P> {
func baz() {
// If T is P, what's the value of bar? There isn't one – because there's no
// implementation of bar's getter defined on P itself.
print(T.bar)
T.foo() // If T is P, what method are we calling here?
}
}
// error: Using 'P' as a concrete type conforming to protocol 'P' is not supported
SomeGeneric<P>().baz()
Мы не можем говорить с точки зрения SomeGeneric<P>
, Нам нужны конкретные реализации требований статического протокола (обратите внимание, что нет реализаций foo()
или же bar
определено в приведенном выше примере). Хотя мы можем определить реализацию этих требований в P
расширение, они определены только для конкретных типов, которые соответствуют P
- вы все еще не можете позвонить им P
сам.
Из-за этого Swift просто полностью запрещает нам использовать протокол как тип, который соответствует самому себе - потому что когда у этого протокола есть статические требования, он этого не делает.
Требования к протоколу экземпляра не являются проблематичными, так как вы должны вызывать их в реальном экземпляре, который соответствует протоколу (и, следовательно, должен был выполнить требования). Поэтому при вызове требования для экземпляра, набранного как P
мы можем просто перенаправить этот вызов на реализацию этого требования базового конкретного типа.
Однако создание особых исключений для правила в этом случае может привести к удивительным несоответствиям в том, как протоколы обрабатываются общим кодом. Хотя, как говорится, ситуация не слишком отличается от associatedtype
требования - которые (в настоящее время) не позволяют использовать протокол в качестве типа. Наличие ограничения, которое не позволяет вам использовать протокол как тип, который соответствует самому себе, когда у него есть статические требования, может быть вариантом для будущей версии языка
Редактировать: И, как показано ниже, это похоже на то, к чему стремится команда Swift.
@objc
протоколы
И на самом деле, именно так трактует язык @objc
протоколы. Когда у них нет статических требований, они соответствуют себе.
Следующие компиляции просто отлично:
import Foundation
@objc protocol P {
func foo()
}
class C : P {
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c)
baz
требует, чтобы T
соответствует P
; но мы можем заменить в P
за T
так как P
не имеет статических требований. Если мы добавим статическое требование к P
, пример больше не компилируется:
import Foundation
@objc protocol P {
static func bar()
func foo()
}
class C : P {
static func bar() {
print("C's bar called")
}
func foo() {
print("C's foo called!")
}
}
func baz<T : P>(_ t: T) {
t.foo()
}
let c: P = C()
baz(c) // error: Cannot invoke 'baz' with an argument list of type '(P)'
Таким образом, один из способов решения этой проблемы - сделать ваш протокол @objc
, Конечно, это не идеальный обходной путь во многих случаях, поскольку он заставляет ваши соответствующие типы быть классами, а также требует времени выполнения Obj-C, поэтому не делает его жизнеспособным на не-Apple платформах, таких как Linux.
Но я подозреваю, что это ограничение (одна из) основных причин того, почему язык уже реализует "протокол без статических требований, соответствует самому себе" для @objc
протоколы. Общий код, написанный вокруг них, может быть значительно упрощен компилятором.
Зачем? Так как @objc
Значения типа протокола являются просто ссылками на классы, требования которых отправляются с использованием objc_msgSend
, С другой стороны, не @objc
Значения типа протокола являются более сложными, поскольку они содержат таблицы значений и таблицы-свидетелей, чтобы одновременно управлять памятью их (возможно, косвенно сохраненных) упакованных значений и определять, какие реализации должны вызывать различные требования, соответственно.
Из-за этого упрощенного представления для @objc
протоколы, значение такого типа протокола P
может использовать то же представление памяти, что и "универсальное значение" типа некоторого универсального заполнителя T : P
Предположительно, это облегчает команде Swift возможность самосогласования. То же самое не относится к @objc
протоколы, тем не менее, в качестве таких общих значений в настоящее время не несут таблицы значений или свидетелей протокола.
Однако эта функция является преднамеренной и, мы надеемся, будет @objc
протоколы, подтвержденные членом команды Swift Славой Пестовым в комментариях SR-55 в ответ на ваш запрос об этом (вызванный этим вопросом):
Matt Neuburg добавил комментарий - 7 сентября 2017 13:33
Это компилирует:
@objc protocol P {} class C: P {} func process<T: P>(item: T) -> T { return item } func f(image: P) { let processed: P = process(item:image) }
Добавление
@objc
делает компиляцию; удаление этого заставляет это не собираться снова. Некоторые из нас в Stack Overflow находят это удивительным и хотели бы знать, является ли это преднамеренным или глючным крайним случаем.Слава Пестов добавил комментарий - 7 сен 2017 в 13:53
Это преднамеренно - снятие этого ограничения - вот о чем эта ошибка. Как я уже сказал, это сложно, и у нас пока нет конкретных планов.
Так что, надеюсь, однажды язык поддержит не @objc
протоколы также.
Но какие текущие решения существуют для @objc
протоколы?
Реализация расширений с ограничениями протокола
В Swift 3.1, если вы хотите расширение с ограничением, согласно которому данный универсальный заполнитель или связанный тип должен быть заданным типом протокола (а не просто конкретный тип, соответствующий этому протоколу) - вы можете просто определить это с помощью ==
ограничение.
Например, мы могли бы написать ваше расширение массива как:
extension Array where Element == P {
func test<T>() -> [T] {
return []
}
}
let arr: [P] = [S()]
let result: [S] = arr.test()
Конечно, теперь это не позволяет нам вызывать его для массива с конкретными элементами типа, которые соответствуют P
, Мы могли бы решить эту проблему, просто определив дополнительное расширение, когда Element : P
и просто вперед на == P
расширение:
extension Array where Element : P {
func test<T>() -> [T] {
return (self as [P]).test()
}
}
let arr = [S()]
let result: [S] = arr.test()
Однако стоит отметить, что это выполнит O(n) преобразование массива в [P]
, так как каждый элемент должен быть упакован в экзистенциальный контейнер. Если производительность является проблемой, вы можете просто решить эту проблему, повторно внедрив метод расширения. Это не совсем удовлетворительное решение - надеюсь, в будущей версии языка будет представлен способ выражения ограничения "тип протокола или соответствие типу протокола".
До Swift 3.1 наиболее общий способ достижения этого, как показывает Роб в своем ответе, - это просто создать тип-обертку для [P]
, который вы можете затем определить свой метод (ы) расширения.
Передача экземпляра с типом протокола ограниченному родовому заполнителю
Рассмотрим следующую (надуманную, но не редкую) ситуацию:
protocol P {
var bar: Int { get set }
func foo(str: String)
}
struct S : P {
var bar: Int
func foo(str: String) {/* ... */}
}
func takesConcreteP<T : P>(_ t: T) {/* ... */}
let p: P = S(bar: 5)
// error: Cannot invoke 'takesConcreteP' with an argument list of type '(P)'
takesConcreteP(p)
Мы не можем пройти p
в takesConcreteP(_:)
поскольку мы не можем заменить P
для общего заполнителя T : P
, Давайте рассмотрим несколько способов решения этой проблемы.
1. Открытие экзисте
Вместо того, чтобы пытаться заменить P
за T : P
что, если бы мы могли копать в конкретный конкретный тип, что P
Типизированное значение было упаковкой и заменой? К сожалению, для этого требуется языковая функция, называемая открытием экзистенциалов, которая в данный момент недоступна пользователям напрямую.
Однако Swift неявно открывает экзистенциалы (значения типа протокола) при доступе к членам на них (т. Е. Выявляет тип среды выполнения и делает его доступным в форме универсального заполнителя). Мы можем использовать этот факт в расширении протокола на P
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Обратите внимание на неявное родовое Self
заполнитель, который принимает метод расширения, который используется для ввода неявного self
параметр - это происходит за кулисами со всеми членами расширения протокола. При вызове такого метода по протоколу вводится значение P
Swift выкапывает базовый конкретный тип и использует его для удовлетворения Self
родовой заполнитель. Вот почему мы можем позвонить takesConcreteP(_:)
с self
- мы удовлетворяем T
с Self
,
Это означает, что теперь мы можем сказать:
p.callTakesConcreteP()
А также takesConcreteP(_:)
вызывается с его общим заполнителем T
будучи удовлетворенным конкретным конкретным типом (в этом случае S
). Обратите внимание, что это не "протоколы, соответствующие самим себе", поскольку мы заменяем конкретный тип, а не P
- попробуйте добавить статическое требование к протоколу и посмотреть, что происходит, когда вы вызываете его изнутри takesConcreteP(_:)
,
Если Swift по-прежнему запрещает протоколам соответствовать самим себе, следующая лучшая альтернатива - неявно открывать экзистенциалы при попытке передать их в качестве аргументов параметрам универсального типа - эффективно делая то же, что и наш батут с расширением протокола, без использования шаблона.
Однако обратите внимание, что открытие экзистенциалов не является общим решением проблемы несоответствующих им протоколов. Он не имеет дело с разнородными коллекциями типизированных по протоколу значений, которые могут иметь разные базовые конкретные типы. Например, рассмотрим:
struct Q : P {
var bar: Int
func foo(str: String) {}
}
// The placeholder `T` must be satisfied by a single type
func takesConcreteArrayOfP<T : P>(_ t: [T]) {}
// ...but an array of `P` could have elements of different underlying concrete types.
let array: [P] = [S(bar: 1), Q(bar: 2)]
// So there's no sensible concrete type we can substitute for `T`.
takesConcreteArrayOfP(array)
По тем же причинам функция с несколькими T
Параметры также могут быть проблематичными, так как параметры должны принимать аргументы одного типа - однако, если у нас есть два P
значения, мы не можем гарантировать во время компиляции, что они оба имеют один и тот же базовый конкретный тип.
Чтобы решить эту проблему, мы можем использовать ластик типов.
2. Создайте ластик типа
Как говорит Роб, ластик типов - это наиболее общее решение проблемы протоколов, не соответствующих им самим. Они позволяют нам обернуть экземпляр с типом протокола в конкретный тип, соответствующий этому протоколу, перенаправив требования к экземпляру в базовый экземпляр.
Итак, давайте создадим окно стирания типа, которое пересылает P
требования к экземпляру базового произвольного экземпляра, который соответствует P
:
struct AnyP : P {
private var base: P
init(_ base: P) {
self.base = base
}
var bar: Int {
get { return base.bar }
set { base.bar = newValue }
}
func foo(str: String) { base.foo(str: str) }
}
Теперь мы можем просто говорить с точки зрения AnyP
вместо P
:
let p = AnyP(S(bar: 5))
takesConcreteP(p)
// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)
Теперь задумайтесь на минуту, почему мы должны были построить эту коробку. Как мы уже говорили ранее, Swift нужен конкретный тип для случаев, когда протокол имеет статические требования. Рассмотрим, если P
было статическое требование - мы должны были бы реализовать это в AnyP
, Но как это должно быть реализовано? Мы имеем дело с произвольными случаями, которые соответствуют P
здесь - мы не знаем о том, как лежащие в их основе конкретные типы реализуют статические требования, поэтому мы не можем осмысленно выразить это в AnyP
,
Следовательно, решение в этом случае действительно полезно только в случае требований протокола экземпляра. В общем случае мы до сих пор не можем лечить P
как конкретный тип, который соответствует P
,
Если вы продлите CollectionType
протокол вместо Array
и ограничение по протоколу как конкретный тип, вы можете переписать предыдущий код следующим образом.
protocol P { }
struct S: P { }
let arr:[P] = [ S() ]
extension CollectionType where Generator.Element == P {
func test<T>() -> [T] {
return []
}
}
let result : [S] = arr.test()