Вопрос

Я занимаюсь некоторым программированием на 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 против switch шаблон в Objective-C

Это было полезно?

Решение

Я надеюсь, вы все простите меня за то, что я здесь рискую, но я хотел бы затронуть более общий вопрос о разборе XML-документов в Cocoa без использования операторов if-else .Вопрос, как было указано изначально, присваивает текущий текст элемента переменной экземпляра объекта character.Как указала 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 .Первым шагом является определение метода a для каждого элемента.

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

Другие советы

Вы должны воспользоваться преимуществами кодирования Ключ-значение:

[character setValue:currentElementText forKey:elementName];

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

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

Если вы хотите использовать как можно меньше кода, и все ваши имена элементов и установщики названы так, что если имяэлемЕнта равно @"foo", то установщик равен setFoo:, вы могли бы сделать что-то вроде:

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

[character performSelector:selector withObject:currentElementText];

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

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

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

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

Осмелюсь ли я предложить использовать макрос?

#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

Один из способов, которым я сделал это с помощью 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;
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// will print "stuff"

Примечания:

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

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

В этом случае я не уверен, сможете ли вы легко реорганизовать класс, чтобы ввести полиморфизм, как предлагает Брэдли, поскольку это класс, основанный на Cocoa.Вместо этого способ Objective-C сделать это - использовать категорию класса для добавления elementNameCode способ определения:

   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] (и получите соответствующие предупреждения компилятора, если вы забудете протестировать один из элементов перечисления и т.д.).

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

Что мы делали в наших проектах, где нам нужно повторять подобные вещи снова и снова, так это настраивали статический 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.

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

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

NSObject+Функциональный файл.h

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

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

NSObject+Функциональный.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 Фильтрующие блоки для тестирования в различных случаях.

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top