Comment passer un type de classe en tant que paramètre de fonction

146

J'ai une fonction générique qui appelle un service Web et sérialise la réponse JSON en un objet.

class func invokeService<T>(service: String, withParams params: Dictionary<String, String>, returningClass: AnyClass, completionHandler handler: ((T) -> ())) {

            /* Construct the URL, call the service and parse the response */
}

Ce que j'essaie d'accomplir, c'est l'équivalent de ce code Java

public <T> T invokeService(final String serviceURLSuffix, final Map<String, String> params,
                               final Class<T> classTypeToReturn) {
}
  • Ma signature de méthode pour ce que j'essaie d'accomplir est-elle correcte?
  • Plus précisément, la spécification AnyClasscomme type de paramètre est-elle la bonne chose à faire?
  • Lors de l'appel de la méthode, je passe MyObject.selfcomme valeur de retourClass, mais j'obtiens une erreur de compilation "Impossible de convertir le type de l'expression '()' en type 'String'"
CastDAO.invokeService("test", withParams: ["test" : "test"], returningClass: CityInfo.self) { cityInfo in /*...*/

}

Éditer:

J'ai essayé d'utiliser object_getClass, comme mentionné par holex, mais maintenant j'obtiens:

erreur: "Le type 'CityInfo.Type' n'est pas conforme au protocole 'AnyObject'"

Que faut-il faire pour se conformer au protocole?

class CityInfo : NSObject {

    var cityName: String?
    var regionCode: String?
    var regionName: String?
}
Jean-François Gagnon
la source
Je ne pense pas que les génériques Swifts fonctionnent comme des javas. ainsi l'inférant ne peut pas être aussi intelligent. id omettre la chose de classe <T> et spécifier explicitement le type génériqueCastDAO.invokeService("test", withParams: ["test" : "test"]) { (ci:CityInfo) in }
Christian Dietrich

Réponses:

144

Vous l'abordez de la mauvaise manière: dans Swift, contrairement à Objective-C, les classes ont des types spécifiques et ont même une hiérarchie d'héritage (c'est-à-dire que si la classe Bhérite de A, alors B.Typehérite également de A.Type):

class A {}
class B: A {}
class C {}

// B inherits from A
let object: A = B()

// B.Type also inherits from A.Type
let type: A.Type = B.self

// Error: 'C' is not a subtype of 'A'
let type2: A.Type = C.self

C'est pourquoi vous ne devriez pas utiliser AnyClass, sauf si vous voulez vraiment autoriser n'importe quelle classe. Dans ce cas, le bon type serait T.Type, car il exprime le lien entre le returningClassparamètre et le paramètre de la fermeture.

En fait, l'utiliser à la place de AnyClasspermet au compilateur de déduire correctement les types dans l'appel de méthode:

class func invokeService<T>(service: String, withParams params: Dictionary<String, String>, returningClass: T.Type, completionHandler handler: ((T) -> ())) {
    // The compiler correctly infers that T is the class of the instances of returningClass
    handler(returningClass())
}

Maintenant, il y a le problème de la construction d'une instance de Tà passer handler: si vous essayez d'exécuter le code maintenant, le compilateur se plaindra que ce Tn'est pas constructible avec (). Et à juste titre: Tdoit être explicitement contraint d'exiger qu'il implémente un initialiseur spécifique.

Cela peut être fait avec un protocole comme le suivant:

protocol Initable {
    init()
}

class CityInfo : NSObject, Initable {
    var cityName: String?
    var regionCode: String?
    var regionName: String?

    // Nothing to change here, CityInfo already implements init()
}

Ensuite, il vous suffit de modifier les contraintes génériques invokeServicede <T>à <T: Initable>.

Pointe

Si vous obtenez des erreurs étranges comme "Impossible de convertir le type de l'expression '()' en type 'Chaîne'", il est souvent utile de déplacer chaque argument de l'appel de méthode vers sa propre variable. Cela permet de réduire le code à l'origine de l'erreur et de découvrir les problèmes d'inférence de type:

let service = "test"
let params = ["test" : "test"]
let returningClass = CityInfo.self

CastDAO.invokeService(service, withParams: params, returningClass: returningClass) { cityInfo in /*...*/

}

Maintenant, il y a deux possibilités: l'erreur se déplace vers l'une des variables (ce qui signifie que la mauvaise partie est là) ou vous obtenez un message cryptique comme "Impossible de convertir le type de l'expression ()en type ($T6) -> ($T6) -> $T5".

La cause de cette dernière erreur est que le compilateur n'est pas en mesure de déduire les types de ce que vous avez écrit. Dans ce cas, le problème est qu'il Tn'est utilisé que dans le paramètre de la fermeture et que la fermeture que vous avez passée n'indique aucun type particulier, de sorte que le compilateur ne sait pas quel type déduire. En modifiant le type de returningClasspour inclure, Tvous donnez au compilateur un moyen de déterminer le paramètre générique.

EliaCereda
la source
Merci pour l'indice T.Type - exactement ce dont j'avais besoin pour passer un type de classe en argument.
Echelon
Le "Conseil" à la fin m'a sauvé
Alex
Au lieu d'utiliser une fonction basée sur un modèle <T: Initiable>, il est également possible de passer le returnClass en tant que Initiable.Type
Devous
Dans le code d'origine, cela aurait produit des résultats différents. Le paramètre générique Test utilisé pour exprimer la relation entre le returningClasset l'objet passé à completionHandler. Si Initiable.Typeest utilisé, cette relation est perdue.
EliaCereda
ET? Les génériques ne permettent pas d'écrirefunc somefunc<U>()
Gargo
34

vous pouvez obtenir la classe de AnyObjectvia de cette façon:

Swift 3.x

let myClass: AnyClass = type(of: self)

Swift 2.x

let myClass: AnyClass = object_getClass(self)

et vous pouvez le passer en tant que paramètre plus tard, si vous le souhaitez.

holex
la source
1
vous pouvez aussi faireself.dynamicType
newacct
1
Chaque fois que j'essaie d'utiliser myClass, cela provoque une erreur "d'utilisation de type non déclaré" myClass ". Swift 3, dernières versions de Xcode et iOS
Confused
@Confused, je ne vois aucun problème de ce type, vous devrez peut-être me donner plus d'informations sur le contexte.
holex
Je fais un bouton. Dans le code de ce bouton (c'est SpriteKit, donc à l'intérieur de touchesBegan), je veux que le bouton appelle une fonction de classe, j'ai donc besoin d'une référence à cette classe. Donc, dans la classe Button, j'ai créé une variable pour contenir une référence à la classe. Mais je ne peux pas l'obtenir pour tenir une classe, ou le type qui est cette classe, quelle que soit la bonne formulation / terminologie. Je ne peux l'obtenir que pour stocker des références à des instances. De préférence, j'aimerais passer une référence au type de classe dans le bouton. Mais j'ai juste commencé par créer une référence interne, et je n'ai même pas pu faire fonctionner cela.
Confus
C'était une continuation sur la méthodologie et la réflexion que j'utilisais ici: stackoverflow.com/questions/41092440/… . Depuis, j'ai eu un peu de logique avec les protocoles, mais je me suis vite heurté au fait que je vais également devoir trouver des types et des génériques associés pour obtenir ce que je pensais pouvoir faire avec des mécanismes plus simples. Ou copiez et collez simplement beaucoup de code. C'est ce que je fais normalement;)
Confused
8

