2008-09-19 9 views
14

Estoy haciendo algo de programación de Objective-C que implica analizar un NSXmlDocument y rellenar las propiedades de un objeto del resultado.Objective-C cambiar utilizando objetos?

La primera versión era la siguiente:

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

Pero no me gusta el patrón if-else-if-else esto produce. En cuanto a la declaración switch veo que solo puedo manejar ints, chars etc. y no objetos ... ¿hay un mejor patrón de implementación del que no tengo conocimiento?

Por cierto que de hecho me llegar a una solución mejor para establecer las propiedades del objeto, pero yo quiero saber específicamente sobre el if - patrón else vs switch en Objective-C

+0

Esto no ayuda en absoluto, pero en VB.Net, puede usar instrucciones de conmutación (seleccionar en vb) en cualquier tipo de valor, y el compilador realizará la conversión al patrón if-elseif-else cuando compile , si no es posible hacerlo a través de un salto directo. Personalmente, creo que todos los idiomas deberían tener esta característica. – Kibbee

+0

Respuesta relacionada: http://stackoverflow.com/questions/8161737/can-objective-c-switch-on-nsstring/10177956#10177956 –

Respuesta

12

espero que todos me perdones por salir a un miembro aquí, pero me gustaría abordar la cuestión más general de análisis de documentos XML en Cocoa sin la necesidad de declaraciones if-else. La pregunta tal como se estableció originalmente asigna el texto del elemento actual a una variable de instancia del objeto de carácter. Como jmah señaló, esto se puede resolver usando codificación de clave-valor. Sin embargo, en un documento XML más complejo esto podría no ser posible. Considera por ejemplo lo siguiente.

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

Hay varios enfoques para hacer frente a esto. Fuera de mi cabeza, puedo pensar en dos usando NSXMLDocument. El primero usa NSXMLElement. Es bastante sencillo y no involucra el problema de if-else en absoluto. Simplemente obtiene el elemento raíz y revisa sus elementos nombrados uno por uno.

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

El siguiente utiliza la NSXMLNode más general, paseos por el árbol, y directamente utiliza la estructura 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 es un buen candidato para la eliminación de la estructura if-else, pero al igual que el problema original, que no puede simplemente usar switch de los casos aquí. Sin embargo, aún podemos eliminar if-else utilizando performSelector. El primer paso es definir el método a 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; 
} 

La magia ocurre en el método invokeMethodForNode: prefix:. Generamos el selector basado en el nombre del elemento y realizamos ese selector con aNode como el único parámetro. Presto bango, hemos eliminado la necesidad de una declaración if-else. Aquí está el código para ese 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; 
} 

Ahora, en vez de nuestra mayor instrucción if-else (el que diferenció entre companyName y corporationID), podemos escribir simplemente una línea de código

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

Ahora me disculpo si tengo cualquier de este error, ha pasado un tiempo desde que escribí algo con NSXMLDocument, es tarde por la noche y en realidad no probé este código. Entonces, si ve algo incorrecto, por favor deje un comentario o edite esta respuesta.

Sin embargo, creo que acabo de mostrar cómo los selectores con nombre propio se pueden utilizar en Cocoa para eliminar completamente las declaraciones if-else en casos como este. Hay algunos errores y casos de esquina. El selector de rendimiento: familia de métodos solo toma 0, 1 o 2 métodos de argumento cuyos argumentos y tipos de retorno son objetos, por lo que si los tipos de los argumentos y el tipo de retorno no son objetos, o si hay más de dos argumentos, entonces usted tiene que usar una Invocación NS para invocarlo. Debe asegurarse de que los nombres de método que genere no invocarán otros métodos, especialmente si el destino de la llamada es otro objeto, y este esquema de nombres de método particular no funcionará en elementos con caracteres no alfanuméricos. Podría solucionarlo escapando de alguna manera de los nombres de los elementos XML, o construyendo un NSDictionary usando los nombres de los métodos como las claves y los selectores como valores. Esto puede consumir mucha memoria y terminar tardando más tiempo. El envío de performSelector como lo describí es bastante rápido. Para sentencias if-else muy grandes, este método puede ser incluso más rápido que una instrucción if-else.

+0

