Переключатель Objective-C с использованием объектов? - PullRequest
14 голосов
/ 19 сентября 2008

Я занимаюсь программированием на 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

Ответы [ 14 ]

13 голосов
/ 21 сентября 2008

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

[character setValue:currentElementText forKey:elementName];

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

if (![validKeysCollection containsObject:elementName])
    // Exception or error
12 голосов
/ 26 сентября 2008

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

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

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

7 голосов
/ 27 сентября 2008

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

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

[character performSelector:selector withObject:currentElementText];

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

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

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

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

7 голосов
/ 26 сентября 2008

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

#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 голосов
/ 23 сентября 2008

Один из способов сделать это с 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;
}
3 голосов
/ 12 апреля 2012

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

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"

Примечания:

  • это первое приближение без проверки ошибок
  • Тот факт, что обработчики падежей являются блоками, требует дополнительного внимания, когда дело доходит до видимости, области видимости и управления памятью переменных, на которые есть ссылки из
  • если вы забудете стража, вы обречены: P
  • вы можете использовать логическое возвращаемое значение для запуска случая «по умолчанию», когда ни один из случаев не был найден
3 голосов
/ 01 августа 2009

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

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

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

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

3 голосов
/ 27 сентября 2008

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

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

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

3 голосов
/ 21 сентября 2008

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

if([elementName isEqualToString:@"companyName"] ) 
  [character setCorporationName:currentElementText]; 
else if([elementName isEqualToString:@"corporationID"] ) 
  [character setCorporationID:currentElementText]; 
else if([elementName isEqualToString:@"name"] ) 
3 голосов
/ 19 сентября 2008

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

...