J'ai un cas d'utilisation similaire dans swift5:

class PlistUtils {

    static let shared = PlistUtils()

    // write data
    func saveItem<T: Encodable>(url: URL, value: T) -> Bool{
        let encoder = PropertyListEncoder()
        do {
            let data = try encoder.encode(value)
            try data.write(to: url)
            return true
        }catch {
            print("encode error: \(error)")
            return false
        }
    }

    // read data

    func loadItem<T: Decodable>(url: URL, type: T.Type) -> Any?{
        if let data = try? Data(contentsOf: url) {
            let decoder = PropertyListDecoder()
            do {
                let result = try decoder.decode(type, from: data)
                return result
            }catch{
                print("items decode failed ")
                return nil
            }
        }
        return nil
    }

}
Johney
la source
Je suis en train de faire quelque chose de similaire, mais je reçois une erreur au callsite: Static method … requires that 'MyDecodable.Type' conform to 'Decodable'. Pourriez-vous mettre à jour votre réponse pour donner un exemple d'appel loadItem?
Barry Jones le
Il s'avère que c'est le point où je dois apprendre la différence entre .Typeet .self(type et métatype).
Barry Jones le
0

Utilisez obj-getclass:

CastDAO.invokeService("test", withParams: ["test" : "test"], returningClass: obj-getclass(self)) { cityInfo in /*...*/

}

En supposant que soi-même est un objet d'information sur la ville.

Annuler
la source
0

Swift 5

Pas exactement la même situation, mais j'avais un problème similaire. Ce qui m'a finalement aidé, c'est ceci:

func myFunction(_ myType: AnyClass)
{
    switch type
    {
        case is MyCustomClass.Type:
            //...
            break

        case is MyCustomClassTwo.Type:
            //...
            break

        default: break
    }
}

Ensuite, vous pouvez l'appeler dans une instance de ladite classe comme ceci:

myFunction(type(of: self))

J'espère que cela aide quelqu'un dans ma même situation.

Merricat
la source