Me gusta la compacidad de esta solución. Solo un par de comentarios: es NSXMLNode no NSNode. NSXMLNode * ret - No puedo pensar en ningún uso para eso ya que aNoNo ya está configurado por nextNode. El uso de performSelector como este dará la advertencia: "performSelector puede causar una fuga porque su selector es desconocido". La forma más rápida de evitar esto es usar objc_msgSend, pero luego debe desactivar "Habilitar la comprobación estricta de objc_msgEnviar llamadas". Envuelva esto en [self respondsToSelector: selector] para aliviar algunas de las deficiencias de objc_msgSend. –

+0

Oye, gracias por el comentario. Esta es una wiki de la comunidad, así que no dude en editar los errores que pueda encontrar. Sin embargo, me gustaría señalar que esta respuesta fue más un ejercicio de exploración de una implementación con una limitación arbitraria, y no pretende ser un consejo sobre cómo escribir código de producción. –

2

La refactorización más común sugiere para eliminar si -las declaraciones de cambio o cambio están introduciendo polimorfismo (ver http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html). La eliminación de dichos condicionales es más importante cuando están duplicados. En el caso del análisis XML como su muestra, básicamente está moviendo los datos a una estructura más natural para que no tenga que duplicar el condicional en otro lugar. En este caso, la instrucción if-else o switch es probablemente lo suficientemente buena.

3

La implementación if-else que tiene es la forma correcta de hacerlo, ya que switch no funcionará con objetos. Aparte de ser un poco más difícil de leer (lo cual es subjetivo), no hay una desventaja real al usar las declaraciones if-else de esta manera.

1

En este caso, no estoy seguro si puede refactorizar fácilmente la clase para introducir polimorfismo como sugiere Bradley, ya que es una clase nativa de Cocoa. En cambio, la forma en Objective-C para hacerlo es utilizar una categoría de clase para agregar un método 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 

En su código, ahora podría usar un interruptor en [elementName elementNameCode] (y ganar las advertencias del compilador asociados si te olvidas de hacer la prueba para uno de los miembros enum, etc.).

Como señala Bradley, puede que no valga la pena si la lógica solo se utiliza en un solo lugar.

3

Aunque no hay necesariamente una mejor manera de hacer algo así para un uso único, ¿por qué usar "comparar" cuando puede usar "isEqualToString"? Eso parecería ser más eficaz ya que la comparación se detendría en el primer carácter no coincidente, en lugar de pasar por todo para calcular un resultado de comparación válido (aunque pensándolo bien, la comparación podría ser clara en el mismo punto) - aunque se vería un poco más limpio porque esa llamada devuelve un BOOL.

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

Como indiqué en la pregunta original, ya encontré una forma mejor de hacer el ajuste de propiedades . Solo estaba buscando consejos sobre cualquier patrón mejor para lidiar con el patrón if-else. – craigb

+0

Bonito voto no deseado. Todavía es información útil para las personas que prefieren formas más limpias de comparar cadenas. Estás abusando de votos a la baja, el único consuelo es que le está costando a tu reputación descartar información perfectamente buena. No cometeré el error de ayudarte de nuevo. –

13

Usted debe tomar ventaja de valor-clave Codificación:

[character setValue:currentElementText forKey:elementName]; 

Si los datos no es de confianza, es posible que desee comprobar que la clave es válida:

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

Como mencioné en la pregunta, ya encontré una forma mejor de establecer estas propiedades y solo estaba buscando consejos sobre el patrón if-else que no me gustó. – craigb

+0

Pero ese es el punto, para evitar hacer el despacho múltiple usted mismo, y dejar que los aspectos del lenguaje (o marco) lo manejen.Se debe desalentar el patrón if-else-else-else o switch en el valor del objeto, cuando puede hacer cosas como búsquedas de diccionario. – jmah

4

Una forma He hecho esto con NSStrings usando NSDictionary y enums. Puede que no sea el más elegante, pero creo que hace que el código sea un poco más legible.El siguiente pseudocódigo se extrae de one of my projects:

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

¿Te atreves sugiero usar 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 
+0

Bien, no había pensado en eso. No estoy acostumbrado a usar macros mucho. – craigb

7

Si desea usar la menor cantidad de código posible, y sus nombres de elementos y definidores tienen nombres para que, si es elementName @ "foo", entonces colocador es setFoo :, significa que podría hacer algo como:

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

[character performSelector:selector withObject:currentElementText]; 

o posiblemente incluso:

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

aunque éstos, por supuesto, ser un poco más lento que usar un montón de sentencias if.

[Editar: La segunda opción ya fue mencionada por alguien; ¡Uy!]

+0

+1 porque es bastante ordenado ya que ObjC puede ser ... – epatel

+0

