Domanda

Sto facendo una programmazione Objective-C che prevede l'analisi di un documento NSXml e il popolamento delle proprietà di un oggetto dal risultato.

La prima versione era simile a questa:

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

Ma non mi piace il modello if-else-if-else che questo produce. Osservando l'istruzione switch vedo che posso gestire solo ints , chars ecc. E non oggetti ... quindi esiste un modello di implementazione migliore Non ne sono a conoscenza?

A proposito, in realtà ho trovato una soluzione migliore per impostare le proprietà dell'oggetto, ma voglio conoscere in modo specifico l'opzione if - else vs modello in Objective-C

È stato utile?

Soluzione

Spero che mi perdonerete tutti per essere usciti di qui, ma vorrei affrontare la questione più generale dell'analisi dei documenti XML in Cocoa senza la necessità di istruzioni if-else. La domanda come inizialmente indicato assegna il testo dell'elemento corrente a una variabile di istanza dell'oggetto carattere. Come sottolineato da jmah, questo può essere risolto utilizzando la codifica del valore-chiave. Tuttavia, in un documento XML più complesso ciò potrebbe non essere possibile. Considera ad esempio quanto segue.

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

Esistono diversi approcci per gestirlo. Dalla parte superiore della mia testa, riesco a pensare a due usando NSXMLDocument. Il primo utilizza NSXMLElement. È abbastanza semplice e non comporta affatto il problema if-else. Ottieni semplicemente l'elemento radice e passa uno alla volta attraverso gli elementi nominati.

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]];

Il prossimo usa il NSXMLNode più generale, cammina attraverso l'albero e usa direttamente la struttura 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]];
            }
        }
    }
}

Questo è un buon candidato per eliminare la struttura if-else, ma come il problema originale, qui non possiamo semplicemente usare la custodia. Tuttavia, possiamo ancora eliminare if-else usando performSelector. Il primo passo è definire il metodo a per ciascun elemento.

- (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 magia si verifica nel metodo invokeMethodForNode: prefix: method. Generiamo il selettore in base al nome dell'elemento ed eseguiamo quel selettore con aNode come unico parametro. Presto bango, abbiamo eliminato la necessità di una dichiarazione if-else. Ecco il codice per quel metodo.

- (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;
}

Ora, invece della nostra più ampia dichiarazione if-else (quella che distingueva tra companyName e corporationID), possiamo semplicemente scrivere una riga di codice

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

Ora mi scuso se ho sbagliato qualcosa, è passato un po 'di tempo da quando ho scritto qualcosa con NSXMLDocument, è notte fonda e in realtà non ho testato questo codice. Quindi, se vedi qualcosa di sbagliato, ti preghiamo di lasciare un commento o modificare questa risposta.

Tuttavia, credo di aver appena mostrato come selettori con nomi propri possano essere usati in Cocoa per eliminare completamente le dichiarazioni if-else in casi come questo. Ci sono alcuni gotcha e custodie angolari. PerformSelector: la famiglia di metodi accetta solo metodi argomento 0, 1 o 2 i cui argomenti e tipi restituiti sono oggetti, quindi se i tipi degli argomenti e il tipo restituito non sono oggetti, o se ci sono più di due argomenti, allora dovresti devi usare un NSInvocation per invocarlo. Devi assicurarti che i nomi dei metodi generati non chiamino altri metodi, specialmente se la destinazione della chiamata è un altro oggetto e questo particolare schema di denominazione dei metodi non funzionerà su elementi con caratteri non alfanumerici. È possibile aggirare ciò sfuggendo in qualche modo ai nomi degli elementi XML nei nomi dei metodi o creando un NSDictionary utilizzando i nomi dei metodi come chiavi e i selettori come valori. Questo può richiedere molta memoria e finire per impiegare più tempo. La spedizione di performSelector come ho descritto è abbastanza veloce. Per istruzioni if-else di grandi dimensioni, questo metodo potrebbe persino essere più veloce di un'istruzione if-else.

Altri suggerimenti

Dovresti trarre vantaggio dalla codifica di valori-chiave:

[character setValue:currentElementText forKey:elementName];

Se i dati non sono attendibili, è possibile verificare che la chiave sia valida:

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

Se vuoi usare il minor codice possibile, e i nomi dei tuoi elementi e setter sono tutti nominati in modo che se elementName è @ " foo " allora setter è setFoo :, potresti fare qualcosa del tipo:

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

[character performSelector:selector withObject:currentElementText];

o forse anche:

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

Sebbene questi saranno ovviamente un po 'più lenti rispetto all'uso di un sacco di istruzioni if.

[Modifica: la seconda opzione è già stata menzionata da qualcuno; oops!]

Osa suggerisco di usare una 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

Un modo in cui l'ho fatto con NSStrings è usando NSDictionary ed enum. Potrebbe non essere il più elegante, ma penso che renda il codice un po 'più leggibile. Il seguente pseudocodice viene estratto da uno dei miei progetti :

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'implementazione if-else che hai è il modo giusto per farlo, poiché switch non funzionerà con gli oggetti. A parte forse essere un po 'più difficile da leggere (che è soggettivo), non c'è un vero svantaggio nell'usare le istruzioni if-else in questo modo.

Anche se non c'è necessariamente un modo migliore per fare qualcosa del genere per un solo utilizzo, perché usare " confronta " quando puoi usare " isEqualToString " ;? Ciò sembrerebbe più performante dal momento che il confronto si fermerebbe al primo carattere non corrispondente, piuttosto che passare attraverso l'intera cosa per calcolare un risultato di confronto valido (anche se arriva a pensarci, il confronto potrebbe essere chiaro allo stesso punto) - anche se sembrerebbe un po 'più pulito perché quella chiamata restituisce un BOOL.

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

In realtà esiste un modo abbastanza semplice per gestire le istruzioni if-else in cascata in un linguaggio come Objective-C. Sì, è possibile utilizzare la sottoclasse e l'override, creando un gruppo di sottoclassi che implementano lo stesso metodo in modo diverso, invocando l'implementazione corretta in fase di runtime utilizzando un messaggio comune. Funziona bene se si desidera scegliere una delle poche implementazioni, ma può provocare un'inutile proliferazione di sottoclassi se si hanno molte implementazioni piccole e leggermente diverse come si tende ad avere in if-else o switch lunghi.

Invece, fattorizza il corpo di ciascuna clausola if / else-if nel suo metodo, tutto nella stessa classe. Nomina i messaggi che li invocano in modo simile. Ora crea un NSArray contenente i selettori di quei messaggi (ottenuti usando @selector ()). Costruisci la stringa che stavi testando nei condizionali in un selettore usando NSSelectorFromString () (potresti dover concatenare prima parole o due punti aggiuntivi a seconda di come hai chiamato quei messaggi e se accettano o meno argomenti). Ora fai eseguire da solo il selettore usando performSelector :.

Questo approccio ha il rovescio della medaglia che può ingombrare la classe con molti nuovi messaggi, ma probabilmente è meglio ingombrare una singola classe rispetto all'intera gerarchia di classi con nuove sottoclassi.

Pubblicando questo come una risposta alla risposta di Wevah sopra - Avrei modificato, ma non ho ancora una reputazione abbastanza alta:

sfortunatamente il primo metodo si rompe per i campi con più di una parola, come xPosition. capitalizedString lo convertirà in Xposition, che quando combinato con il formato ti darà setXposition:. Sicuramente non quello che si voleva qui. Ecco cosa sto usando nel mio codice:

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

Non carino come il primo metodo, ma funziona.

Ho trovato una soluzione che utilizza i blocchi per creare una struttura simile a un interruttore per gli oggetti. Ecco qua:

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;
}

