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 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 permettre T
d'être P
.
Considérez ce qui se passerait dans l'exemple suivant si nous permettions à l' Array
extension 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 bar
définies dans l'exemple ci-dessus). Bien que nous puissions définir des implémentations de ces exigences dans une P
extension, celles-ci ne sont définies que pour les types concrets qui se conforment à P
- vous ne pouvez toujours pas les appeler sur P
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 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 @objc
protocoles. 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)
baz
exige que se T
conforme à P
; mais nous pouvons remplacer P
par T
car P
n'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 @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 de objc_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 protocole P
peut 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é. @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 ):
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 @objc
fait 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- @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:
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' == P
extension:
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 P
un 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 P
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
:
extension P {
func callTakesConcreteP/*<Self : P>*/(/*self: Self*/) {
takesConcreteP(self)
}
}
Notez l' Self
espace réservé générique implicite pris par la méthode d'extension, qui est utilisé pour taper le self
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 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 T
avec 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 T
paramètres serait également problématique, car les paramètres doivent prendre des arguments du même type - cependant si nous avons deux P
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
:
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 AnyP
au 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 P
avait 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 P
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 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 P
comme un type concret conforme à P
.
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.