Le protocole ne se conforme pas à lui-même?

125

Pourquoi ce code Swift ne se compile-t-il pas?

protocol P { }
struct S: P { }

let arr:[P] = [ S() ]

extension Array where Element : P {
    func test<T>() -> [T] {
        return []
    }
}

let result : [S] = arr.test()

Le compilateur dit: "Le type Pn'est pas conforme au protocole P" (ou, dans les versions ultérieures de Swift, "L'utilisation de 'P' comme type concret conforme au protocole 'P' n'est pas prise en charge.").

Pourquoi pas? Cela ressemble à un trou dans la langue, en quelque sorte. Je me rends compte que le problème provient de la déclaration du tableau en arrtant que tableau d'un type de protocole , mais est-ce une chose déraisonnable à faire? Je pensais que les protocoles étaient exactement là pour aider à fournir des structures avec quelque chose comme une hiérarchie de types?

mat
la source
1
Lorsque vous supprimez l'annotation de type dans la let arrligne, le compilateur déduit le type [S]et le code se compile. Il semble qu'un type de protocole ne peut pas être utilisé de la même manière qu'une relation classe-super classe.
vadian
1
@vadian Correct, c'est ce à quoi je faisais référence dans ma question quand j'ai dit "Je me rends compte que le problème vient de la déclaration du tableau arr comme un tableau d'un type de protocole". Mais, comme je le dis dans ma question, tout l'intérêt des protocoles est généralement qu'ils peuvent être utilisés de la même manière qu'une relation classe-superclasse! Ils sont destinés à fournir une sorte de structure hiérarchique au monde des structures. Et ils le font généralement. La question est, pourquoi cela ne fonctionnerait-il pas ici ?
mat
1
Ne fonctionne toujours pas dans Xcode 7.1, mais le message d'erreur est maintenant "l'utilisation de 'P' comme un type concret conforme au protocole 'P' n'est pas supporté" .
Martin R
1
@MartinR C'est un meilleur message d'erreur. Mais cela me semble toujours un trou dans la langue.
mat
Sûr! Même avec protocol P : Q { }, P n'est pas conforme à Q.
Martin R

Réponses:

66

EDIT: Encore dix-huit mois de travail avec Swift, une autre version majeure (qui fournit un nouveau diagnostic), et un commentaire de @AyBayBay me donne envie de réécrire cette réponse. Le nouveau diagnostic est:

"L'utilisation de 'P' comme type concret conforme au protocole 'P' n'est pas prise en charge."

Cela rend tout cela beaucoup plus clair. Cette extension:

