Objective-C переключатель с использованием объектов?

Я занимаюсь программированием Objective-C, который включает в себя анализ NSXmlDocument и заполнение свойств объектов из результата.

Первая версия выглядела так:

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

Но мне не нравится шаблон if-else-if-else. Глядя на инструкцию switch, я вижу, что я могу обрабатывать только теги ints, chars и т.д., А не объекты... так что есть лучший образец реализации, о котором я не знаю?

Кстати, я действительно придумал лучшее решение для установки свойств объекта, но я хочу знать конкретно о шаблоне if - else vs switch в Objective-C

Ответ 1

Надеюсь, вы все простите меня за то, что вышли на конечность здесь, но я хотел бы рассмотреть более общий вопрос об анализе XML-документов в Cocoa без необходимости инструкций if-else. Вопрос, как изначально указано, присваивает текущему элементу текст переменной экземпляра объекта символа. Как отметил jmah, это можно решить, используя кодирование с ключом. Однако в более сложном документе XML это может оказаться невозможным. Рассмотрим, например, следующее.

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

Существует множество подходов к решению этой проблемы. В верхней части головы я могу думать о двух, используя NSXMLDocument. Первый использует NSXMLElement. Это довольно просто и не затрагивает проблему if-else. Вы просто получаете корневой элемент и каждый раз идите через его именованные элементы.

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

Следующий использует более общий NSXMLNode, просматривает дерево и напрямую использует структуру 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]];
            }
        }
    }
}

Это хороший кандидат на устранение структуры if-else, но, как и исходная проблема, мы не можем просто использовать здесь switch-case. Однако мы все же можем устранить if-else с помощью функции performSelector. Первый шаг - определить метод для каждого элемента.

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

Магия происходит в методе invokeMethodForNode: prefix:. Мы создаем селектор, основанный на имени элемента, и выполняем этот селектор с помощью aNode в качестве единственного параметра. Presto bango, мы устранили необходимость в выражении if-else. Здесь код для этого метода.

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

Теперь вместо нашего более крупного оператора if-else (который отличается от companyName и corporationID) мы можем просто написать одну строку кода

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

Теперь я приношу свои извинения, если у меня есть что-то в этом роде, прошло некоторое время с тех пор, как я написал что-нибудь с NSXMLDocument, это было поздно ночью, и я фактически не тестировал этот код. Поэтому, если вы видите что-то не так, оставьте комментарий или отредактируйте этот ответ.

Однако, я считаю, что я только что показал, как правильно названные селектора могут использоваться в Cocoa, чтобы полностью исключить инструкции if-else в таких случаях. Есть несколько чешских и угловых случаев. Семейство методов performSelector: принимает только 0, 1 или 2 аргумента, аргументы и типы возвращаемых объектов - это объекты, поэтому, если типы аргументов и тип возвращаемого значения не являются объектами или если существует более двух аргументов, должны использовать NSInvocation для его вызова. Вы должны убедиться, что имена методов, которые вы генерируете, не будут вызывать другие методы, особенно если целью вызова является другой объект, и эта схема именования конкретного метода не будет работать с элементами с не буквенно-цифровыми символами. Вы можете обойти это, как-то избегая имен элементов XML в именах методов или создавая NSDictionary, используя имена методов в качестве ключей и селекторов в качестве значений. Это может привести к интенсивной памяти и в конечном итоге увеличить время. performSelector отправка, как описано выше, довольно быстро. Для очень больших операторов if-else этот метод может быть даже быстрее, чем оператор if-else.

Ответ 2

Вы должны использовать Key-Value Coding:

[character setValue:currentElementText forKey:elementName];

Если данные недоверены, вы можете проверить, что ключ действителен:

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

Ответ 3

Dare Я предлагаю использовать макрос?

#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

Ответ 4

Если вы хотите использовать как можно меньше кода, а ваши имена и сеттеры всех названы так, что если elementName @ "foo", то setter is setFoo:, вы можете сделать что-то вроде:

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

[character performSelector:selector withObject:currentElementText];

или, возможно, даже:

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

Хотя это, конечно, будет немного медленнее, чем использование кучи операторов if.

[Изменить: второй вариант уже упоминался кем-то; упс!]

Ответ 5

Один из способов сделать это с помощью NSStrings - использовать NSDictionary и перечисления. Возможно, он не самый элегантный, но я думаю, что он делает код более читаемым. Следующий псевдокод извлекается из одного из моих проектов:

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

Ответ 6

Реализация if-else у вас есть правильный способ сделать это, так как switch не будет работать с объектами. Помимо того, что, возможно, немного труднее читать (что является субъективным), нет никакого реального недостатка в использовании операторов if-else таким образом.

Ответ 7

Хотя не обязательно лучший способ сделать что-то подобное для использования в одно время, зачем использовать "сравнивать", когда вы можете использовать "isEqualToString"? Казалось бы, это было бы более результативным, поскольку сравнение остановилось бы при первом несогласованном символе, вместо того чтобы проходить через все это, чтобы вычислить достоверный результат сравнения (хотя подумать, что сравнение может быть ясно в той же точке) - также, хотя это выглядело бы немного чище, потому что этот вызов возвращает BOOL.

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

