Swift: appel d'une méthode Obj-C à  nombre d'arguments variables

Salut,


 


Il semble impossible d'appeler ce genre de méthode au nombre d'arguments variable depuis une classe Swift:



- (BOOL)executeUpdate:(NSString*)sql, ...;

D'ailleurs, l'auto-complétion ne la suggère pas.

 

Alors que son alternative au nombre d'arguments fixes est parfaitement reconnue:

 



- (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;

Y-a-t-il une astuce car je préfère utiliser la première méthode... (dans la deuxième, on envoie un array d'arguments... ce qui me pose problème quand ils ont des types différents)



 


Réponses

  • AliGatorAliGator Membre, Modérateur
    Swift supporte parfaitement les méthodes aux nombres d'argument variable (par exemple NSString(format: "Un nombre %d", 5)). On peut tout à  fait déclarer des méthodes et fonctions Swift avec un nombre variable d'argument.

    Par contre peut-être que le bridging ObjC -> Swift n'arrive pas à  traduire la méthode ObjC correctement... T'as fait un Pomme-Clic sur le nom de ta classe ObjC dans ton code Swift, histoire de voir les headers Swifts (headers ObjC traduits à  la volée en Swift par Xcode) et donc voir comment il t'a traduit ce nom de méthode ?

    Normalement cette méthode devrait être traduite en Swift sous la forme "func executeUpdate(sql: NSString, _ args: AnyObject...)" (ou peut-être "func executeUpdate(sql: NSString, _ args: CVarArg...)" mais bon ça change pas grand chose) et donc peut être appelée avec executeUpdate(sql: "TA REQUETE", arg1, arg2, arg3) depuis Swift.

    Après quand tu utilises des APIs ObjC traduites automatiquement en Swift, peut-être que la traduction à  la volée des APIs en Swift ne donne pas toujours exactement la signature que tu attends... et peut-être que la traduction de méthodes à  nombre variable d'argument donne des APIs différentes, dans le doute faut regarder les headers Swift
  • muqaddarmuqaddar Administrateur
    juin 2014 modifié #3

    OK, merci.


    J'ai réussi à  la faire afficher dans l'auto-complétion finalement. (avec un Pomme-Clic ça l'a réveillé ?).


     


    Donc, ça me suggère ça:



    executeUpdate(<#sql: String?#>, withVAList: CVaListPointer)

    sauf que je ne vois pas comment remplir CVaListPointer qui est une structure déclarée dans le header swift ?


  • AliGatorAliGator Membre, Modérateur
    Ah fuck. CVaListPointer ça sent le type spécial pour le bridging pour pouvoir encore manipuler un peu de bas niveau (structures va_list* en C) depuis le Swift, du coup ça a pas l'air automatique.

    T'es sûr qu'il n'y a pas 2 variantes ?

    Par exemple pour [NSString stringWithFormat:] en ObjC il y a 2 variantes : "stringWithFormat:" qui prend un format + un nombre variable d'arguments, et "stringWithFormat:arguments:" qui prend un format + un va_list.

    Ce paramètre de type va_list (qui est un type C bas niveau pour manipuler les arguments d'une fonction) collerait avec son équivalent CVaListPointer que tu vois en Swift. Et ca collerait aussi avec le nom de la méthode "executeUpdate:(_, withVAList:)" équivalent à  une hypothétique méthode ObjC "executeUpdate:withVAList:" (comme on a "stringWithFormat:arguments:" sur NSString).

    Donc à  mon avis il doit en trainer encore une autre version de ta méthode, qui ne prend pas un va_list / CVaListPointer en paramètre mais prend directement les paramètres variables... non ?
  • AliGatorAliGator Membre, Modérateur
    Bah withArgumentsInArray il m'a l'air très bien, non ? Un peu lourd à  utiliser (faudra que l'auteur de FMDatabase revoit ses nommages de méthodes / spécialise son API pour avoir une API dédiée à  Swift qui soit un peu plus sympa, mais bon)

    executeUpdate(sql:"SELECT * FROM table WHERE name='$1' AND value='$2'" withArgumentsInArray:[ param1, param2 ])
    L'API Swift idéale serait celle qui te permettrait de passer les param1 & param2 directement, ce que tu peux faire en te faisant un wrapper en attendant que l'auteur de FMDatabase adapte son API :
    func executeUpdate(sql: String, _ arguments: AnyObject...) {
    // Dans le corps de la fonction, arguments est un *tableau* AnyObject[] contenant lesdits arguments
    executeUpdate(sql: sql, withArgumentsInArray: arguments)
    }

    executeUpdate("SELECT * FROM table WHERE name='$1' AND value='$2", param1, param2)
    PS : Je n'utilise pas FMDatabase donc je ne sais pas ce qu'on est sensé être en placeholder dans la requête SQL, j'ai mis "$1" et "$2" mais c'est p'tet pas comme ça qu'on fait je te laisse adapter ;)
  • muqaddarmuqaddar Administrateur


    Bah withArgumentsInArray il m'a l'air très bien, non ? Un peu lourd à  utiliser (faudra que l'auteur de FMDatabase revoit ses nommages de méthodes / spécialise son API pour avoir une API dédiée à  Swift qui soit un peu plus sympa, mais bon)




     


    Sauf que si je lui envoie un array Swift, il faut que les types soient les mêmes, ce qui n'est pas souvent le cas... (on peut envoyer un ID Int, un name String...etc). C'est pour ça que je voulais l'autre méthode qui envoie les arguments aux types différents sans soucis.


     




    L'API Swift idéale serait celle qui te permettrait de passer les param1 & param2 directement, ce que tu peux faire en te faisant un wrapper en attendant que l'auteur de FMDatabase adapte son API :




     


    J'ai essayé. J'en suis là :



    import Foundation

    class FMDatabaseWrapper
    {
      var database:FMDatabase
      
      init()
      {
        database = Manager.sharedManager().database as FMDatabase
      }
      
      func executeUpdate(sql:String, _ arguments: AnyObject...)
      {
        // Dans le corps de la fonction, arguments est un *tableau* AnyObject[] contenant lesdits arguments
        database.executeUpdate(sql, withArgumentsInArray:arguments)
      }
      
    }

    Et j'appelle comme ça:



    let success:Bool = FMDatabaseWrapper().executeUpdate(request, food.ID!, food.name!)

    Par contre, il me force à  unwrapper food.ID et food.name avec (!) ce qui est contraire à  ce que je veux faire: je veux pouvoir envoyer nil pour chaque argument (et c'est pour ça qu'ils sont déclarés en String?).


     


    J'imagine que c'est la transformation en array qui pose problème pour les possibles arguments à  nil ?


    J'ai essayé d'ajouter (?) à  AnyObject pour autoriser des valeurs nil, mais le compilo bronche ! Pas simple !

  • AliGatorAliGator Membre, Modérateur
    Si le compilo gueule quand tu déclares "AnyObject?..." en type de dernier paramètre je dirais que c'est un bug.


    Essayé avec le type explicite Optional<AnyObject> plutôt que AnyObject? pour voir si ça passe mieux ?
  • muqaddarmuqaddar Administrateur
    juin 2014 modifié #9

    Ah, je crois que j'ai trouvé !



    let success:Bool = FMDatabaseWrapper().executeUpdate(request, food.ID?, food.name?)

    func executeUpdate(sql:String, _ arguments: AnyObject?...) -> Bool
      {
        // Dans le corps de la fonction, arguments est un *tableau* AnyObject[] contenant lesdits arguments
        return database.executeUpdate(sql, withArgumentsInArray:arguments as NSArray)
      }

    Il fallait caster arguments en NSArray en plus de mettre ? à  AnyObject.


    Sûrement parce que NSArray supporte des nil contrairement à  son homologue array de Swift.


     


    Cela dit, si tu peux m'expliquer le underscore (avant arguments) que je commence à  voir en Swift... je n'ai pas encore lu de doc dessus ? Bizarre.


  • AliGatorAliGator Membre, Modérateur
    Le "_" est dans la doc, t'inquiète tu vas le trouver :D

    C'est un placeholder qui sert principalement dans 2 cas :
    1) pour dire "là  je pourrais mettre une variable sauf que je m'en fous de sa valeur". Exemple typique dans les boucles for, si tu t'en fous de la valeur de l'index dans ta boucle for donc autant mettre "_" plutôt que d'avoir une variable i qui ne sert à  rien. Ou encore si tu itères sur les clés d'un dictionaire mais que tu t'en fous des valeurs associées :
    for _ in 1..10 { println("Hello") }
    for (k,_) in enumerate(dico) { println("La clé est \(k) et la valeur on s'en fout") }
    2) Pour donner un "nom de paramètre externe" qui soit "vide" dans une déclaration de fonction.
    En effet, quand tu déclares une méthode dans une classe, alors par défaut le premier argument n'a pas de nom externe mais les arguments suivants ont le même nom externe et interne, pour coller avec ce qu'on a l'habitude de voir en ObjC. Par exemple une méthode ObjC avec la signature "winesWithColor:vineyard:year:" aura pour équivalent en Swift "winesWithColor(color: VCColor, vineyard: VCVineyard, year: Int)" mais tu vas l'appeler "winesWithColor(aColor, vineyard:aVineyard, year:2007)" (et pas juste winesWithColor(aColor,aVineyard,2007) à  cause de ce comportement par défaut. C'est comme si tu avais écrit "func winesWithColor(color: VCColor, vineyard vineyard: VCVineyard, year year: Int)", avec le même nom pour les paramètres externes (nom à  donner au paramètre lors de l'appel) et internes (noms de variables que tu vas utiliser dans le corps de ta fonction) pour les paramètres 2 et 3.

    C'est le comportement par défaut des déclarations de méthodes dans une classe (bien pratique pour retrouver ses repères ObjC) mais du coup si tu n'en veux pas de ces "noms de paramètres externes" et que tu veux pouvoir appeler cette méthode avec "winesWithColor(aColor, aVineyard, 2007)", tu peux utiliser ce "_" pour forcer un "nom de paramètre externe" qui soit "vide" : avec "func winesWithColor(color: VCColor, _ vineyard: VCVineyard, _ year: Int)", le 2e et le 3e paramètre ont comme nom interne "vineyard" et "year", mais ont comme nom de paramètre externe "_" qui est la valeur spéciale pour forcer "pas de nom". Ce qui permet donc de contourner le comportement par défaut et de forcer ce paramètre à  ne pas avoir de "nom de paramètre externe".



    (A voir ce que j'ai rédigé je me rends compte que c'est plus compliqué à  expliquer que ça ne l'est, le concept est très simple, c'est une sorte de placeholder pour "pas de nom" / "je veux pas de cette variable")
  • muqaddarmuqaddar Administrateur
    juin 2014 modifié #11


    (A voir ce que j'ai rédigé je me rends compte que c'est plus compliqué à  expliquer que ça ne l'est, le concept est très simple, c'est une sorte de placeholder pour "pas de nom" / "je veux pas de cette variable")




     


    Non, j'ai bien compris ce que tu as expliqué.


    C'est pratique, même si je ne sais pas si je vais souvent m'en servir.


     


    L'exemple dans les boucles à  la limite, oui, ça peut être sympa.


  • AliGatorAliGator Membre, Modérateur
    Bah à  vrai dire le comportement par défaut est plutôt bien donc je suis d'accord on ne va pas s'en servir tous les 4 matins surtout qu'avec notre passif ObjectiveC on aime bien avoir des paramètres nommés.


    Mais pour des rares cas genre ici avec ton nombre variable d'arguments, bah en général tu veux mettre tes paramètres variables direct à  la suite de la virgule sans avoir à  les préfixer par "arguments:" donc c'est quand même utile ;-)
  • muqaddarmuqaddar Administrateur


    Mais pour des rares cas genre ici avec ton nombre variable d'arguments, bah en général tu veux mettre tes paramètres variables direct à  la suite de la virgule sans avoir à  les préfixer par "arguments:" donc c'est quand même utile ;-)




     


    Oui tu as eu une bonne idée... ou plutôt un bon réflexe.

  • AliGatorAliGator Membre, Modérateur


    Oui tu as eu une bonne idée... ou plutôt un bon réflexe.

    que veux-tu j'ai déjà  Swift dans la peau je le parle couramment maintenant déjà ... ;-)
  • AliGatorAliGator Membre, Modérateur
    J'ai essayé de faire mumuse quand même car ton problème m'a intriqué
    (merci la commande "pod try FMDB", en 2 secondes j'avais un projet vide avec FMDB d'importé où j'ai pu tester un peu directement du code sur FBDatabase alors que je ne l'avais jamais utilisé :))

    Pour moi il y a clairement un bug concernant l'exposition des méthodes ObjC à  nombre variables d'argument sur Swift. Aucune des méthodes déclarées avec un nombre variable d'argument n'est visible par Swift.

    Les 3 seules méthodes que tu vois sur la classe FMDatabase qui commencent par "executeUpdate..." sont celles déclarées en ObjC comme :

    - (BOOL)executeUpdate:(NSString*)sql withArgumentsInArray:(NSArray *)arguments;
    - (BOOL)executeUpdate:(NSString*)sql withParameterDictionary:(NSDictionary *)arguments;
    - (BOOL)executeUpdate:(NSString*)sql withVAList: (va_list)args;
    Par contre les autres méthodes, celles qui prennent un nombre variable d'argument, sont impossibles à  appeler depuis Swift, en pratique je pense que Swift (enfin le module de LLVM qui se charge de faire la transformation des APIs ObjC en Swift pour être précis) ne les voit même pas :

    - (BOOL)executeUpdate:(NSString*)sql withErrorAndBindings:(NSError**)outErr, ...
    - (BOOL)executeUpdate:(NSString*)sql, ...
    - (BOOL)executeUpdateWithFormat:(NSString *)format, ...
    Du coup pour moi c'est un réel bug, officiel, et qu'il faut remonter à  Apple avec un BugReport.
    (C'est en remontant des bugs qu'Apple va les corriger, sinon tu peux toujours attendre...)
  • muqaddarmuqaddar Administrateur
    juin 2014 modifié #16

    Oui, je vais le faire.


    ça sera mon deuxième BugReport de la semaine !


     


    EDIT: cela dit le Wrapper créé plante à  l'exécution, et plante Xcode (même en ajoutant un pointeur)... j'ai juste le temps de voir Exec Bad Access.


     


    EDIT 2: le Wrapper marche en SELECT mais pas sur UPDATE (sûrement un problème avec les arguments)


     


    EDIT 3: chaque breakpoint ajouté plante Xcode 6 :)


Connectez-vous ou Inscrivez-vous pour répondre.