Подтвердить что ты не робот

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

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

4b9b3361

Ответ 1

EDIT: Еще восемнадцать месяцев работы с 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, чтобы сделать то же самое более автоматически.

Ответ 2

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

Предоставление протоколам в соответствии с самим собой в общем случае является необоснованным. Проблема связана с требованиями статического протокола.

Они включают:

  • 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 the value of bar? There isn't one – because there no
    // implementation of bar 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 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 bar called")
  }

  func foo() {
    print("C 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 решения этой проблемы является создание протокола @objc. Разумеется, во многих случаях это не идеальный способ обхода, поскольку он заставляет ваши соответствующие типы быть классами, а также требует времени выполнения Obj-C, поэтому не делает его жизнеспособным на платформах Apple non-, таких как Linux.

Но я подозреваю, что это ограничение (одна из) - основные причины, по которым язык уже реализует "протокол без статических требований, сам по себе соответствует" для протоколов @objc. Компилятор может значительно упростить общий код, написанный вокруг них.

Зачем? Поскольку @objc протоколе @objc, фактически представляют собой просто ссылки на классы, требования которых отправляются с использованием objc_msgSend. С другой стороны, non- @objc протоколе @objc, более сложны, поскольку они несут как таблицы значений, так и таблицы свидетельств, чтобы как управлять памятью их (потенциально косвенно сохраненной) завершенной стоимостью, так и определять, какие реализации для вызова для разных требований, соответственно.

Из-за этого упрощенного представления для протоколов @objc значение такого типа протокола P может разделять то же представление памяти, что и "общее значение" типа, какого-то общего заполнителя T: P, что, по-видимому, облегчает команде Swift возможность само-соответствия. То же самое не относится к протоколам non- @objc, так как такие общие значения в настоящее время не переносят таблицы значений или протоколов протокола.

Однако эта функция является преднамеренной и, как мы надеемся, будет представлена в non- протоколах @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 позволяет скомпилировать; удаление его заставляет его не компилироваться снова. Некоторые из нас на Qaru находят это удивительным и хотели бы знать, является ли это преднамеренным или ошибочным краеугольным камнем.

Слава Пестов добавил (а) комментарий - 7 сен 2017 13:53

Это преднамеренное - снятие этого ограничения - вот что такое ошибка. Как я сказал, это сложно, и у нас пока нет конкретных планов.

Так что, надеюсь, что-то, что этот язык однажды поддержит для протоколов non- @objc.

Но какие текущие решения существуют для non- протоколов @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 placeholder, который принимает метод расширения, который используется для ввода неявного параметра self - это происходит за кулисами со всеми членами расширения протокола. При вызове такого метода на заданном протоколе значение P Swift выкачивает базовый конкретный тип и использует его для удовлетворения Self generic placeholder. Вот почему мы можем называть 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 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

Ответ 3

Если вы расширяете протокол 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()