【问题标题】:Objective-C switch using objects?Objective-C 使用对象切换?
【发布时间】:2008-09-19 18:29:45
【问题描述】:

我正在做一些涉及解析 NSXmlDocument 并从结果中填充对象属性的 Objective-C 编程。

第一个版本如下所示:

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 语句,我发现我只能处理intschars 等而不是对象......那么有没有更好的实现模式我不知道?

顺便说一句,我确实想出了一个更好的解决方案来设置对象的属性,但我想具体了解 Objective-C 中的 if-elseswitch 模式

【问题讨论】:

  • 这根本没有帮助,但是在 VB.Net 中,您可以对任何类型的值使用 switch(在 vb 中选择)语句,并且编译器将转换为 if-elseif- else 编译时的模式,如果不能通过直接跳转来完成。个人觉得所有语言都应该有这个功能。

标签: objective-c design-patterns switch-statement


【解决方案1】:

您应该利用键值编码:

[character setValue:currentElementText forKey:elementName];

如果数据不可信,您可能需要检查密钥是否有效:

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

【讨论】:

  • 如问题所述,我已经找到了设置这些属性的更好方法,只是在寻找关于我不喜欢的 if-else 模式的建议。
  • 但这就是重点,避免自己进行多次调度,让语言(或框架)的各个方面来处理它。当您可以执行诸如字典查找之类的操作时,不鼓励使用对象值上的 if-else-else-else 或 switch 模式。
【解决方案2】:

我希望你们都原谅我在这里跑题了,但是我想解决在 Cocoa 中解析 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。但是,我们仍然可以通过使用 performSelector 来消除 if-else。第一步是为每个元素定义一个方法。

- (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 和corporateID 的语句)

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 语句更快。

【讨论】:

  • 我确实喜欢这个解决方案的紧凑性。只是几个评论:它是 NSXMLNode 而不是 NSNode。 NSXMLNode *ret——我想不出任何用处,因为 aNode 已经由 nextNode 设置。像这样使用 performSelector 会给出警告:“performSelector 可能会导致泄漏,因为它的选择器是未知的”。解决此问题的最短方法是使用 objc_msgSend,但随后您必须关闭“启用对 objc_msgSend 调用的严格检查”。将其包装在 [self respondsToSelector:selector] 中以减轻 objc_msgSend 的一些缺点。
  • 嘿,感谢您的评论。此答案是一个社区 wiki,因此请随时编辑您可能发现的任何错误。然而,我想指出,这个答案更多的是探索具有任意限制的实现的练习,而不是关于如何实际编写生产代码的建议。
【解决方案3】:

如果您想使用尽可能少的代码,并且您的元素名称和 setter 都已命名,如果 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 语句要慢一些。

[编辑:第二个选项已经有人提到过;哎呀!]

【讨论】:

【解决方案4】:

我敢建议使用宏吗?

#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

【讨论】:

  • 不错,没想到那个。不太习惯使用宏。
【解决方案5】:

我使用 NSStrings 完成此操作的一种方法是使用 NSDictionary 和枚举。它可能不是最优雅的,但我认为它使代码更具可读性。以下伪代码摘自one of my projects

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"] ) 
      

      【讨论】:

      • 正如在原始问题中指出的那样,我已经找到了一种更好的方法来设置属性。我只是在寻找有关处理 if-else 模式的更好模式的建议。
      • 不错的反对票。对于希望以更简洁的方式比较字符串的人来说,它仍然是有用的信息。您正在滥用投票,唯一的安慰是,丢弃非常好的信息会损害您的声誉。我不会再犯错帮助你了。
      【解决方案8】:

      在像 Objective-C 这样的语言中,实际上有一种相当简单的方法来处理级联 if-else 语句。是的,您可以使用子类化和覆盖,创建一组以不同方式实现相同方法的子类,在运行时使用公共消息调用正确的实现。如果您希望从几个实现中选择一种,这很有效,但如果您有许多小的、稍微不同的实现,就像您在长 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;
          }
          

          如您所见,这是一个带有变量参数列表的老式 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,+1 我喜欢这种块方法。我很好奇,您对我刚刚发布的基于块的过滤器链方法有何看法。
          【解决方案11】:

          为消除 if-else 或 switch 语句而建议的最常见重构是引入多态性(请参阅http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html)。当它们被重复时,消除这些条件是最重要的。在像您的示例这样的 XML 解析的情况下,您实际上是将数据移动到更自然的结构,这样您就不必在其他地方复制条件。在这种情况下,if-else 或 switch 语句可能就足够了。

          【讨论】:

            【解决方案12】:

            在这种情况下,我不确定您是否可以像 Bradley 建议的那样轻松重构该类以引入多态性,因为它是一个 Cocoa-native 类。相反,Objective-C 的做法是使用类类别向 NSSting 添加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] 上的开关(如果您忘记测试枚举成员之一等,则会获得相关的编译器警告)。

            正如 Bradley 指出的那样,如果逻辑只用在一个地方,这可能不值得。

            【讨论】:

              【解决方案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];
                

                这种方法可用于切换,也可用于任何(条件)过滤器链应用程序,因为块可以编辑元素并将其传递。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2016-05-09
                  • 1970-01-01
                  • 2010-10-16
                  相关资源
                  最近更新 更多