Переключатель 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

Это совсем не помогает, но в VB.Net вы можете использовать операторы switch (select in vb) для любого типа значения, и компилятор выполнит преобразование в шаблон if-elseif-else при компиляции, если он невозможно сделать с помощью прямого прыжка. Лично я считаю, что эта функция должна быть на всех языках.

Kibbee 10.11.2008 17:04

Связанный ответ: stackoverflow.com/questions/8161737/…

Graham Perks 03.01.2013 20:22
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
14
2
30 862
14
Перейти к ответу Данный вопрос помечен как решенный

Ответы 14

Наиболее распространенный рефакторинг, предлагаемый для исключения операторов 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.

craigb 21.09.2008 23:10

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

Kendall Helmstetter Gelner 22.09.2008 00:44

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

[character setValue:currentElementText forKey:elementName];

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

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

Как указано в вопросе, я уже нашел лучший способ установки этих свойств и просто искал совет по шаблону if-else, который мне не нравился.

craigb 21.09.2008 23:11

Но в том-то и дело, чтобы избежать выполнения множественной отправки самостоятельно и позволить аспектам языка (или фреймворка) справиться с этим. Не рекомендуется использовать шаблон if-else-else-else или переключатель для значения объекта, если вы можете делать такие вещи, как поиск по словарю.

jmah 22.09.2008 07:23

Один из способов сделать это с помощью 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.

Elise van Looij 06.06.2016 20:18

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

Michael Buckley 27.06.2016 18:59

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

#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

Хорошо, не думал об этом. Не привык много использовать макросы.

craigb 27.09.2008 04:46

Если вы хотите использовать как можно меньше кода, и все ваши имена элементов и сеттеры названы так, что если 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/…

Dennis Munsie 01.08.2009 07:48

На самом деле существует довольно простой способ справиться с каскадными операторами 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"

Примечания:

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

Я поставил извлечение блока case до проверки isEqual:, иначе объект будет без необходимости проверяться на равенство с объектом блока.

Lvsti 13.04.2012 12:50

@Lvsti, +1 Мне нравится этот блочный подход. Мне любопытно, что вы думаете о моем подходе к цепочке фильтров на основе блоков, который я только что опубликовал в качестве ответа.

vikingosegundo 09.02.2013 19:21

Как и в случае с 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];

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

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