Pergunta

Eu estou fazendo alguma programação Objective-C que envolve analisar um NSXmlDocument e preencher um propriedades de objetos a partir do resultado.

A primeira versão ficou assim:

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

Mas eu não gosto do padrão if-else-if-else isso produz. Olhando para a declaração switch vejo que só pode lidar com ints, chars etc e não objetos ... assim há um padrão melhor implementação Eu não estou ciente de?

BTW eu fiz realmente chegar a uma solução melhor para definir as propriedades do objeto, mas eu quero saber especificamente sobre o if-else vs padrão switch em Objective-C

Foi útil?

Solução

Espero que tudo que você me perdoe por sair em um membro aqui, mas eu gostaria de abordar a questão mais geral de análise de documentos XML em Cocoa, sem a necessidade de declarações if-else. A questão de cessionários inicialmente previsto a corrente elemento de texto para uma variável de instância do objeto personagem. Como JMAH apontou, isso pode ser resolvido usando codificação de valores-chave. No entanto, em um documento XML mais complexa isso pode não ser possível. Considere por exemplo a seguir.

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

Existem várias abordagens para lidar com isso. Fora do topo da minha cabeça, eu posso pensar em dois usando NSXMLDocument. O primeiro usa NSXMLElement. É bastante simples e não envolve a questão if-else em tudo. Você simplesmente obter o elemento raiz e ir através de seus elementos nomeados um por um.

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

O próximo usa o NSXMLNode mais geral, percorre a árvore, e directamente usa a estrutura 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]];
            }
        }
    }
}

Este é um bom candidato para eliminar a estrutura if-else, mas como o problema original, não podemos simplesmente usar switch-case aqui. No entanto, ainda pode eliminar if-else usando performSelector. O primeiro passo consiste em definir a um método para cada 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;
}

A magia acontece no invokeMethodForNode: prefixo: método. Geramos o seletor com base no nome do elemento, e realizar que seletor com ânodo como o único parâmetro. Presto Bango, nós eliminamos a necessidade de uma declaração if-else. Aqui está o código para esse método.

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

Agora, em vez do nosso maior se-else (aquele que diferenciou entre companyName e corporationID), podemos simplesmente escrever uma linha de código

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

Agora, eu peço desculpas se eu tenho nada disso errado, tem sido um tempo desde que eu escrevi qualquer coisa com NSXMLDocument, é tarde da noite e eu realmente não testar esse código. Então, se você vê algo errado, por favor deixe um comentário ou editar esta resposta.

No entanto, creio ter mostrado apenas como seletores devidamente nomeados pode ser usado em Cocoa para eliminar completamente if-else em casos como este. Existem algumas armadilhas e casos de canto. O performSelector: Família de métodos leva apenas 0, 1, ou 2 métodos argumento cujos argumentos e tipos de retorno são objetos, por isso, se os tipos de argumentos e tipo de retorno não são objetos, ou se há mais de dois argumentos, então você faria tem que usar um NSInvocation para invocá-lo. Você tem que se certificar de que os nomes dos métodos que você gerar não vão chamar outros métodos, especialmente se o alvo da chamada é outro objeto, e este esquema método de nomeação em particular não irá funcionar em elementos com caracteres não alfanuméricos. Você pode contornar isso por escapar os nomes dos elementos XML em seus nomes de métodos de alguma forma, ou através da construção de um NSDictionary usando os nomes de métodos como as chaves e os seletores como os valores. Isto pode começar a memória muito intensivo e acabam tendo um tempo mais longo. performSelector expedição como eu descrevi é bastante rápido. Para declarações muito grandes if-else, este método pode até ser mais rápido do que uma declaração if-else.

Outras dicas

Você deve aproveitar Key-Value Codificação:

[character setValue:currentElementText forKey:elementName];

Se os dados não é confiável, você pode querer verificar que a chave é válido:

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

Se você quiser usar o mínimo de código possível, e seus nomes de elementos e setters são todos nomeados de modo que se elementName é @ "foo", em seguida, setter é setFoo :, você poderia fazer algo como:

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

[character performSelector:selector withObject:currentElementText];

ou possivelmente até mesmo:

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

Embora estes será, naturalmente, um pouco mais lento do que usar um monte de se declarações.

[Edit: A segunda opção já foi mencionado por alguém; oops!]

Atrevo-me a sugerir o uso de uma 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

Uma maneira que eu fiz isso com NSStrings é usando um NSDictionary e enums. Pode não ser o mais elegante, mas eu acho que torna o código um pouco mais legível. O seguinte pseudocódigo é extraído um dos meus projetos :

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

A implementação if-else que você tem é o caminho certo para fazer isso, pois switch não funcionará com os objetos. Além de talvez ser um pouco mais difícil de ler (o que é subjetivo), não há nenhuma desvantagem real no uso de declarações if-else desta forma.

Embora não é necessariamente a melhor maneira de fazer algo assim para um uso do tempo, por que usar "comparar" quando você pode usar "isEqualToString"? Que parecem ser mais performance desde a comparação pararia no primeiro caractere não-correspondência, ao invés de passar a coisa toda para calcular um resultado comparação válida (embora chegou a pensar que a comparação pode ser claro no mesmo ponto) -. também que ele ficaria um pouco mais limpo, porque essa chamada retorna um BOOL

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

