Почему протоколы не соответствуют себе?
Допускать соответствие протоколов самим себе в общем случае неразумно. Проблема заключается в требованиях статического протокола.
Это включает:
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()
a [P]
, потому что P
(the 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 в ответ на ваш запрос об этом (вызванный этим вопросом ):
Мэтт Нойбург добавил комментарий - 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
. Мы могли бы решить эту проблему, просто определив дополнительное расширение для when 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 наиболее общий способ добиться этого, как показывает Роб в своем ответе , состоял в том , чтобы просто создать тип оболочки для a [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
.
let arr
строке, компилятор определяет тип,[S]
и код компилируется. Похоже, что тип протокола нельзя использовать так же, как отношения класс-суперкласс.