Agregué una corrección a esto a continuación - http://stackoverflow.com/questions/104339/objective-c-switch-using-objects/1215783#1215783 –

3

En realidad, hay una manera bastante simple de tratar con sentencias if-else en cascada en un lenguaje como Objective-C. Sí, puede usar la creación de subclases y la anulación, creando un grupo de subclases que implementan el mismo método de manera diferente, invocando la implementación correcta en tiempo de ejecución usando un mensaje común. Esto funciona bien si desea elegir una de las pocas implementaciones, pero puede generar una proliferación innecesaria de subclases si tiene muchas implementaciones pequeñas y ligeramente diferentes, como las que suele tener en sentencias largas if-else o switch.

En su lugar, factorizar el cuerpo de cada cláusula if/else-if en su propio método, todos en la misma clase. Nombra los mensajes que los invocan de forma similar. Ahora crea un NSArray que contenga los selectores de esos mensajes (obtenidos usando @selector()). Fuerce la cadena que estaba probando en los condicionales en un selector usando NSSelectorFromString() (puede que necesite concatenar palabras adicionales o dos puntos en primer lugar dependiendo de cómo haya nombrado esos mensajes, y de si toman argumentos o no). Ahora haz auto-ejecutar el selector usando performSelector :.

Este enfoque tiene la desventaja de que puede aglomerar la clase con muchos mensajes nuevos, pero es probable que sea mejor agrupar una única clase que toda la jerarquía de clases con nuevas subclases.

1

Lo que hemos hecho en nuestros proyectos donde necesitamos para este tipo de cosas una y otra vez, es establecer un mapeo CFDictionary estático de las cadenas/objetos para comparar con un valor entero simple. Conduce a código que se parece a esto:

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 usted está apuntando solamente leopardo, se puede usar un NSMapTable en lugar de un CFDictionary.

3

publicar esto como una respuesta a la respuesta de Wevah anterior - Me he editado, pero no tienen buena reputación suficiente todavía:

lamentablemente las primeras rupturas método para campos con más de una palabra en ellas - como xPosition. capitalizedString lo convertirá en Xposition, que cuando se combina con el formato te da setXposition:. Definitivamente no es lo que se quería aquí. Esto es lo que estoy usando en mi código:

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

No es tan bonito como el primer método, pero funciona.

+0

Gracias por la captura! – Wevah

3

He encontrado una solución que usa bloques para crear una estructura tipo interruptor para los objetos. Ahí va:

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 puede ver, esta es una función C de la escuela antigua con lista de argumentos variables. Paso el objeto a probar en el primer argumento, seguido de los pares case_value-case_block.(Recuerde que los bloques Objective-C son solo objetos). El ciclo while sigue extrayendo estos pares hasta que el valor del objeto coincida o no queden casos (consulte las notas a continuación).

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 es una primera aproximación sin ninguna comprobación de errores
  • el hecho de que los encargados del caso son bloques, requiere un cuidado adicional cuando se trata de la visibilidad , alcance y gestión de la memoria de las variables a las que se hace referencia desde
  • si olvida el centinela, está condenado: P
  • puede utilizar el valor de retorno booleano para desencadenar un caso "default" cuando ninguno de los casos se han emparejado
+0

interesante, me gusta :-) – craigb

+0

He puesto la extracción del bloque de la caja antes de la comprobación 'isEqual:'; de lo contrario, el objeto se verifica innecesariamente para determinar la igualdad con el objeto del bloque. – Lvsti

+0

@Lvsti, +1 Me gusta este enfoque de bloque. Tengo curiosidad, lo que piensas sobre mi enfoque de cadena de filtro basado en bloques que acabo de publicar como respuesta. – vikingosegundo

0

similares a Lvsti estoy usando bloques para llevar a cabo un patrón de conmutación en los objetos.

Escribí una cadena basada en bloques de filtro muy simple, que toma n bloques de filtro y realiza cada filtro en el objeto.
Cada filtro puede alterar el objeto, pero debe devolverlo. No importa qué.

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 

Ahora podemos configurar n FilterBlocks para probar los 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; 
}; 

Ahora nos ceñimos aquellos bloque queremos probar como una cadena de filtros en una matriz:

NSArray *filters = @[caseYES, caseNO]; 

y puede realizar en un objeto

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

Este enfoque se puede utilizar para cambiar pero también para cualquier aplicación de cadena de filtro (condicional), ya que los bloques pueden editar el elemento y pasarlo.

Cuestiones relacionadas