web-dev-qa-db-fra.com

Pourquoi ne puis-je pas passer un Protocol.Type à un paramètre générique T.Type?

Je travaillais avec Swinject et un problème me dérange. J'en ai été coincé pendant presque une journée entière. Je soupçonne que cela est dû au Swift étant un langage tapé statiquement mais je ne suis pas tout à fait sûr.

J'ai résumé mon problème dans ce terrain de jeu

protocol Protocol {}

class Class: Protocol {}

let test: Protocol.Type = Class.self

func printType(confromingClassType: Protocol.Type) {
    print(confromingClassType)
}

func printType<Service>(serviceType: Service.Type) {
    print(serviceType)
}

print(Class.self) // "Class"
printType(serviceType: Class.self) // "Class"
print(test) // "Class"
printType(confromingClassType: test) // "Class"

printType(serviceType: test) // "note: expected an argument list of type '(serviceType: Service.Type)'"

J'ai essayé différentes solutions comme test.self ou type (of: test) mais aucune ne fonctionne.

Donc je suppose que je ne peux pas appeler une fonction avec un paramètre générique fourni comme variable?

19
Guillaume L.

P.Type Contre P.Protocol

Il existe deux types de métatypes de protocole. Pour certains protocoles P et un type conforme C:

  • Un P.Protocol Décrit le type d'un protocole lui-même (la seule valeur qu'il peut contenir est P.self).
  • Un P.Type Décrit un type concret conforme au protocole. Il peut contenir une valeur de C.self, Mais pas P.self Car les protocoles ne sont pas conformes à eux-mêmes (bien qu'une exception à cette règle soit Any, comme Any est le top type , donc toute valeur de métatype peut être tapée comme Any.Type; y compris Any.self).

Le problème auquel vous êtes confronté est que pour un espace réservé générique donné T, lorsque T est un protocole P, T.Type Est pas P.Type - c'est P.Protocol.

Donc, si nous revenons à votre exemple:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
    print(serviceType)
}

let test: P.Type = C.self

// Cannot invoke 'printType' with an argument list of type '(serviceType: P.Type)'
printType(serviceType: test)

Nous ne pouvons pas passer test comme argument à printType(serviceType:). Pourquoi? Parce que test est un P.Type; et il n'y a pas de substitution pour T qui fait que le paramètre serviceType: prend un P.Type.

Si nous substituons dans P à T, le paramètre prend un P.Protocol:

printType(serviceType: P.self) // fine, P.self is of type P.Protocol, not P.Type

Si nous substituons dans un type concret pour T, tel que C, le paramètre prend un C.Type:

printType(serviceType: C.self) // C.self is of type C.Type

Pirater avec des extensions de protocole

Bon, nous avons donc appris que si nous pouvons substituer dans un type concret pour T, nous pouvons passer un C.Type à la fonction. Peut-on substituer au type dynamique que le P.Type Encapsule? 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 lors de l'accès aux membres sur une instance ou un métatype de type protocole (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 en tirer parti dans une extension de protocole:

protocol P {}
class C : P {}

func printType<T>(serviceType: T.Type) {
  print("T.self = \(T.self)")
  print("serviceType = \(serviceType)")
}

extension P {
  static func callPrintType/*<Self : P>*/(/*_ self: Self.Type*/) {
    printType(serviceType: self)
  }
}

let test: P.Type = C.self
test.callPrintType()
// T.self = C
// serviceType = C

Il y a pas mal de choses qui se passent ici, alors déballons un peu:

  • Le membre d'extension callPrintType() sur P a un espace réservé générique implicite Self qui est contraint à P. Le paramètre implicite self est saisi à l'aide de cet espace réservé.

  • Lorsque vous appelez callPrintType() sur un P.Type, Swift creuse implicitement le type dynamique que le P.Type Encapsule (il s'agit de l'ouverture du existentielle) et l'utilise pour satisfaire l'espace réservé Self. Il transmet ensuite ce métatype dynamique au paramètre implicite self.

  • Ainsi, Self sera satisfait par C, qui pourra ensuite être transféré sur l'espace réservé générique de printTypeT.


Pourquoi T.Type N'est pas P.Type Quand T == P?

Vous remarquerez comment fonctionne la solution de contournement ci-dessus, car nous avons évité de substituer dans P le paramètre générique T. Mais pourquoi lors de la substitution dans un type de protocole P pour T, T.Type n'est pas P.Type?

Eh bien, considérez:

func foo<T>(_: T.Type) {
    let t: T.Type = T.self
    print(t)
}

Et si nous substituions dans P à T? Si T.Type Est P.Type, Alors ce que nous avons est:

func foo(_: P.Type) {
    // Cannot convert value of type 'P.Protocol' to specified type 'P.Type'
    let p: P.Type = P.self
    print(p)
}

ce qui est illégal; nous ne pouvons pas affecter P.self à P.Type, car il s'agit du type P.Protocol, et non de P.Type.

Donc, le résultat est que si vous voulez un paramètre de fonction qui prend un métatype décrivant tout type concret conforme à P (plutôt que un seul type spécifique de béton conforme) - vous voulez juste un paramètre P.Type, pas des génériques. Les génériques ne modélisent pas les types hétérogènes, c'est à cela que servent les types de protocoles.

Et c'est exactement ce que vous avez avec printType(conformingClassType:):

func printType(conformingClassType: P.Type) {
    print(conformingClassType)
}

printType(conformingClassType: test) // okay

Vous pouvez lui passer test car il a un paramètre de type P.Type. Mais vous remarquerez que cela signifie maintenant que nous ne pouvons pas lui passer P.self, Car il n'est pas de type P.Type:

// Cannot convert value of type 'P.Protocol' to expected argument type 'P.Type'
printType(conformingClassType: P.self) 
30
Hamish

J'ai exécuté votre code dans une cour de récréation, et il semble que ce soit la raison pour laquelle il ne compilera pas

let test: Protocol.Type = Class.self

Si vous supprimez la déclaration de type pour test, le code fonctionnera et imprimera Class.Type à la ligne 15.

Ainsi, le code suivant se compile et s'exécute:

protocol Protocol {}

class Class: Protocol {}

let test = Class.self

func printType<Service>(serviceType: Service.Type) {
    print(serviceType)
}

print(Class.Type.self) // "Class.Type"
printType(serviceType: Class.Type.self) // "Class.Type"
print(type(of: test)) // "Class.Type"

printType(serviceType: type(of: test)) // "Class.Type"

J'espère que cela résout votre problème.


Modifier

L'erreur que je reçois dans la cour de récréation avec le code d'origine est la suivante:

Playground execution failed: error: Untitled Page 2.xcplaygroundpage:9:1: error: cannot invoke 'printType' with an argument list of type '(serviceType: Protocol.Type.Type)'
printType(serviceType: type(of: test)) // "Class.Type"

Cela signifie que vous appelez Type 2 fois, c'est pourquoi le code ne sera pas compilé, car le type que vous appelez déjà la méthode avec l'argument de type Protocol.Type.

Si vous modifiez la signature de la méthode comme ceci:

laissez test: Protocol.Type = Class.self

func printType<Service>(serviceType: Service) {
    print(serviceType)
}

tout se compilera et fonctionnera correctement, en imprimant Class.type

C'est aussi la raison pour laquelle ma première version de la réponse sera compilée, car elle affectera le bon type pour test peut appeler .Type juste une fois.

1
Catalina T.