使用Objective-C切换对象?

时间:2020-03-06 14:27:15  来源:igfitidea点击:

我正在做一些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`等对象,而不能处理对象...所以有更好的实现模式我不知道吗?

顺便说一句,我确实提出了一个更好的解决方案来设置对象的属性,但是我想专门了解Objective-C中的if-else和switch模式。

解决方案

建议用于消除if-else或者switch语句的最常见的重构方法是引入多态性(请参见http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html)。当重复这些条件时,消除这些条件最为重要。在像样本一样进行XML解析的情况下,我们实际上是将数据移至更自然的结构,因此我们不必在其他地方重复条件。在这种情况下,if-else或者switch语句可能就足够了。

我们拥有的if-else实现是正确的方法,因为switch不适用于对象。除了可能难以阅读(这是主观的)以外,以这种方式使用if-else语句并没有真正的不利之处。

在这种情况下,我不确定是否可以像Bradley建议的那样轻松地重构该类以引入多态性,因为它是可可本机类。相反,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指出的那样,如果仅在一个地方使用逻辑,这可能不值得。

尽管不一定有更好的方法可以一次性使用类似的方法,但是当可以使用" isEqualToString"时为什么还要使用"比较"呢?由于比较会在第一个不匹配的字符处停止,而不是遍历整个过程来计算有效的比较结果(尽管考虑到这一点,比较可能在同一时间点就很清楚了),因此这似乎表现得更好。虽然看起来会更干净一点,因为该调用返回了BOOL。

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

我们应该利用键值编码:

[character setValue:currentElementText forKey:elementName];

如果数据不受信任,则可能要检查密钥是否有效:

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

我使用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;
}

我希望大家都原谅我的努力,但是我想解决在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和corporationID的语句)。

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

敢于建议使用宏吗?

#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语句要慢一些。

[编辑:第二种选择已经被某人提及;哎呀!]

实际上,有一种相当简单的方法来处理诸如Objective-C之类的级联if-else语句。是的,我们可以使用子类化和重写,创建一组以不同方式实现相同方法的子类,并在运行时使用一条公共消息调用正确的实现。如果我们希望选择几种实现之一,则此方法效果很好,但如果我们拥有许多小的,略有不同的实现,例如倾向于使用长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]);

虽然不如第一种方法漂亮,但是它可以工作。