Há realmente uma maneira bastante simples para lidar com cascata declarações if-else em uma linguagem como Objective-C. Sim, você pode usar subclassificação e substituindo, criando um grupo de subclasses que implementam o mesmo método diferente, invocando a correcta aplicação em tempo de execução através de uma mensagem comum. Isso funciona bem se você deseja escolher uma das poucas implementações, mas pode resultar em uma proliferação desnecessária de subclasses se você tem muitos pequenos, implementações ligeiramente diferentes, como você tende a ter no longo if-else declarações ou switch.

Em vez disso, o fator do corpo de cada cláusula if / else-se em seu próprio método, todos na mesma classe. Nomeie as mensagens que eles invocam em uma forma similar. Agora criar um NSArray contendo os seletores dessas mensagens (obtido usando @selector ()). Coagir a cadeia que estavam testando nos condicionais em um seletor usando NSSelectorFromString () (pode ser necessário para concatenar palavras ou dois pontos adicionais a ele primeiro, dependendo de como você nomeou essas mensagens, e se ou não eles tomam argumentos). Agora tem auto executar o seletor usando performSelector:.

Esta abordagem tem a desvantagem de que ele pode encher-se a classe com muitas mensagens novas, mas é provavelmente melhor para a desordem-up uma única classe que toda a hierarquia de classes com novas subclasses.

postando isso como uma resposta a resposta de Wevah acima - eu teria editado, mas eu não tenho reputação suficiente alta ainda:

Infelizmente, as primeiras quebras de método para campos com mais de uma palavra em si - como xPosition. capitalizedString irá convertê-lo em Xposition, que quando combinado com o formato dará setXPosition:. Definitivamente não é o que se queria aqui. Aqui está o que eu estou usando no meu código:

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

Não tão bonita como o primeiro método, mas funciona.

Eu vim com uma solução que usa blocos para criar um switch-como a estrutura para objetos. Lá vai:

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

Como você pode ver, esta é uma função oldschool C com lista de argumentos variável. I passar o objecto a ser testado, em primeiro argumento, seguido pelos pares case_value-case_block. (Lembre-se que bloqueia Objective-C são apenas objetos.) O loop while mantém extrair esses pares até que o valor do objeto é correspondido ou não há casos restantes (veja notas abaixo).

Uso:

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

// will print "stuff"

Notas:

  • esta é uma primeira aproximação sem qualquer verificação de erros
  • o fato de que os responsáveis ??pelos casos são blocos, requer cuidados adicionais quando se trata de visibilidade, escopo e gerenciamento de memória de variáveis ??referenciadas a partir de dentro
  • se você esquecer a sentinela, você está condenado: P
  • você pode usar o valor de retorno booleano para desencadear um caso "default" quando nenhum dos casos foram acompanhados

A refatoração mais comum sugerido para eliminar declarações if-else ou interruptor é a introdução de polimorfismo (ver http : //www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html ). Eliminando esses condicionais é mais importante quando eles são duplicados. No caso da análise XML como sua amostra que são essencialmente mover os dados para uma estrutura mais natural para que você não terá que duplicar a outro lugar condicional. Neste caso, a declaração if-else ou switch é provavelmente bom o suficiente.

Neste caso, eu não tenho certeza se você pode facilmente refazer a classe para apresentar polimorfismo como Bradley sugere, uma vez que é uma classe de cacau nativo. Em vez disso, a forma como Objective-C para fazer isso é usar uma categoria de classe para adicionar um método elementNameCode para 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

Em seu código, agora você pode usar um interruptor na [elementName elementNameCode] (e ganhar os avisos do compilador associados se você esquecer de teste para um dos membros enum etc.).

Como Bradley aponta, isso pode não valer a pena se a lógica é usada apenas em um lugar.

O que fizemos nos nossos projectos em que temos de modo que este tipo de coisa uma e outra, é a criação de uma estática CFDictionary mapeamento das cordas / objetos para verificar contra a um valor inteiro simples. Isso leva a código que se parece com isso:

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 você está visando apenas Leopard, você poderia usar um NSMapTable em vez de um CFDictionary.

Semelhante ao Lvsti Eu estou usando blocos para executar um padrão de comutação em objetos.

Eu escrevi uma cadeia baseada em blocos de filtro muito simples, que leva n blocos de filtros e executa cada filtro no objeto.
Cada filtro pode alterar o objeto, mas deve devolvê-lo. Não importa o que.

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

Agora podemos configurar FilterBlocks n para testar os diferentes casos.

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

Agora que cumpri os blocos queremos teste como uma cadeia de filtros em uma matriz:

NSArray *filters = @[caseYES, caseNO];

e pode executar em um objeto

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

Esta abordagem pode ser utilizada para a ligação, mas também para qualquer aplicação cadeia filtro (condicional), como os blocos pode editar o elemento e transmiti-lo.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top