Протокол не соответствует самому себе?

126

Почему этот код 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как массива типа протокола , но разве это неразумно? Я думал, что протоколы существуют именно для того, чтобы помогать структурам с чем-то вроде иерархии типов?

матовый
источник
1
Когда вы удаляете аннотацию типа в let arrстроке, компилятор определяет тип, [S]и код компилируется. Похоже, что тип протокола нельзя использовать так же, как отношения класс-суперкласс.
vadian
1
@vadian Правильно, это то, что я имел в виду в своем вопросе, когда сказал: «Я понимаю, что проблема связана с объявлением массива arr как массива типа протокола». Но, как я продолжаю говорить в своем вопросе, весь смысл протоколов обычно заключается в том, что их можно использовать так же, как отношения класс-суперкласс! Они предназначены для обеспечения своего рода иерархической структуры мира структур. И они обычно это делают. Вопрос в том, почему это не должно работать здесь ?
Мэтт
1
По-прежнему не работает в Xcode 7.1, но теперь сообщение об ошибке «использование 'P', поскольку конкретный тип, соответствующий протоколу 'P', не поддерживается» .
Martin R
1
@MartinR Это лучшее сообщение об ошибке. Но мне это все еще кажется дырой в языке.
Мэтт
Конечно! Даже при protocol P : Q { }, P не соответствует Q.
Мартин Р.

Ответы:

66

EDIT: еще 18 месяцев работы со Swift, еще один крупный выпуск (который предоставляет новую диагностику) и комментарий от @AyBayBay заставляют меня переписать этот ответ. Новая диагностика:

«Использование« P »в качестве конкретного типа, соответствующего протоколу« P », не поддерживается».

Это на самом деле делает все это намного понятнее. Это расширение:

extension Array where Element : P {

не применяется , если Element == Pтак Pне рассматривается конкретное соответствие с P. (Решение "положить в коробку", приведенное ниже, по-прежнему является наиболее общим.)


Старый ответ:

Это еще один случай метатипов. Swift действительно хочет, чтобы вы выбрали конкретный тип для большинства нетривиальных вещей. [P]не является конкретным типом (вы не можете выделить блок памяти известного размера P). (Я не думаю, что это на самом деле правда; вы абсолютно можете создать что-то большого размера, Pпотому что это делается косвенно .) Я не думаю, что есть какие-либо доказательства того, что это случай «не должен» работать. Это очень похоже на одно из их дел "еще не работает". (К сожалению, практически невозможно заставить Apple подтвердить разницу между этими случаями.) Тот факт, что это Array<P>может быть переменный тип (гдеArrayне может) указывает на то, что они уже проделали некоторую работу в этом направлении, но у метатипов Swift есть много острых углов и нереализованных случаев. Я не думаю, что вы получите лучший ответ «почему», чем этот. «Потому что компилятор этого не позволяет». (Я знаю, это неудовлетворительно. Вся моя жизнь в 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чтобы сделать то же самое более автоматически.

Роб Напье
источник
В этом ответе много полезной информации, но фактическое решение в ответе Томохиро лучше, чем представленное здесь решение для бокса.
jsadler 01
@jsadler Вопрос не в том, как обойти ограничение, а в том, почему оно существует. Действительно, что касается объяснения, обходной путь Томохиро вызывает больше вопросов, чем дает ответов. Если мы используем ==в моем примере с массивом, мы получим ошибку: требование того же типа делает общий параметр «Элемент» не универсальным. «Почему использование Томохиро не ==генерирует ту же ошибку?»
Мэтт
@Rob Napier Я все еще озадачен вашим ответом. Как Swift видит в вашем решении больше конкретности по сравнению с оригиналом? Кажется, вы только что обернули все в структуру ... Возможно, я изо всех сил
пытаюсь
@AyBayBay Обновленный ответ.
Роб Напье
Большое спасибо @RobNapier. Меня всегда поражала скорость ваших ответов и, откровенно говоря, то, как вы находите время, чтобы помогать людям так же сильно, как и вы. Тем не менее, ваши новые правки определенно видят это в перспективе. Еще одна вещь, на которую я хотел бы обратить внимание, мне помогло и понимание стирания шрифтов. Эта статья, в частности, проделала фантастическую работу: krakendev.io/blog/generic-protocols-and-their-shortcomings TBH Idk, как я отношусь к некоторым из этих материалов. Похоже, мы
учитываем
110

Почему протоколы не соответствуют себе?

Допускать соответствие протоколов самим себе в общем случае неразумно. Проблема заключается в требованиях статического протокола.

Это включает:

  • 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.

Хэмиш
источник
2
Может, я просто веду себя глупо, но я не понимаю, почему статический случай особенный. Мы (компилятор) знаем о статическом свойстве прототипа во время компиляции столько или меньше, сколько мы знаем о свойстве экземпляра протокола, а именно о том, что его реализует пользователь. Так в чем разница?
матовый
1
@matt Экземпляр с типизированным протоколом (то есть экземпляр с конкретным типом, завернутый в экзистенциальный P) - это нормально, потому что мы можем просто перенаправить вызовы требований экземпляра в базовый экземпляр. Однако для самого типа протокола (т. P.ProtocolЕ. Буквально просто типа, который описывает протокол) - нет адепта, поэтому нечего вызывать статические требования, поэтому в приведенном выше примере у нас не может быть SomeGeneric<P>(Это отличается от P.Type(экзистенциального метатипа), который описывает конкретный метатип чего-то, что соответствует P- но это уже другая история)
Хэмиш
Вопрос, который я задаю в верхней части этой страницы, заключается в том, почему последователь типа протокола хорош, а сам тип протокола - нет. Я понимаю, что для самого типа протокола нет адоптера. - Я не понимаю, почему перенаправить статические вызовы принимающему типу труднее, чем перенаправить вызовы экземпляра принимающему типу. Вы утверждаете, что причина сложности здесь, в частности, в природе статических требований, но я не понимаю, насколько статические требования сложнее, чем требования экземпляра.
матовый
@matt Дело не в том, что статические требования «сложнее», чем требования экземпляра - компилятор может нормально обрабатывать как экзистенциальные данные для экземпляров (т.е. экземпляр, типизированный как P), так и экзистенциальные метатипы (например, P.Typeметатипы). Проблема в том, что для дженериков мы на самом деле не сравниваем подобное с подобным. Когда Tесть P, не существует подстилающего конкретного (мета) типа для пересылки статических требований на ( Tэто P.Protocol, а не а P.Type) ....
Хэмиш
1
Меня действительно не волнует надежность и т. Д., Я просто хочу писать приложения, и если мне кажется, что это должно работать, просто должно. Язык должен быть просто инструментом, а не продуктом. Если есть какие-то случаи, в которых это действительно не сработает, тогда прекрасно запретите это в этих случаях, но позвольте всем остальным использовать кейсы, для которых он работает, и позвольте им продолжить писать приложения.
Джонатан.
17

Если вы расширяете 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()
Томохиро Кумагаи
источник
Я не думаю , что коллекция против массива имеет значение здесь, важное изменение использует == Pпротив : P. С == исходный пример тоже работает. И потенциальная проблема ( в зависимости от контекста) с == в том , что он исключает суб-протоколы: если я создаю protocol SubP: P, а затем определить , arrкак [SubP]тогда arr.test()больше не будет работать (ошибка: SubP и Р должны быть эквивалентны).
Имре