Question

Je fais de la programmation Objective-C qui consiste à analyser un document NSXmlDocument et à renseigner les propriétés d'un objet à partir du résultat.

La première version ressemblait à ceci:

if([elementName compare:@"companyName"] == 0) 
  [character setCorporationName:currentElementText]; 
else if([elementName compare:@"corporationID"] == 0) 
  [character setCorporationID:currentElementText]; 
else if([elementName compare:@"name"] == 0) 
  ...

Mais je n'aime pas le motif if-else-if-else que cela produit. En regardant l'instruction switch , je constate que je ne peux gérer que ints , chars , etc., et non des objets ... il existe donc un meilleur modèle d'implémentation. Je ne suis pas au courant?

BTW, j’ai en fait proposé une meilleure solution pour définir les propriétés de l’objet, mais je souhaite en savoir plus sur le si - sinon vs switch pattern en Objective-C

Était-ce utile?

La solution

J'espère que vous me pardonnerez tous de m'avoir mis dans une impasse ici, mais j'aimerais aborder la question plus générale de l'analyse de documents XML dans Cocoa sans avoir besoin de déclarations if-else. La question telle qu'énoncée à l'origine affecte le texte de l'élément en cours à une variable d'instance de l'objet de caractère. Comme l'a souligné jmah, cela peut être résolu en utilisant un codage clé-valeur. Cependant, dans un document XML plus complexe, cela pourrait ne pas être possible. Considérez par exemple ce qui suit.

<xmlroot>
    <corporationID>
        <stockSymbol>EXAM</stockSymbol>
        <uuid>31337</uuid>
    </corporationID>
    <companyName>Example Inc.</companyName>
</xmlroot>

Il existe plusieurs approches pour traiter ce problème. De mémoire, je peux penser à deux utilisateurs de NSXMLDocument. Le premier utilise NSXMLElement. C'est assez simple et n'implique pas du tout le problème "if-else". Vous obtenez simplement l'élément racine et parcourez ses éléments nommés un par un.

NSXMLElement* root = [xmlDocument rootElement];

// Assuming that we only have one of each element.
[character setCorperationName:[[[root elementsForName:@"companyName"] objectAtIndex:0] stringValue]];

NSXMLElement* corperationId = [root elementsForName:@"corporationID"];
[character setCorperationStockSymbol:[[[corperationId elementsForName:@"stockSymbol"] objectAtIndex:0] stringValue]];
[character setCorperationUUID:[[[corperationId elementsForName:@"uuid"] objectAtIndex:0] stringValue]];

Le prochain utilise le NSXMLNode plus général, parcourt l’arborescence et utilise directement la structure if-else.

// The first line is the same as the last example, because NSXMLElement inherits from NSXMLNode
NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
    if([[aNode name] isEqualToString:@"companyName"]){
        [character setCorperationName:[aNode stringValue]];
    }else if([[aNode name] isEqualToString:@"corporationID"]){
        NSXMLNode* correctParent = aNode;
        while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
            if([[aNode name] isEqualToString:@"stockSymbol"]){
                [character setCorperationStockSymbol:[aNode stringValue]];
            }else if([[aNode name] isEqualToString:@"uuid"]){
                [character setCorperationUUID:[aNode stringValue]];
            }
        }
    }
}

C’est un bon candidat pour éliminer la structure if-else, mais comme pour le problème initial, nous ne pouvons pas simplement utiliser l’interrupteur-cas ici. Cependant, nous pouvons toujours éliminer if-else en utilisant performSelector. La première étape consiste à définir la méthode a pour chaque élément.

- (NSNode*)parse_companyName:(NSNode*)aNode
{
    [character setCorperationName:[aNode stringValue]];
    return aNode;
}

- (NSNode*)parse_corporationID:(NSNode*)aNode
{
    NSXMLNode* correctParent = aNode;
    while((aNode = [aNode nextNode]) == nil && [aNode parent != correctParent){
        [self invokeMethodForNode:aNode prefix:@"parse_corporationID_"];
    }
    return [aNode previousNode];
}

- (NSNode*)parse_corporationID_stockSymbol:(NSNode*)aNode
{
    [character setCorperationStockSymbol:[aNode stringValue]];
    return aNode;
}

- (NSNode*)parse_corporationID_uuid:(NSNode*)aNode
{
    [character setCorperationUUID:[aNode stringValue]];
    return aNode;
}

La magie se passe dans la méthode invokeMethodForNode: prefix:. Nous générons le sélecteur en fonction du nom de l'élément et le réalisons avec un nœud comme seul paramètre. Presto bango, nous avons éliminé le besoin d'une instruction if-else. Voici le code pour cette méthode.

- (NSNode*)invokeMethodForNode:(NSNode*)aNode prefix:(NSString*)aPrefix
{
    NSNode* ret = nil;
    NSString* methodName = [NSString stringWithFormat:@"%@%@:", prefix, [aNode name]];
    SEL selector = NSSelectorFromString(methodName);
    if([self respondsToSelector:selector])
        ret = [self performSelector:selector withObject:aNode];
    return ret;
}

Maintenant, au lieu de notre plus grande instruction if-else (celle qui différenciait companyName et corporationID), nous pouvons simplement écrire une ligne de code

NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
    aNode = [self invokeMethodForNode:aNode prefix:@"parse_"];
}

