Протокол не відповідає собі?


125

Чому цей код 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]і компілює код. Схоже, тип протоколу не може використовуватися так само, як відносини клас - надклас.
вадян

1
@vadian Правильно, саме на це я говорив у своєму запитанні, коли я сказав: "Я розумію, що проблема пов'язана з оголошенням масиву масиву як масиву типу протоколу". Але, як я продовжую говорити в своєму запитанні, вся суть протоколів зазвичай полягає в тому, що вони можуть використовуватися так само, як відносини клас - надклас! Вони покликані забезпечити своєрідну ієрархічну структуру світу структур. І вони зазвичай роблять. Питання в тому, чому це не повинно працювати тут ?
матовий

1
Досі не працює в Xcode 7.1, але повідомлення про помилку зараз "використовує" P "як конкретний тип, що відповідає протоколу" P ", не підтримується" .
Martin R

1
@MartinR Це краще повідомлення про помилку. Але мені це все одно здається діркою в мові.
мат

Звичайно! Навіть з protocol P : Q { }, P не відповідає Q.
Martin R

Відповіді:


66

EDIT: Ще вісімнадцять місяців роботи w / Swift, ще один головний реліз (який забезпечує нову діагностику), і коментар від @AyBayBay змушує мене написати цю відповідь. Нова діагностика:

"Використання" P "як конкретного типу, що відповідає протоколу" P ", не підтримується."

Це насправді робить це все набагато зрозумілішим. Це розширення:

extension Array where Element : P {

не застосовується, коли Element == Pоскільки Pне вважається конкретною відповідністю P. (Рішення "покласти його в коробку" нижче) як і раніше є найбільш загальним рішенням.)


Стара відповідь:

Це ще один випадок метатипів. Свіфт дуже хоче, щоб ви перейшли до конкретного типу для більшості нетривіальних речей. [P]не конкретний тип (ви не можете виділити блок пам'яті відомого розміру для P). (Я не думаю, що це насправді правда; ви можете абсолютно створити щось розмірне, Pтому що це робиться за допомогою непрямості .) Я не думаю, що є жодні докази того, що це справа "не повинна" працювати. Це схоже на те, що один із їх "ще не працює" справ. (На жаль, майже неможливо змусити Apple підтвердити різницю між цими випадками.) Факт, який Array<P>може бути змінним типом (деArrayне може) вказує на те, що вони вже провели деяку роботу в цьому напрямку, але метатипи Свіфта мають безліч гострих країв і нереалізованих випадків. Я не думаю, що ти отримаєш кращу відповідь "чому", ніж це. "Тому що компілятор цього не дозволяє." (Я не знаю, я знаю. Все моє швидке життя ...)

Рішення майже завжди - скласти речі в коробку. Ми будуємо гумку типу.

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

@jsadler Питання полягало не в тому, як обійти обмеження, а чому обмеження існує. Дійсно, що стосується пояснень, томохіро вирішує питання більше, ніж відповідає. Якщо ми використовуємо ==в моєму прикладі масиву, ми отримуємо помилку; вимога одного типу робить загальний параметр "Елемент" негенерічним. "Чому Томохіро не використовує ==таку ж помилку?
мат

@Rob Napier Я все ще здивований вашою відповіддю. Як Свіфт бачить більше конкретності у вашому рішенні порівняно з оригіналом? Ви, здавалося, щойно загорнули речі в структуру ... Ідк, можливо, я намагаюся зрозуміти систему швидкого типу, але все це здається магічним вуду
AyBayBay

@AyBayBay Оновлена ​​відповідь.
Роб Нап'є

Дуже дякую @RobNapier Я завжди вражений швидкістю ваших відповідей і відверто кажучи, як ви знаходите час, щоб допомогти людям, як і ви. Тим не менш, ваші нові зміни безумовно поставлять це в перспективу. Ще одне, що я хотів би зазначити, розуміння стирання типу також допомогло мені. Ця стаття, зокрема, зробила фантастичну роботу: krakendev.io/blog/generic-protocols-and-their-shortcomings TBH Idk, як я відчуваю деякі речі. Схоже, ми
обчислюємо

109

Чому протоколи не відповідають самим собі?

Дозволити протоколам відповідати самим собі в загальному випадку - це невідомо. Проблема полягає в вимогах статичного протоколу.

До них належать:

  • 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вимог - які (на даний момент) заважають вам використовувати протокол як тип. Наявність обмеження, яке заважає використовувати протокол як тип, який відповідає самому собі, коли він має статичні вимоги, може стати варіантом для майбутньої версії мови

Редагувати: І як це було досліджено нижче, це схоже на те, до чого прагне команда Свіфт.


@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) масиву в a [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)

А тепер подумайте на хвилинку лише, чому нам довелося будувати цей ящик. Як ми обговорювали на початку, Свіфт потребує конкретного типу для випадків, коли протокол має статичні вимоги. Поміркуйте, якби 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. З == оригінальний приклад теж працює. І потенційна проблема (залежно від контексту) з == полягає в тому, що вона виключає підпротоколи : якщо я створю a protocol SubP: P, а потім визначати arrяк [SubP]тоді, arr.test()більше не буде працювати (помилка: SubP і P повинні бути еквівалентними).
imre
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.