extension Array where Element : P {

ne s'applique pas lorsque Element == Ppuisque Pn'est pas considéré comme une conformité concrète de P. (La solution «mettre dans une boîte» ci-dessous est toujours la solution la plus générale.)


Ancienne réponse:

C'est encore un autre cas de métatypes. Swift veut vraiment que vous arriviez à un type concret pour la plupart des choses non triviales. [P]n'est pas un type concret (vous ne pouvez pas allouer un bloc de mémoire de taille connue pour P). (Je ne pense pas que ce soit vraiment vrai; vous pouvez absolument créer quelque chose de taille Pparce que c'est fait par indirection .) Je ne pense pas qu'il y ait aucune preuve que c'est un cas de "ne devrait pas" fonctionner. Cela ressemble beaucoup à l'un de leurs cas "ne fonctionne pas encore". (Malheureusement, il est presque impossible d'amener Apple à confirmer la différence entre ces cas.) Le fait que cela Array<P>puisse être un type variable (oùArraycannot) indique qu'ils ont déjà fait du travail dans ce sens, mais les métatypes Swift ont beaucoup d'arêtes vives et de cas non implémentés. Je ne pense pas que vous obtiendrez une meilleure réponse «pourquoi» que cela. "Parce que le compilateur ne le permet pas." (Insatisfaisant, je sais. Toute ma vie Swift…)

La solution est presque toujours de mettre les choses dans une boîte. Nous construisons une gomme à effacer.

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()

Lorsque Swift vous permet de le faire directement (ce à quoi je m'attends finalement), ce sera probablement simplement en créant cette boîte pour vous automatiquement. Les énumérations récursives avaient exactement cette histoire. Vous deviez les boxer et c'était incroyablement ennuyeux et contraignant, puis finalement le compilateur a ajouté indirectpour faire la même chose plus automatiquement.

Rob Napier
la source
Beaucoup d'informations utiles dans cette réponse, mais la solution réelle dans la réponse de Tomohiro est meilleure que la solution de boxe présentée ici.
jsadler
@jsadler La question n'était pas de savoir comment contourner la limitation, mais pourquoi la limitation existe. En effet, en ce qui concerne l'explication, la solution de contournement de Tomohiro soulève plus de questions qu'elle ne répond. Si nous utilisons ==dans mon exemple Array, nous obtenons une erreur, L'exigence de même type rend le paramètre générique 'Element' non générique. "Pourquoi l'utilisation de Tomohiro ne ==génère- t-elle pas la même erreur?
matt
@Rob Napier Je suis toujours perplexe devant votre réponse. Comment Swift voit-il plus de concret dans votre solution par rapport à l'original? Vous semblez avoir juste enveloppé les choses dans une structure ... Idk peut-être que j'ai du mal à comprendre le système de type rapide mais tout cela semble être du vaudou magique
AyBayBay
@AyBayBay Réponse mise à jour.
Rob Napier
Merci beaucoup @RobNapier Je suis toujours étonné de la rapidité de vos réponses et franchement de la façon dont vous trouvez le temps d'aider les gens autant que vous. Néanmoins, vos nouvelles modifications le mettent définitivement en perspective. Une dernière chose que je voudrais souligner, comprendre l'effacement de caractères m'a également aidé. Cet article en particulier a fait un travail fantastique: krakendev.io/blog/generic-protocols-and-their-shortcomings TBH Idk ce que je ressens à propos de certaines de ces choses. Il semble que nous tenions compte des trous dans la langue, mais je pense comment Apple en créerait une partie.
AyBayBay
109

Pourquoi les protocoles ne se conforment-ils pas à eux-mêmes?

Permettre aux protocoles de se conformer à eux-mêmes dans le cas général n'est pas valable. Le problème réside dans les exigences de protocole statique.

Ceux-ci inclus:

  • static méthodes et propriétés
  • Initialiseurs
  • Types associés (bien que ceux-ci empêchent actuellement l'utilisation d'un protocole comme type réel)

Nous pouvons accéder à ces exigences sur un espace réservé générique TT : P- mais nous ne pouvons pas y accéder sur le type de protocole lui-même, car il n'y a pas de type conforme concret vers lequel transférer. Par conséquent, nous ne pouvons pas permettre Td'être P.

Considérez ce qui se passerait dans l'exemple suivant si nous permettions à l' Arrayextension d'être applicable à [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()

Nous ne pouvons pas appeler appendNew()a [P], car P(the Element) n'est pas un type concret et ne peut donc pas être instancié. Il doit être appelé sur un tableau avec des éléments de type béton, où ce type est conforme P.

C'est une histoire similaire avec une méthode statique et des exigences de propriété:

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()

On ne peut pas parler en termes de SomeGeneric<P>. Nous avons besoin d'implémentations concrètes des exigences du protocole statique (notez qu'il n'y a pas d' implémentations de foo()ou bardéfinies dans l'exemple ci-dessus). Bien que nous puissions définir des implémentations de ces exigences dans une Pextension, celles-ci ne sont définies que pour les types concrets qui se conforment à P- vous ne pouvez toujours pas les appeler sur Plui-même.

Pour cette raison, Swift nous interdit complètement d'utiliser un protocole en tant que type qui se conforme à lui-même - parce que lorsque ce protocole a des exigences statiques, ce n'est pas le cas.

Les exigences du protocole d'instance ne sont pas problématiques, car vous devez les appeler sur une instance réelle qui est conforme au protocole (et doit donc avoir implémenté les exigences). Ainsi, lorsque vous appelez une exigence sur une instance typée comme P, nous pouvons simplement transférer cet appel sur l'implémentation du type concret sous-jacent de cette exigence.

Cependant, faire des exceptions spéciales pour la règle dans ce cas pourrait conduire à des incohérences surprenantes dans la façon dont les protocoles sont traités par le code générique. Bien que cela soit dit, la situation n'est pas trop différente des associatedtypeexigences - qui (actuellement) vous empêchent d'utiliser un protocole comme type. Avoir une restriction qui vous empêche d'utiliser un protocole comme un type qui se conforme à lui-même lorsqu'il a des exigences statiques pourrait être une option pour une future version du langage

Edit: Et comme expliqué ci-dessous, cela ressemble à ce que l'équipe Swift vise.


@objc protocoles

Et en fait, c'est exactement ainsi que le langage traite les @objcprotocoles. Lorsqu'ils n'ont pas d'exigences statiques, ils se conforment à eux-mêmes.

Ce qui suit compile très bien:

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)

bazexige que se Tconforme à P; mais nous pouvons remplacer Ppar Tcar Pn'a pas d'exigences statiques. Si nous ajoutons une exigence statique à P, l'exemple ne se compile plus:

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)'

Une solution de contournement à ce problème consiste donc à créer votre protocole @objc. Certes, ce n'est pas une solution de contournement idéale dans de nombreux cas, car cela oblige vos types conformes à être des classes, tout en nécessitant le runtime Obj-C, ce qui ne le rend pas viable sur des plates-formes non Apple telles que Linux.

Mais je soupçonne que cette limitation est (l'une des) principales raisons pour lesquelles le langage implémente déjà «un protocole sans exigences statiques se conforme à lui-même» pour les @objcprotocoles. Le code générique écrit autour d'eux peut être considérablement simplifié par le compilateur.

Pourquoi? Parce que @objcles valeurs de type protocole ne sont en fait que des références de classe dont les exigences sont distribuées à l'aide de objc_msgSend. D'un autre côté, @objcles valeurs non typées par protocole sont plus compliquées, car elles transportent à la fois des tables de valeurs et de témoins afin à la fois de gérer la mémoire de leur valeur enveloppée (potentiellement stockée indirectement) et de déterminer les implémentations à appeler pour les différentes exigences, respectivement.

En raison de cette représentation simplifiée des @objcprotocoles, une valeur d'un tel type de protocole Ppeut partager la même représentation mémoire qu'une `` valeur générique '' de type un espace réservé générique T : P, ce qui permet vraisemblablement à l'équipe Swift d'autoriser l'auto-conformité. @objcCependant, il n'en va pas de même pour les non- protocoles, car ces valeurs génériques ne portent actuellement pas de tables de valeurs ou de témoins de protocole.

Cependant, cette fonctionnalité est intentionnelle et devrait être déployée sur des non- @objcprotocoles, comme l'a confirmé Slava Pestov, membre de l'équipe Swift, dans les commentaires de SR-55 en réponse à votre question à ce sujet (invitée par cette question ):

Matt Neuburg a ajouté un commentaire - 7 sept. 2017 13:33

Cela compile:

@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) }

L'ajout le @objcfait compiler; le supprimer l'empêche de se recompiler. Certains d'entre nous sur Stack Overflow trouvent cela surprenant et aimeraient savoir si c'est délibéré ou un buggy edge-case.

Slava Pestov a ajouté un commentaire - 7 sept. 2017 13:53

C'est délibéré - la levée de cette restriction est le sujet de ce bogue. Comme je l'ai dit, c'est délicat et nous n'avons pas encore de plans concrets.

J'espère donc que c'est quelque chose que le langage supportera un jour pour les non- @objcprotocoles également.

Mais quelles sont les solutions actuelles pour les non- @objcprotocoles?


Implémentation d'extensions avec des contraintes de protocole

Dans Swift 3.1, si vous voulez une extension avec une contrainte selon laquelle un espace réservé générique donné ou un type associé doit être un type de protocole donné (pas seulement un type concret conforme à ce protocole) - vous pouvez simplement le définir avec une ==contrainte.

Par exemple, nous pourrions écrire votre extension de tableau comme suit:

extension Array where Element == P {
  func test<T>() -> [T] {
    return []
  }
}

let arr: [P] = [S()]
let result: [S] = arr.test()

Bien sûr, cela nous empêche maintenant de l'appeler sur un tableau avec des éléments de type concret conformes à P. Nous pourrions résoudre ce problème en définissant simplement une extension supplémentaire pour quand Element : P, et juste en avant sur l' == Pextension:

extension Array where Element : P {
  func test<T>() -> [T] {
    return (self as [P]).test()
  }
}

let arr = [S()]
let result: [S] = arr.test()

Cependant, il convient de noter que cela effectuera une conversion O (n) du tableau en a [P], car chaque élément devra être encadré dans un conteneur existentiel. Si les performances sont un problème, vous pouvez simplement le résoudre en réimplémentant la méthode d'extension. Ce n'est pas une solution entièrement satisfaisante - j'espère qu'une future version du langage comprendra un moyen d'exprimer une contrainte de type de protocole ou conforme au type de protocole.

Avant Swift 3.1, le moyen le plus général d'y parvenir, comme Rob le montre dans sa réponse , consiste simplement à créer un type de wrapper pour a [P], sur lequel vous pouvez ensuite définir vos méthodes d'extension.


Passer une instance de type protocole à un espace réservé générique contraint

Considérez la situation suivante (artificielle, mais pas rare):

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)