Come puoi vedere, questa è una funzione C oldschool con elenco di argomenti variabili. Passo l'oggetto da testare nel primo argomento, seguito dalle coppie case_value-case_block. (Ricorda che i blocchi Objective-C sono solo oggetti.) Il ciclo while continua a estrarre queste coppie fino a quando il valore dell'oggetto non viene abbinato o non rimangono altri casi (vedi note sotto).

Utilizzo:

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

// will print "stuff"

Note:

  • questa è una prima approssimazione senza alcun controllo degli errori
  • il fatto che i gestori di case siano blocchi, richiede ulteriore attenzione quando si tratta di visibilità, portata e gestione della memoria delle variabili a cui si fa riferimento
  • se dimentichi la sentinella, sei condannato: P
  • puoi utilizzare il valore di ritorno booleano per attivare un " default " caso in cui nessuno dei casi è stato abbinato

Il refactoring più comune suggerito per eliminare le istruzioni if-else o switch è l'introduzione del polimorfismo (vedi http : //www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html ). L'eliminazione di tali condizionali è molto importante quando vengono duplicati. Nel caso di analisi XML come il tuo esempio, stai essenzialmente spostando i dati in una struttura più naturale in modo da non dover duplicare il condizionale altrove. In questo caso l'istruzione if-else o switch è probabilmente abbastanza buona.

In questo caso, non sono sicuro che si possa facilmente riformattare la classe per introdurre il polimorfismo come suggerisce Bradley, poiché è una classe nativa del cacao. Invece, il modo Objective-C per farlo è usare una categoria di classe per aggiungere un metodo elementNameCode a 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

Nel tuo codice, ora puoi usare un interruttore su [elementName elementNameCode] (e ottenere gli avvisi del compilatore associati se dimentichi di provare uno dei membri enum ecc.).

Come sottolinea Bradley, questo potrebbe non valerne la pena se la logica viene utilizzata in un solo posto.

Ciò che abbiamo fatto nei nostri progetti in cui dobbiamo fare questo genere di cose ancora e ancora, è impostare un CFDictionary statico che mappi le stringhe / oggetti da confrontare con un semplice valore intero. Porta a un codice simile al seguente:

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;
}

Se stai prendendo di mira solo Leopard, puoi utilizzare una NSMapTable anziché un CFDictionary.

Simile a Lvsti Sto usando i blocchi per eseguire un modello di commutazione sugli oggetti.

Ho scritto una catena basata su un blocco filtro molto semplice, che prende n blocchi di filtro ed esegue ogni filtro sull'oggetto.
Ogni filtro può modificare l'oggetto, ma deve restituirlo. Non importa cosa.

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

Ora possiamo impostare n FilterBlocks per verificare i diversi casi.

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;
};

Ora incolliamo quei blocchi che vogliamo testare come una catena di filtri in un array:

NSArray *filters = @[caseYES, caseNO];

e può eseguirlo su un oggetto

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

Questo approccio può essere utilizzato per la commutazione ma anche per qualsiasi applicazione (condizionale) della catena di filtri, poiché i blocchi possono modificare l'elemento e trasmetterlo.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top