web-dev-qa-db-fra.com

Swift Generics: impossible de convertir la valeur de type en type d'argument attendu

Voici mon code:

protocol SomeProtocol {
}

class A: SomeProtocol {
}

func f1<T: SomeProtocol>(ofType: T.Type, listener: (T?) -> Void) {
}

func f2<T: SomeProtocol>(ofType: T.Type, listener: ([T]?) -> Void) {
}

func g() {
    let l1: (SomeProtocol?) -> Void = ...
    let l2: ([SomeProtocol]?) -> Void = ...
    f1(ofType: A.self, listener: l1) // NO ERROR
    f2(ofType: A.self, listener: l2) // COMPILE ERROR: Cannot convert value of type '([SomeProtocol]?) -> Void' to expected argument type '([_]?) -> Void'
}

Quel est le problème avec la seconde fermeture ayant l'argument d'un tableau d'objets de type générique?

7
frangulyan

Swift 4.1 Mise à jour

Ceci est un bogue qui a été corrigé dans cette demande d'extraction } _, ce qui en fera une nouvelle version de Swift 4.1. Votre code est maintenant compilé comme prévu dans un instantané 4.1.


Pre Swift 4.1

On dirait que vous étirez trop le compilateur.

  • Il peut traiter des conversions de tableaux d'éléments sous-typés en tableaux d'éléments super-typés, par exemple [A] à [SomeProtocol] - il s'agit d'une covariance. Il est à noter que les tableaux ont toujours été un cas Edge, car les génériques arbitraires sont invariants. Certaines collections, telles que Array, ne font que obtenir un traitement spécial du compilateur permettant une covariance.

  • Il peut traiter des conversions de fonctions avec des paramètres super-typés en fonctions avec des paramètres sous-typés, par exemple (SomeProtocol) -> Void à (A) -> Void - ceci est une contravariance.

Cependant, il semble qu’il ne puisse actuellement pas faire les deux à la fois (mais, en réalité, vous devriez pouvoir vous sentir libre de signaler un bogue ).

Pour ce que cela vaut, cela n'a rien à voir avec les génériques, ce qui suit reproduit le même comportement:

protocol SomeProtocol {}
class A : SomeProtocol {}

func f1(listener: (A) -> Void) {}
func f2(listener: ([A]) -> Void) {}
func f3(listener: () -> [SomeProtocol]) {}

func g() {

    let l1: (SomeProtocol) -> Void = { _ in }        
    f1(listener: l1) // NO ERROR

    let l2: ([SomeProtocol]) -> Void = { _ in }
    f2(listener: l2) 
    // COMPILER ERROR: Cannot convert value of type '([SomeProtocol]) -> Void' to
    // expected argument type '([A]) -> Void'

    // it's the same story for function return types
    let l3: () -> [A] = { [] }
    f3(listener: l3)
    // COMPILER ERROR: Cannot convert value of type '() -> [A]' to
    // expected argument type '() -> [SomeProtocol]'
}

Jusqu'à ce que cela soit corrigé, une solution dans ce cas consiste simplement à utiliser une expression de fermeture pour agir comme un trampoline entre les deux types de fonctions:

// converting a ([SomeProtocol]) -> Void to a ([A]) -> Void.
// compiler infers closure expression to be of type ([A]) -> Void, and in the
// implementation, $0 gets implicitly converted from [A] to [SomeProtocol].
f2(listener: { l2($0) })

// converting a () -> [A] to a () -> [SomeProtocol].
// compiler infers closure expression to be of type () -> [SomeProtocol], and in the
// implementation, the result of l3 gets implicitly converted from [A] to [SomeProtocol]
f3(listener: { l3() })

Et, appliqué à votre code:

f2(ofType: A.self, listener: { l2($0) })

Cela fonctionne car le compilateur déduit que l'expression de fermeture est du type ([T]?) -> Void, qui peut être passé à f2. Dans l'implémentation de la fermeture, le compilateur effectue ensuite une conversion implicite de $0 de [T]? en [SomeProtocol]?.

Et, comme le suggère Dominik , ce trampoline pourrait également être utilisé comme surcharge supplémentaire de f2:

func f2<T : SomeProtocol>(ofType type: T.Type, listener: ([SomeProtocol]?) -> Void) {
    // pass a closure expression of type ([T]?) -> Void to the original f2, we then
    // deal with the conversion from [T]? to [SomeProtocol]? in the closure.
    // (and by "we", I mean the compiler, implicitly)
    f2(ofType: type, listener: { (arr: [T]?) in listener(arr) })
}

Vous permettant de l'appeler à nouveau comme f2(ofType: A.self, listener: l2).

7
Hamish

La fermeture de l'auditeur dans func f2<T: SomeProtocol>(ofType: T.Type, listener: ([T]?) -> Void) {...} requiert que son argument soit un tableau de T, où T est un type qui implémente SomeProtocol. En écrivant <T: SomeProtocol>, vous imposez que all les éléments de ce tableau sont du même type. 

Disons par exemple que vous avez deux classes: A et B. Les deux sont complètement distincts. Pourtant, les deux implémentent SomeProtocol. Dans ce cas, le tableau d'entrée ne peut pas être [A(), B()] en raison de la contrainte de type. Le tableau d'entrée peut être soit [A(), A()], soit [B(), B()].

Toutefois, lorsque vous définissez l2 en tant que let l2: ([SomeProtocol]?) -> Void = ..., vous autorisez la fermeture à accepter un argument tel que [A(), B()]. Par conséquent, cette fermeture et la fermeture que vous définissez dans f2 sont incompatibles et le compilateur ne peut pas convertir entre les deux.

Malheureusement, vous ne pouvez pas ajouter d’application de type à une variable telle que l2, comme indiqué ici . Ce que vous pouvez faire, c'est que si vous savez que l2 va fonctionner sur des tableaux de classe A, vous pouvez le redéfinir comme suit:

let l2: ([A]?) -> Void = { ... }

Permettez-moi d'essayer d'expliquer cela avec un exemple plus simple. Supposons que vous écriviez une fonction générique pour trouver le plus grand élément d'un tableau de comparables:

func greatest<T: Comparable>(array: [T]) -> T {
    // return greatest element in the array
}

Maintenant, si vous essayez d’appeler cette fonction comme suit:

let comparables: [Comparable] = [1, "hello"]
print(greatest(array: comparables))

Le compilateur se plaindra car il n'y a aucun moyen de comparer un int et une chaîne. Ce que vous devez faire à la place est le suivant:

let comparables: [Int] = [1, 5, 2]
print(greatest(array: comparables))
2
mohak

N'avoir rien sur la réponse de Hamish, il a 100% raison. Mais si vous voulez une solution super simple sans aucune explication ou code, ne vous contentez pas de travailler, utilisez ceci:

func f1<T: SomeProtocol>(ofType: T.Type, listener: (T?) -> Void) {
}

func f2<Z: SomeProtocol>(ofType: Z.Type, listener: ([SomeProtocol]?) -> Void) {
}
0
Dominik Bucher