Nous ne pouvons pas passer pà takesConcreteP(_:), car nous ne pouvons actuellement pas remplacer Pun espace réservé générique T : P. Jetons un coup d'œil à quelques façons dont nous pouvons résoudre ce problème.

1. Ouvrir les existentiels

Plutôt que d' essayer de substituer Pà T : P, si nous pouvions creuser dans le type de béton sous - jacente que la Pvaleur typée était emballage et substitut qu'au lieu? Malheureusement, cela nécessite une fonctionnalité de langage appelée ouverture des existentiels , qui n'est actuellement pas directement disponible pour les utilisateurs.

Cependant, Swift fait implicitement ouvert (existentiaux valeurs dactylographié protocole-) lors de l' accès des membres sur les (il creuse le type d'exécution et le rend accessible sous la forme d'un espace réservé générique). Nous pouvons exploiter ce fait dans une extension de protocole sur P:

extension P {
  func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
    takesConcreteP(self)
  }
}

Notez l' Selfespace réservé générique implicite pris par la méthode d'extension, qui est utilisé pour taper le selfparamètre implicite - cela se produit en arrière-plan avec tous les membres d'extension de protocole. Lors de l'appel d'une telle méthode sur une valeur typée de protocole P, Swift extrait le type concret sous-jacent et l'utilise pour satisfaire leSelf espace réservé générique. C'est la raison pour laquelle nous sommes en mesure d'appeler takesConcreteP(_:)avec self- nous satisfaire Tavec Self.