Ответ 8

На самом деле существует довольно простой способ справиться с каскадными инструкциями if-else на языке типа Objective-C. Да, вы можете использовать подклассирование и переопределение, создавая группу подклассов, которые реализуют один и тот же метод по-разному, вызывая правильную реализацию во время выполнения с использованием общего сообщения. Это хорошо работает, если вы хотите выбрать одну из нескольких реализаций, но это может привести к ненужному распространению подклассов, если у вас много небольших, немного разных реализаций, как вы, как правило, в длинных операциях if-else или switch.

Вместо этого вычтите тело каждого предложения if/else-if в свой собственный метод, все в одном классе. Назовите сообщения, вызывающие их аналогичным образом. Теперь создайте NSArray, содержащий селектор этих сообщений (полученный с помощью @selector()). Поверните строку, которую вы тестировали в условных выражениях, в селектор, используя NSSelectorFromString() (вам может понадобиться сначала связать с ним дополнительные слова или двоеточия в зависимости от того, как вы назвали эти сообщения, и не принимают ли они аргументы). Теперь вы можете выполнить селектор с помощью функции performSelector:.

Этот подход имеет недостаток, что он может загромождать класс множеством новых сообщений, но, вероятно, лучше загромождать один класс, чем всю иерархию классов с новыми подклассами.

Ответ 9

Проводя это как ответ на ответ Wevah выше - я бы отредактировал, но у меня пока нет достаточно высокой репутации:

К сожалению, первый метод разбивается на поля с более чем одним словом в них - например, xPosition. capitalizedString преобразует это в Xposition, которое в сочетании с форматом дает setXposition:. Определенно не то, что здесь нужно. Вот что я использую в своем коде:

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

Не так красиво, как первый метод, но он работает.

Ответ 10

Я придумал решение, которое использует блоки для создания подобной коммутатору структуры для объектов. Вот оно:

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

Как вы можете видеть, это функция oldschool C с переменным списком аргументов. Я передаю объект для тестирования в первом аргументе, за которым следуют пары case_value-case_block. (Напомним, что блоки Objective-C - это просто объекты.) Цикл while продолжает извлекать эти пары до тех пор, пока не будет сопоставлено значение объекта или не осталось дел (см. Примечания ниже).

Использование:

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

// will print "stuff"

Примечания:

  • это первое приближение без проверки ошибок
  • тот факт, что обработчики case являются блоками, требует дополнительной осторожности, когда дело касается видимости, объема и управления памятью переменных, на которые ссылаются внутри
  • Если вы забудете часового, вы обречены: P
  • вы можете использовать возвращаемое значение boolean для запуска случая по умолчанию, когда ни один из случаев не был сопоставлен

Ответ 11

Наиболее распространенный рефакторинг, предлагаемый для устранения операторов if-else или switch, вводит полиморфизм (см. http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html). Устранение таких условностей является наиболее важным, когда они дублируются. В случае синтаксического анализа XML, как и ваш образец, вы по существу перемещаете данные в более естественную структуру, чтобы вам не пришлось дублировать условие в другом месте. В этом случае оператор if-else или switch, вероятно, достаточно хорош.

Ответ 12

В этом случае я не уверен, что вы можете легко реорганизовать класс для введения полиморфизма, как предлагает Брэдли, так как это класс Cocoa -native. Вместо этого способ Objective-C сделать это - использовать категорию класса для добавления метода 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

В вашем коде теперь вы можете использовать переключатель [elementName elementNameCode] (и получить связанные предупреждения компилятора, если вы забыли проверить один из членов перечисления и т.д.).

Как указывает Брэдли, это может не стоить того, если логика используется только в одном месте.

Ответ 13

То, что мы делали в наших проектах, где нам нужно так много раз повторяться, состоит в том, чтобы установить статический CFDictionary, сопоставляющий строки/объекты для проверки против простого целочисленного значения. Это приводит к коду, который выглядит следующим образом:

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

Если вы ориентируетесь только на Leopard, вы можете использовать NSMapTable вместо CFDictionary.

Ответ 14

Подобно Lvsti, я использую блоки для выполнения шаблона переключения объектов.

Я написал очень простую цепочку на основе блока фильтров, которая принимает n блоков фильтров и выполняет каждый фильтр на объекте.
Каждый фильтр может изменять объект, но должен возвращать его. Независимо от того, что.

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

Теперь мы можем настроить n FilterBlocks для проверки различных случаев.

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

Теперь мы вставляем те блоки, которые хотим проверить как цепочку фильтров в массиве:

NSArray *filters = @[caseYES, caseNO];

и может выполнять его на объекте

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

Этот подход может быть использован для переключения, но также и для любого (условного) приложения цепочки фильтров, поскольку блоки могут редактировать элемент и передавать его.