Я занимаюсь программированием на 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
Связанный ответ: stackoverflow.com/questions/8161737/…





Наиболее распространенный рефакторинг, предлагаемый для исключения операторов if-else или switch, - это введение полиморфизма (см. http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html). Устранение таких условных выражений наиболее важно при их дублировании. В случае синтаксического анализа XML, подобного вашему образцу, вы, по сути, перемещаете данные в более естественную структуру, чтобы вам не пришлось дублировать условное выражение в другом месте. В этом случае, вероятно, будет достаточно оператора if-else или switch.
Имеющаяся у вас реализация if-else - правильный способ сделать это, поскольку switch не будет работать с объектами. Помимо того, что, возможно, его труднее читать (что субъективно), нет никаких реальных недостатков в использовании операторов if-else таким образом.
В этом случае я не уверен, что вы можете легко реорганизовать класс, чтобы ввести полиморфизм, как предлагает Брэдли, поскольку это родной для Какао класс. Вместо этого в 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] (и получать соответствующие предупреждения компилятора, если вы забыли проверить один из членов перечисления и т. д.).
Как указывает Брэдли, это может не стоить того, если логика используется только в одном месте.
Хотя не обязательно лучший способ сделать что-то подобное для одноразового использования, зачем использовать «сравнение», если можно использовать «isEqualToString»? Это могло бы показаться более эффективным, поскольку сравнение остановилось бы на первом несоответствующем символе, вместо того, чтобы проходить все это, чтобы вычислить действительный результат сравнения (хотя, если подумать, сравнение может быть ясным в той же точке) - хотя это выглядело бы немного чище, потому что этот вызов возвращает BOOL.
if ([elementName isEqualToString:@"companyName"] )
[character setCorporationName:currentElementText];
else if ([elementName isEqualToString:@"corporationID"] )
[character setCorporationID:currentElementText];
else if ([elementName isEqualToString:@"name"] )
Как указано в исходном вопросе, я уже нашел лучший способ настроить свойства. Я просто искал совета по поводу лучшего шаблона для работы с шаблоном if-else.
Хороший голос против. Это все еще полезная информация для людей, которые предпочитают более чистые способы сравнения строк. Вы злоупотребляете отрицательными голосами, единственное утешение в том, что отбрасывание совершенно достоверной информации стоит вашей репутации. Я не совершу ошибки, снова помогая тебе.
Вам следует воспользоваться преимуществами кодирования «ключ-значение»:
[character setValue:currentElementText forKey:elementName];
Если данные не являются надежными, вы можете проверить правильность ключа:
if (![validKeysCollection containsObject:elementName])
// Exception or error
Как указано в вопросе, я уже нашел лучший способ установки этих свойств и просто искал совет по шаблону if-else, который мне не нравился.
Но в том-то и дело, чтобы избежать выполнения множественной отправки самостоятельно и позволить аспектам языка (или фреймворка) справиться с этим. Не рекомендуется использовать шаблон if-else-else-else или переключатель для значения объекта, если вы можете делать такие вещи, как поиск по словарю.
Один из способов сделать это с помощью 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;
}
Я надеюсь, что вы все простите меня за то, что я здесь рискую, но я хотел бы обратиться к более общему вопросу синтаксического анализа XML-документов в Какао без необходимости использования операторов if-else. Первоначально указанный вопрос назначает текущий текст элемента переменной экземпляра символьного объекта. Как указал jmah, это можно решить с помощью кодирования значения ключа. Однако в более сложном XML-документе это может быть невозможно. Рассмотрим, например, следующее.
<xmlroot>
<corporationID>
<stockSymbol>EXAM</stockSymbol>
<uuid>31337</uuid>
</corporationID>
<companyName>Example Inc.</companyName>
</xmlroot>
Есть несколько подходов к этому. Внезапно я могу придумать два, использующих NSXMLDocument. Первый использует NSXMLElement. Это довольно просто и вообще не связано с проблемой «если-еще». Вы просто получаете корневой элемент и просматриваете его именованные элементы один за другим.
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 (того, который различает имя компании и идентификатор корпорации), мы можем просто написать одну строку кода
NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
aNode = [self invokeMethodForNode:aNode prefix:@"parse_"];
}
Приношу свои извинения, если я ошибся, прошло много времени с тех пор, как я писал что-либо с NSXMLDocument, уже поздно ночью, и я фактически не тестировал этот код. Так что, если вы видите что-то не так, оставьте комментарий или отредактируйте этот ответ.
Однако я считаю, что только что показал, как правильно названные селекторы могут использоваться в Какао для полного исключения операторов if-else в подобных случаях. Есть несколько подводных камней и угловых случаев. Семейство методов performSelector: принимает только методы с 0, 1 или 2 аргументами, чьи аргументы и возвращаемые типы являются объектами, поэтому, если типы аргументов и возвращаемый тип не являются объектами или если имеется более двух аргументов, вы должны должны использовать NSInvocation для его вызова. Вы должны убедиться, что генерируемые вами имена методов не будут вызывать другие методы, особенно если целью вызова является другой объект, и эта конкретная схема именования методов не будет работать с элементами с не буквенно-цифровыми символами. Вы можете обойти это, экранируя имена элементов XML в именах ваших методов каким-то образом или создав NSDictionary, используя имена методов в качестве ключей и селекторы в качестве значений. Это может потребовать значительных затрат памяти и в конечном итоге занять больше времени. Отправка performSelector, как я описал, выполняется довольно быстро. Для очень больших операторов if-else этот метод может быть даже быстрее, чем оператор if-else.
Мне очень нравится компактность этого решения. Пара замечаний: это NSXMLNode, а не NSNode. NSXMLNode * ret - я не могу придумать для этого никакого применения, поскольку aNode уже установлен nextNode. При таком использовании performSelector будет выдано предупреждение: «performSelector может вызвать утечку, потому что его селектор неизвестен». Самый простой способ обойти это - использовать objc_msgSend, но тогда вам нужно отключить «Включить строгую проверку вызовов objc_msgSend». Оберните это в [self responseToSelector: selector], чтобы устранить некоторые недостатки objc_msgSend.
Привет, спасибо за комментарий. Этот ответ - вики сообщества, поэтому, пожалуйста, не стесняйтесь редактировать любые ошибки, которые вы можете найти. Однако я хотел бы отметить, что этот ответ был скорее упражнением по изучению реализации с произвольным ограничением, а не советом о том, как на самом деле писать производственный код.
Осмелюсь ли я предложить использовать макрос?
#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
Хорошо, не думал об этом. Не привык много использовать макросы.
Если вы хотите использовать как можно меньше кода, и все ваши имена элементов и сеттеры названы так, что если elementName - это @ "foo", то сеттер - это setFoo :, вы можете сделать что-то вроде:
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", [elementName capitalizedString]]);
[character performSelector:selector withObject:currentElementText];
или, возможно, даже:
[character setValue:currentElementText forKey:elementName]; // KVC-style
Хотя это, конечно, будет немного медленнее, чем использование нескольких операторов if.
[Edit: второй вариант уже был кем-то упомянут; упс!]
Я добавил исправление к этому ниже - stackoverflow.com/questions/104339/…
На самом деле существует довольно простой способ справиться с каскадными операторами if-else на языке вроде Objective-C. Да, вы можете использовать подклассы и переопределение, создавая группу подклассов, реализующих один и тот же метод по-разному, вызывая правильную реализацию во время выполнения с помощью общего сообщения. Это хорошо работает, если вы хотите выбрать одну из нескольких реализаций, но это может привести к ненужному увеличению подклассов, если у вас есть много небольших, немного разных реализаций, таких как у вас, как правило, есть длинные операторы if-else или switch.
Вместо этого выделите тело каждого предложения if / else-if в отдельный метод, все в одном классе. Назовите сообщения, которые их вызывают, аналогичным образом. Теперь создайте NSArray, содержащий селекторы этих сообщений (полученных с помощью @selector ()). Приведите строку, которую вы тестировали в условных выражениях, в селектор с помощью NSSelectorFromString () (вам может потребоваться сначала объединить с ней дополнительные слова или двоеточия, в зависимости от того, как вы назвали эти сообщения, и принимают ли они аргументы). Теперь нужно самостоятельно выполнить селектор с помощью performSelector :.
У этого подхода есть обратная сторона: он может загромождать класс множеством новых сообщений, но, вероятно, лучше загромождать один класс, чем всю иерархию классов новыми подклассами.
То, что мы сделали в наших проектах, где нам нужно повторять подобные вещи снова и снова, - это настроить статический 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.
Публикуя это как ответ на ответ 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 - это просто объекты.) Цикл 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 до проверки isEqual:, иначе объект будет без необходимости проверяться на равенство с объектом блока.
@Lvsti, +1 Мне нравится этот блочный подход. Мне любопытно, что вы думаете о моем подходе к цепочке фильтров на основе блоков, который я только что опубликовал в качестве ответа.
Как и в случае с 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];
Этот подход можно использовать для переключения, но также и для любого (условного) приложения цепочки фильтров, поскольку блоки могут редактировать элемент и передавать его.
Это совсем не помогает, но в VB.Net вы можете использовать операторы switch (select in vb) для любого типа значения, и компилятор выполнит преобразование в шаблон if-elseif-else при компиляции, если он невозможно сделать с помощью прямого прыжка. Лично я считаю, что эта функция должна быть на всех языках.