Cela signifie que nous pouvons maintenant dire:

p.callTakesConcreteP()

Et takesConcreteP(_:)est appelé avec son espace réservé génériqueT satisfait par le type concret sous-jacent (dans ce cas S). Notez qu'il ne s'agit pas de "protocoles conformes à eux-mêmes", car nous substituons un type concret plutôt que P- essayez d'ajouter une exigence statique au protocole et voyez ce qui se passe lorsque vous l'appelez de l'intérieur takesConcreteP(_:).

Si Swift continue à empêcher les protocoles de se conformer à eux-mêmes, la meilleure alternative suivante serait d'ouvrir implicitement les existentiels en essayant de les passer en tant qu'arguments à des paramètres de type générique - en faisant exactement ce que notre trampoline d'extension de protocole a fait, juste sans le passe-partout.

Cependant, notez que l'ouverture des existentiels n'est pas une solution générale au problème des protocoles non conformes à eux-mêmes. Il ne traite pas des collections hétérogènes de valeurs de type protocole, qui peuvent toutes avoir différents types concrets sous-jacents. Par exemple, considérez:

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) 

Pour les mêmes raisons, une fonction avec plusieurs Tparamètres serait également problématique, car les paramètres doivent prendre des arguments du même type - cependant si nous avons deux Pvaleurs, il n'y a aucun moyen de garantir au moment de la compilation qu'ils ont tous les deux le même béton sous-jacent type.

Afin de résoudre ce problème, nous pouvons utiliser une gomme à effacer.

2. Créez une gomme à effacer

Comme le dit Rob , une gomme à effacer est la solution la plus générale au problème des protocoles non conformes à eux-mêmes. Ils nous permettent d'encapsuler une instance de type protocole dans un type concret conforme à ce protocole, en transmettant les exigences de l'instance à l'instance sous-jacente.

Alors, construisons une boîte d'effacement de type qui transmet Ples exigences d'instance sur une instance arbitraire sous-jacente conforme à 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) }
}

Maintenant, nous pouvons simplement parler en termes de AnyPau lieu de P:

let p = AnyP(S(bar: 5))
takesConcreteP(p)