Maintenant, je m'excuse si je me trompe, cela fait un moment que je n'ai rien écrit avec NSXMLDocument, il est tard dans la nuit et je n'ai pas testé ce code. Donc, si vous voyez quelque chose qui ne va pas, veuillez laisser un commentaire ou modifier cette réponse.

Cependant, je crois que je viens de montrer comment des sélecteurs correctement nommés peuvent être utilisés dans Cocoa pour éliminer complètement les déclarations if-else dans des cas comme celui-ci. Il y a quelques pièges et cas d'angle. La famille de méthodes performSelector: prend uniquement 0, 1 ou 2 arguments, dont les arguments et les types de retour sont des objets. Ainsi, si les types des arguments et le type de retour ne sont pas des objets, ou s'il y a plus de deux arguments, devez utiliser un NSInvocation pour l'invoquer. Vous devez vous assurer que les noms de méthode que vous générez ne vont pas appeler d'autres méthodes, en particulier si la cible de l'appel est un autre objet, et ce système de nommage de méthode en particulier ne fonctionnera pas avec des éléments contenant des caractères non alphanumériques. Vous pouvez contourner ce problème en échappant les noms d'éléments XML dans vos noms de méthodes, ou en construisant un NSDictionary en utilisant les noms de méthodes comme clés et les sélecteurs comme valeurs. Cela peut demander beaucoup de mémoire et prendre plus de temps. La distribution de performSelector, comme je l’ai décrit, est assez rapide. Pour les très grandes déclarations if-else, cette méthode peut même être plus rapide qu'une instruction if-else.

Autres conseils

Vous devriez tirer parti du codage clé-valeur:

[character setValue:currentElementText forKey:elementName];

Si les données ne sont pas fiables, vous pouvez vérifier que la clé est valide:

if (![validKeysCollection containsObject:elementName])
    // Exception or error

Si vous souhaitez utiliser le moins de code possible et que les noms et les paramètres de vos éléments sont tous nommés, ainsi, si nom_élément est défini par @ " foo " alors le setter est setFoo: vous pouvez faire quelque chose comme:

SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", [elementName capitalizedString]]);

[character performSelector:selector withObject:currentElementText];

ou éventuellement même:

[character setValue:currentElementText forKey:elementName]; // KVC-style

Bien que cela soit bien sûr un peu plus lent que d'utiliser un ensemble d'instructions if.

