web-dev-qa-db-fra.com

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

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, "Utiliser '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 vient de la déclaration du tableau arr en tant que tableau d'un type de protocole, mais est-ce une chose déraisonnable à faire? Je pensais que les protocoles étaient là exactement pour aider à fournir des structures avec quelque chose comme une hiérarchie de types?

108
matt

EDIT: Dix-huit mois supplémentaires 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 == P puisque P n'est pas considéré comme une conformité concrète de P. (La solution "mettez-la 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 obteniez 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 vrai; vous pouvez absolument créer quelque chose de taille P parce que c'est fait par indirection .) Je ne pense pas qu'il y ait de preuve que ce soit 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'obtenir Apple pour confirmer la différence entre ces cas.) Le fait que Array<P> peut être un type variable (où Array ne peut pas) indique qu'ils ont déjà effectué un certain travail dans ce sens, mais 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. Mon ensemble Swift la vie…)

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 en créant automatiquement cette boîte pour vous. Les énumérations récursives avaient exactement cet historique. Vous deviez les encadrer et c'était incroyablement ennuyeux et contraignant, puis finalement le compilateur a ajouté indirect pour faire la même chose plus automatiquement.

58
Rob Napier

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 du protocole statique.

Ceux-ci inclus:

  • static méthodes et propriétés
  • Initialisateurs
  • 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 concret type conforme à transmettre. Par conséquent, nous ne pouvons pas permettre à T d'être P.

Considérez ce qui se passerait dans l'exemple suivant si nous autorisions l'extension Array à s'appliquer à [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() sur un [P], Car P (le 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 concret, où ce type est conforme à P.

C'est une histoire similaire avec des exigences de méthode statique et 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()

Nous ne pouvons pas parler en termes de SomeGeneric<P>. Nous avons besoin d'implémentations concrètes des exigences du protocole statique (notez comment il n'y a aucune implémentation de foo() ou bar définie dans l'exemple ci-dessus). Bien que nous puissions définir des implémentations de ces exigences dans une extension P, elles ne sont définies que pour les types concrets conformes à P - vous ne pouvez toujours pas les appeler sur P lui-même.

Pour cette raison, Swift nous interdit tout simplement d'utiliser un protocole comme un 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 conforme au protocole (et donc devez avoir implémenté les exigences). Ainsi, lorsque vous appelez une exigence sur une instance de type P, nous pouvons simplement transférer cet appel vers 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 du code générique. Bien que cela soit dit, la situation n'est pas trop différente des exigences de associatedtype - ce qui (actuellement) vous empêche 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 de la langue

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


@objc Protocoles

Et en fait, c'est exactement comment le langage traite les protocoles @objc. Quand 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 requiert que T soit conforme à P; mais nous pouvons substituer dans P pour T parce que P n'a pas d'exigences statiques. Si nous ajoutons une exigence statique à P, l'exemple ne 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 à 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 elle 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à "le protocole sans exigences statiques se conforme à lui-même" pour les protocoles @objc. 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 réparties à l'aide de objc_msgSend. D'un autre côté, les valeurs de type protocole non - @objc Sont plus compliquées, car elles transportent à la fois les tables de valeurs et de témoins afin de gérer la mémoire de leur valeur encapsulée (potentiellement stockée indirectement) et de déterminer ce qui implémentations pour appeler les différentes exigences, respectivement.

En raison de cette représentation simplifiée pour les protocoles @objc, 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 , vraisemblablement ce qui facilite la tâche de l'équipe Swift pour permettre l'auto-conformité. Il n'en va pas de même pour les non - @objc Cependant, ces valeurs génériques ne contiennent actuellement pas de tables témoins de valeur ou de protocole.

Cependant, cette fonctionnalité est intentionnelle et devrait être déployée dans des protocoles non - @objc, Comme confirmé par Swift membre de l'équipe Slava Pestov dans les commentaires de SR-55 en réponse à votre question à ce sujet (invité 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 de @objc Le rend compilable; le supprimer ne le compile plus. Certains d'entre nous sur Stack Overflow trouvent cela surprenant et aimeraient savoir s'il s'agit d'un cas délibéré ou d'un buggy Edge-case.

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

C'est délibéré - lever cette restriction est le sujet de ce bug. 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 la langue supportera un jour pour les protocoles non @objc.

Mais quelles sont les solutions actuelles pour les protocoles autres que @objc?


Implémentation d'extensions avec des contraintes de protocole

Dans Swift 3.1, si vous voulez une extension avec une contrainte qu'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 définissez simplement ceci avec une contrainte ==.

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

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 concrets conformes à P. Nous pourrions résoudre ce problème en définissant simplement une extension supplémentaire pour quand Element : P, Et simplement en avant sur l'extension == P:

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 [P], Car chaque élément devra être placé dans un conteneur existentiel. Si les performances est 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 inclura un moyen de exprimer une contrainte "type de protocole ou conforme au type de protocole".

Avant Swift 3.1, la façon la plus générale d'y parvenir, comme le montre Rob dans sa réponse , consiste simplement à construire un type de wrapper pour un [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 pas actuellement substituer 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. Ouverture des existentiels

Plutôt que d'essayer de remplacer P par T : P, Que se passerait-il si nous pouvions creuser dans le type concret sous-jacent que la valeur typée P enveloppait et le remplacer à la place? Malheureusement, cela nécessite une fonctionnalité de langage appelée ouverture d'existentiels , qui n'est actuellement pas directement disponible pour les utilisateurs.

Cependant, Swift ouvre implicitement les existentiels (valeurs de type protocole) lors de l'accès aux membres sur eux (c'est-à-dire qu'il déterre le type d'exécution et le rend accessible sous la forme d'un espace réservé générique. On peut exploiter ce fait dans une extension de protocole sur P:

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

Notez l'espace réservé générique implicite Self utilisé par la méthode d'extension, qui est utilisé pour taper le paramètre implicite self - cela se produit en arrière-plan avec tous les membres d'extension de protocole. Lorsque vous appelez une telle méthode sur une valeur de type protocole P, Swift déterre le type concret sous-jacent et l'utilise pour satisfaire l'espace réservé générique Self. c'est pourquoi nous pouvons appeler takesConcreteP(_:) avec self - nous satisfaisons T avec Self.

Cela signifie que nous pouvons maintenant dire:

p.callTakesConcreteP()

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

Si Swift continue d'interdire aux protocoles de se conformer à eux-mêmes, la meilleure alternative suivante serait d'ouvrir implicitement les existentiels lorsque vous tentez de les passer comme 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.

Notez cependant 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 paramètres T serait également problématique, car les paramètres doivent prendre des arguments du même type - cependant, si nous avons deux valeurs P, nous ne pouvons pas garantir au moment de la compilation qu'ils ont tous les deux le même type de béton sous-jacent.

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

2. Construisez une gomme à effacer

Comme Rob dit , un type eraser , 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 d'instance à l'instance sous-jacente.

Construisons donc une boîte d'effacement de type qui transmet les exigences d'instance de P à 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, réfléchissez un instant à la raison pour laquelle 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 si P avait une exigence statique - nous aurions dû l'implémenter dans AnyP. Mais comment aurait-il dû être implémenté? Nous avons affaire à des instances arbitraires conformes à P ici - nous ne savons pas comment leurs types concrets sous-jacents implémentent les exigences statiques, donc nous ne pouvons pas exprimer cela de manière significative dans AnyP.

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

81
Hamish

Si vous étendez le protocole CollectionType au lieu de Array et la contrainte par protocole comme 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()
16
Tomohiro Kumagai