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 P
n'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 arr
tant 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?
let arr
ligne, 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.protocol P : Q { }
, P n'est pas conforme à Q.Réponses:
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:
Cela rend tout cela beaucoup plus clair. Cette extension:
ne s'applique pas lorsque
Element == P
puisqueP
n'est pas considéré comme une conformité concrète deP
. (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.(Je ne pense pas que ce soit vraiment vrai; vous pouvez absolument créer quelque chose de taille[P]
n'est pas un type concret (vous ne pouvez pas allouer un bloc de mémoire de taille connue pourP
).P
parce 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 celaArray<P>
puisse être un type variable (oùArray
cannot) 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.
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é
indirect
pour faire la même chose plus automatiquement.la source
==
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?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ésNous pouvons accéder à ces exigences sur un espace réservé générique
T
oùT : 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 permettreT
d'êtreP
.Considérez ce qui se passerait dans l'exemple suivant si nous permettions à l'
Array
extension d'être applicable à[P]
:Nous ne pouvons pas appeler
appendNew()
a[P]
, carP
(theElement
) 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 conformeP
.C'est une histoire similaire avec une méthode statique et des exigences de propriété:
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 defoo()
oubar
définies dans l'exemple ci-dessus). Bien que nous puissions définir des implémentations de ces exigences dans uneP
extension, celles-ci ne sont définies que pour les types concrets qui se conforment àP
- vous ne pouvez toujours pas les appeler surP
lui-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
associatedtype
exigences - 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 langageEdit: Et comme expliqué ci-dessous, cela ressemble à ce que l'équipe Swift vise.
@objc
protocolesEt en fait, c'est exactement ainsi que le langage traite les
@objc
protocoles. Lorsqu'ils n'ont pas d'exigences statiques, ils se conforment à eux-mêmes.Ce qui suit compile très bien:
baz
exige que seT
conforme àP
; mais nous pouvons remplacerP
parT
carP
n'a pas d'exigences statiques. Si nous ajoutons une exigence statique àP
, l'exemple ne se compile plus: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
@objc
protocoles. Le code générique écrit autour d'eux peut être considérablement simplifié par le compilateur.Pourquoi? Parce que
@objc
les valeurs de type protocole ne sont en fait que des références de classe dont les exigences sont distribuées à l'aide deobjc_msgSend
. D'un autre côté,@objc
les 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
@objc
protocoles, une valeur d'un tel type de protocoleP
peut partager la même représentation mémoire qu'une `` valeur générique '' de type un espace réservé génériqueT : P
, ce qui permet vraisemblablement à l'équipe Swift d'autoriser l'auto-conformité.@objc
Cependant, 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-
@objc
protocoles, 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 ):J'espère donc que c'est quelque chose que le langage supportera un jour pour les non-
@objc
protocoles également.Mais quelles sont les solutions actuelles pour les non-
@objc
protocoles?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:
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 quandElement : P
, et juste en avant sur l'== P
extension: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):
Nous ne pouvons pas passer
p
àtakesConcreteP(_:)
, car nous ne pouvons actuellement pas remplacerP
un espace réservé génériqueT : 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 laP
valeur 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
:Notez l'
Self
espace réservé générique implicite pris par la méthode d'extension, qui est utilisé pour taper leself
paramè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 protocoleP
, 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'appelertakesConcreteP(_:)
avecself
- nous satisfaireT
avecSelf
.Cela signifie que nous pouvons maintenant dire:
Et
takesConcreteP(_:)
est appelé avec son espace réservé génériqueT
satisfait par le type concret sous-jacent (dans ce casS
). Notez qu'il ne s'agit pas de "protocoles conformes à eux-mêmes", car nous substituons un type concret plutôt queP
- essayez d'ajouter une exigence statique au protocole et voyez ce qui se passe lorsque vous l'appelez de l'intérieurtakesConcreteP(_:)
.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:
Pour les mêmes raisons, une fonction avec plusieurs
T
paramètres serait également problématique, car les paramètres doivent prendre des arguments du même type - cependant si nous avons deuxP
valeurs, 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
P
les exigences d'instance sur une instance arbitraire sous-jacente conforme àP
:Maintenant, nous pouvons simplement parler en termes de
AnyP
au lieu deP
: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
P
avait une exigence statique - nous aurions dû l'implémenter dansAnyP
. Mais comment aurait-il dû être mis en œuvre? Nous avons affaire à des instances arbitraires qui se conformentP
ici - 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 significativeAnyP
.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
P
comme un type concret conforme àP
.la source
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 aP.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 avoirSomeGeneric<P>
(c'est différent pour unP.Type
(métatype existentiel), qui décrit un métatype concret de quelque chose qui se conformeP
- mais c'est une autre histoire)P
) et les métatypes existentiels (c'est-à-dire lesP.Type
métatypes). Le problème est que pour les génériques - nous ne comparons pas vraiment comme pour le genre. QuandT
c'estP
, il n'y a pas de type concret (méta) sous-jacent pour transmettre les exigences statiques à (T
est unP.Protocol
, pas unP.Type
) ....Si vous étendez
CollectionType
protocole au lieu deArray
et contrainte par protocole en tant que type concret, vous pouvez réécrire le code précédent comme suit.la source
== P
vs: 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 unprotocol SubP: P
, et que je définisarr
comme[SubP]
alorsarr.test()
ne fonctionnera plus (erreur: SubP et P doivent être équivalents).