// example from #1...
let array = [AnyP(S(bar: 1)), AnyP(Q(bar: 2))]
takesConcreteArrayOfP(array)

Maintenant, considérez un instant pourquoi nous avons dû construire cette boîte. Comme nous l'avons vu précédemment, Swift a besoin d'un type concret pour les cas où le protocole a des exigences statiques. Considérez s'il y Pavait une exigence statique - nous aurions dû l'implémenter dans AnyP. Mais comment aurait-il dû être mis en œuvre? Nous avons affaire à des instances arbitraires qui se conforment Pici - nous ne savons pas comment leurs types concrets sous-jacents implémentent les exigences statiques, nous ne pouvons donc pas l'exprimer de manière significative AnyP.

Par conséquent, la solution dans ce cas n'est vraiment utile que dans le cas des exigences de protocole d' instance . Dans le cas général, on ne peut toujours pas traiter Pcomme un type concret conforme à P.

Hamish
la source
2
Peut-être que je suis simplement dense, mais je ne comprends pas pourquoi le cas statique est spécial. Nous (le compilateur) en savons autant ou aussi peu sur la propriété statique d'un prototol au moment de la compilation que nous en savons sur la propriété d'instance d'un protocole, à savoir que l'adoptant l'implémentera. Alors, quelle est la différence?
mat
1
@matt Une instance de type protocole (c'est-à-dire une instance de type concret encapsulée dans existentielle P) est bien car nous pouvons simplement transférer les appels aux exigences de l'instance vers l'instance sous-jacente. Cependant, pour un type de protocole lui-même (c'est-à-dire a P.Protocol, littéralement juste le type qui décrit un protocole) - il n'y a pas d'adoptant, donc il n'y a rien sur lequel appeler les exigences statiques, c'est pourquoi dans l'exemple ci-dessus nous ne pouvons pas avoir SomeGeneric<P>(c'est différent pour un P.Type(métatype existentiel), qui décrit un métatype concret de quelque chose qui se conforme P- mais c'est une autre histoire)
Hamish
La question que je pose en haut de cette page est de savoir pourquoi un adoptant de type protocole convient et un type de protocole lui-même ne l'est pas. Je comprends que pour un type de protocole lui-même, il n'y a pas d'adoptant. - Ce que je ne comprends pas, c'est pourquoi il est plus difficile de transférer les appels statiques vers le type adoptant que de transférer les appels d'instance vers le type adoptant. Vous faites valoir que la raison pour laquelle il y a une difficulté ici est à cause de la nature des exigences statiques en particulier, mais je ne vois pas comment les exigences statiques sont plus difficiles que les exigences d'instance.
mat
@matt Ce n'est pas que les exigences statiques soient "plus dures" que les exigences d'instance - le compilateur peut gérer à la fois les exigences existentielles pour les instances (c'est-à-dire les instances typées comme P) et les métatypes existentiels (c'est-à-dire les P.Typemétatypes). Le problème est que pour les génériques - nous ne comparons pas vraiment comme pour le genre. Quand Tc'est P, il n'y a pas de type concret (méta) sous-jacent pour transmettre les exigences statiques à ( Test un P.Protocol, pas un P.Type) ....
Hamish
1
Je ne me soucie vraiment pas de la solidité, etc., je veux juste écrire des applications, et si cela devrait fonctionner, cela devrait tout simplement fonctionner. La langue doit être simplement un outil, pas un produit en soi. S'il y a des cas pour lesquels cela ne fonctionnerait vraiment pas, alors interdisez-le dans ces cas, mais laissez tout le monde utiliser les cas pour lesquels cela fonctionne et laissez-les continuer à écrire des applications.
Jonathan.
17

Si vous étendez CollectionTypeprotocole au lieu de Arrayet contrainte par protocole en tant que type concret, vous pouvez réécrire le code précédent comme suit.

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()
Tomohiro Kumagai
la source
Je ne pense pas que la collection vs tableau est pertinent en l' espèce, la modification importante utilise == Pvs : P. Avec ==, l'exemple d'origine fonctionne aussi. Et un problème potentiel (selon le contexte) avec == est qu'il exclut les sous-protocoles: si je crée un protocol SubP: P, et que je définis arrcomme [SubP]alors arr.test()ne fonctionnera plus (erreur: SubP et P doivent être équivalents).
imre le