[Modifier: la deuxième option a déjà été mentionnée par quelqu'un; oups!]

Puis-je suggérer d'utiliser une macro?

#define TEST( _name, _method ) \
  if ([elementName isEqualToString:@ _name] ) \
    [character _method:currentElementText]; else
#define ENDTEST { /* empty */ }

TEST( "companyName",      setCorporationName )
TEST( "setCorporationID", setCorporationID   )
TEST( "name",             setName            )
:
:
ENDTEST

L’une des méthodes utilisées avec NSStrings consiste à utiliser un NSDictionary et des énumérations. Ce n'est peut-être pas le plus élégant, mais je pense que cela rend le code un peu plus lisible. Le pseudocode suivant est extrait de un de mes projets :

typedef enum { UNKNOWNRESIDUE, DEOXYADENINE, DEOXYCYTOSINE, DEOXYGUANINE, DEOXYTHYMINE } SLSResidueType;

static NSDictionary *pdbResidueLookupTable;
...

if (pdbResidueLookupTable == nil)
{
    pdbResidueLookupTable = [[NSDictionary alloc] initWithObjectsAndKeys:
                          [NSNumber numberWithInteger:DEOXYADENINE], @"DA", 
                          [NSNumber numberWithInteger:DEOXYCYTOSINE], @"DC",
                          [NSNumber numberWithInteger:DEOXYGUANINE], @"DG",
                          [NSNumber numberWithInteger:DEOXYTHYMINE], @"DT",
                          nil]; 
}

SLSResidueType residueIdentifier = [[pdbResidueLookupTable objectForKey:residueType] intValue];
switch (residueIdentifier)
{
    case DEOXYADENINE: do something; break;
    case DEOXYCYTOSINE: do something; break;
    case DEOXYGUANINE: do something; break;
    case DEOXYTHYMINE: do something; break;
}

L’implémentation if-else que vous avez est la bonne façon de procéder, car switch ne fonctionnera pas avec les objets. À part peut-être un peu plus difficile à lire (ce qui est subjectif), il n’ya pas de réel inconvénient à utiliser les instructions if-else de cette façon.

Bien qu'il n'y ait pas nécessairement de meilleure façon de faire quelque chose comme ça pour un usage unique, pourquoi utiliser "comparer". quand vous pouvez utiliser "isEqualToString"? Cela semblerait être plus performant puisque la comparaison s’arrêterait au premier caractère qui ne correspond pas, plutôt que de parcourir tout le processus pour calculer un résultat de comparaison valable (même si on y pense, la comparaison pourrait être claire au même point) - également si cela aurait l'air un peu plus propre car cet appel renvoie un BOOL.

if([elementName isEqualToString:@"companyName"] ) 
  [character setCorporationName:currentElementText]; 
else if([elementName isEqualToString:@"corporationID"] ) 
  [character setCorporationID:currentElementText]; 
else if([elementName isEqualToString:@"name"] ) 

Il existe en fait un moyen assez simple de traiter les instructions if-else en cascade dans un langage comme Objective-C. Oui, vous pouvez utiliser des sous-classes et des substitutions pour créer un groupe de sous-classes qui implémentent la même méthode différemment, en appelant la bonne implémentation lors de l'exécution à l'aide d'un message commun. Cela fonctionne bien si vous souhaitez choisir l’une des quelques implémentations, mais cela peut entraîner une prolifération inutile de sous-classes si vous avez de nombreuses petites implémentations légèrement différentes de celles que vous avez tendance à avoir dans des instructions long if-else ou switch.

Au lieu de cela, factorisez le corps de chaque clause if / else-if dans sa propre méthode, le tout dans la même classe. Nommez les messages qui les invoquent de la même manière. Créez maintenant un tableau NSArray contenant les sélecteurs de ces messages (obtenus avec @selector ()). Utilisez NSSelectorFromString () pour contraindre la chaîne que vous testiez dans les conditionnelles dans un sélecteur (vous devrez peut-être d'abord concaténer des mots ou des points supplémentaires en fonction de la façon dont vous avez nommé ces messages et de leur argument). Maintenant, effectuez vous-même le sélecteur avec performSelector:.

Cette approche présente l'inconvénient de pouvoir encombrer la classe de nombreux nouveaux messages, mais il vaut probablement mieux encombrer une seule classe que la hiérarchie entière de la classe avec de nouvelles sous-classes.

En publiant cela en réponse à la réponse de Wevah ci-dessus - J'aurais édité, mais je n'ai pas encore assez de réputation:

Malheureusement, la première méthode est interrompue pour les champs contenant plus d'un mot - comme xPosition. capitalizedString convertira cela en Xposition, qui, une fois combiné avec le format, vous donne setXposition:. Certainement pas ce qui était recherché ici. Voici ce que j'utilise dans mon code:

NSString *capName = [elementName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[elementName substringToIndex:1] uppercaseString]];
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", capName]);

Pas aussi jolie que la première méthode, mais ça marche.

J'ai mis au point une solution qui utilise des blocs pour créer une structure de type commutateur pour les objets. Ça y est:

BOOL switch_object(id aObject, ...)
{
    va_list args;
    va_start(args, aObject);

    id value = nil;
    BOOL matchFound = NO;

    while ( (value = va_arg(args,id)) )
    {
        void (^block)(void) = va_arg(args,id);
        if ( [aObject isEqual:value] )
        {
            block();
            matchFound = YES;
            break;
        }
    }

    va_end(args);
    return matchFound;
}

Comme vous pouvez le constater, il s’agit d’une fonction C oldschool avec une liste d’arguments variable. Je passe l'objet à tester dans le premier argument, suivi des paires case_value-case_block. (Rappelez-vous que les blocs Objective-C ne sont que des objets.) La boucle while continue à extraire ces paires jusqu'à ce que la valeur de l'objet soit vérifiée ou qu'il ne reste plus de cas (voir notes ci-dessous).

Utilisation:

NSString* str = @"stuff";
switch_object(str,
              @"blah", ^{
                  NSLog(@"blah");
              },
              @"foobar", ^{
                  NSLog(@"foobar");
              },
              @"stuff", ^{
                  NSLog(@"stuff");
              },
              @"poing", ^{
                  NSLog(@"poing");
              },
              nil);   // <-- sentinel

// will print "stuff"

Notes:

  • il s'agit d'une première approximation sans vérification d'erreur
  • le fait que les gestionnaires de cas soient des blocs, nécessite une attention particulière en ce qui concerne la visibilité, la portée et la gestion de la mémoire des variables référencées de l'intérieur
  • si vous oubliez la sentinelle, vous êtes condamné: P
  • vous pouvez utiliser la valeur de retour booléenne pour déclencher un " défaut " cas où aucun cas n'a été mis en correspondance

Le refactoring le plus couramment suggéré pour éliminer les instructions if-else ou switch consiste à introduire le polymorphisme (voir http : //www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html ). L'élimination de telles conditions est primordiale lorsqu'elles sont dupliquées. Dans le cas d'une analyse XML similaire à celle de votre exemple, vous déplacez les données vers une structure plus naturelle, de sorte que vous n'ayez pas à dupliquer le conditionnel ailleurs. Dans ce cas, l'instruction if-else ou switch est probablement suffisante.

Dans ce cas, je ne sais pas si vous pouvez facilement refactoriser la classe pour y introduire le polymorphisme, comme le suggère Bradley, puisqu'il s'agit d'une classe native pour Cocoa. Au lieu de cela, la méthode Objective-C consiste à utiliser une catégorie de classe pour ajouter une méthode elementNameCode à NSSting:

   typedef enum { 
       companyName = 0,
       companyID,  
       ...,
       Unknown
    } ElementCode;

    @interface NSString (ElementNameCodeAdditions)
    - (ElementCode)elementNameCode; 
    @end

    @implementation NSString (ElementNameCodeAdditions)
    - (ElementCode)elementNameCode {
        if([self compare:@"companyName"]==0) {
            return companyName;
        } else if([self compare:@"companyID"]==0) {
            return companyID;
        } ... {

        }

        return Unknown;
    }
    @end

Dans votre code, vous pouvez maintenant utiliser un commutateur sur [NomElément NomCode] (et obtenir les avertissements associés du compilateur si vous oubliez de tester l'un des membres de l'énum, ??etc.).

Comme le souligne Bradley, cela ne vaut peut-être pas la peine si la logique n’est utilisée qu’à un seul endroit.

Ce que nous avons fait dans nos projets là où nous en avons besoin, c'est donc de créer un CFDictionary statique mappant les chaînes / objets à comparer avec une simple valeur entière. Cela conduit à un code qui ressemble à ceci:

static CFDictionaryRef  map = NULL;
int count = 3;
const void *keys[count] = { @"key1", @"key2", @"key3" };
const void *values[count] = { (uintptr_t)1, (uintptr_t)2, (uintptr_t)3 };

if (map == NULL)
    map = CFDictionaryCreate(NULL,keys,values,count,&kCFTypeDictionaryKeyCallBacks,NULL);


switch((uintptr_t)CFDictionaryGetValue(map,[node name]))
{
    case 1:
        // do something
        break;
    case 2:
        // do something else
        break;
    case 3:
        // this other thing too
        break;
}

Si vous ne ciblez que Leopard, vous pouvez utiliser un NSMapTable au lieu d'un CFDictionary.

Semblable à Lvsti, j’utilise des blocs pour effectuer un schéma de commutation sur des objets.

J'ai écrit une chaîne très simple basée sur des blocs de filtres, qui prend n blocs de filtres et effectue chaque filtre sur l'objet.
Chaque filtre peut modifier l'objet, mais doit le retourner. Quoi qu'il arrive.

NSObject + Functional.h

#import <Foundation/Foundation.h>
typedef id(^FilterBlock)(id element, NSUInteger idx, BOOL *stop);

@interface NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks;
@end

NSObject + Functional.m

@implementation NSObject (Functional)
-(id)processByPerformingFilterBlocks:(NSArray *)filterBlocks
{
    __block id blockSelf = self;
    [filterBlocks enumerateObjectsUsingBlock:^( id (^block)(id,NSUInteger idx, BOOL*) , NSUInteger idx, BOOL *stop) {
        blockSelf = block(blockSelf, idx, stop);
    }];

    return blockSelf;
}
@end

Nous pouvons maintenant configurer n FilterBlocks pour tester les différents cas.

FilterBlock caseYES = ^id(id element, NSUInteger idx, BOOL *breakAfter){ 
    if ([element isEqualToString:@"YES"]) { 
        NSLog(@"You did it");  
        *breakAfter = YES;
    } 
    return element;
};

FilterBlock caseNO  = ^id(id element, NSUInteger idx, BOOL *breakAfter){ 
    if ([element isEqualToString:@"NO"] ) { 
        NSLog(@"Nope");
        *breakAfter = YES;
    }
    return element;
};

Maintenant, nous collons les blocs que nous voulons tester en tant que chaîne de filtrage dans un tableau:

NSArray *filters = @[caseYES, caseNO];

et peut l'exécuter sur un objet

id obj1 = @"YES";
id obj2 = @"NO";
[obj1 processByPerformingFilterBlocks:filters];
[obj2 processByPerformingFilterBlocks:filters];

Cette approche peut être utilisée pour la commutation mais également pour n’importe quelle application de chaîne de filtres (conditionnelle), car les blocs peuvent éditer l’élément et le transmettre.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top