目标-C开关使用的对象?
-
01-07-2019 - |
题
我做了一些目标-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
图案在目标C
解决方案
我希望你们都能原谅我在这里走出困境,但我想解决在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语句更快。
其他提示
您应该利用键值编码:
[character setValue:currentElementText forKey:elementName];
如果数据不受信任,您可能需要检查密钥是否有效:
if (![validKeysCollection containsObject:elementName])
// Exception or error
如果你想尽可能少地使用代码,你的元素名称和setter都被命名为如果elementName是@&quot; foo&quot;然后setter是setFoo:,你可以这样做:
SEL selector = NSSelectorFromString([NSString stringWithFormat:@"set%@:", [elementName capitalizedString]]);
[character performSelector:selector withObject:currentElementText];
或甚至可能:
[character setValue:currentElementText forKey:elementName]; // KVC-style
虽然这些当然会比使用一堆if语句慢一些。
[编辑:第二个选项已被某人提及;糟糕!]
我敢建议使用宏吗?
#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
我使用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;
}
您拥有的 if-else
实现是正确的方法,因为 switch
不适用于对象。除了可能更难阅读(这是主观的)之外,以这种方式使用 if-else
语句并没有真正的缺点。
虽然不一定有更好的方法来做一次性使用,但为什么要使用“比较”?什么时候可以使用“isEqualToString”?这似乎更具性能,因为比较会在第一个不匹配的字符处停止,而不是通过整个事情来计算有效的比较结果(尽管想到它可能在同一点上清楚比较) - 虽然看起来有点干净,因为该调用返回BOOL。
if([elementName isEqualToString:@"companyName"] )
[character setCorporationName:currentElementText];
else if([elementName isEqualToString:@"corporationID"] )
[character setCorporationID:currentElementText];
else if([elementName isEqualToString:@"name"] )
实际上有一种相当简单的方法来处理像Objective-C这样的语言中的级联if-else语句。是的,您可以使用子类化和重写,创建一组以不同方式实现相同方法的子类,使用公共消息在运行时调用正确的实现。如果你想选择一些实现中的一个,这很有效,但是如果你有很多小的,稍微不同的实现,就像你往往在if-else或switch语句中那样,它会导致子类的不必要的扩散。
相反,将每个if / else-if子句的主体分解为它自己的方法,所有这些都在同一个类中。命名以类似方式调用它们的消息。现在创建一个NSArray,其中包含这些消息的选择器(使用@selector()获得)。使用NSSelectorFromString()将您在条件中测试的字符串强制转换为选择器(您可能需要首先将其他单词或冒号连接到它,具体取决于您如何命名这些消息,以及它们是否接受参数)。现在使用performSelector:。
自我执行选择器这种方法的缺点是它可能会使许多新消息混乱,但是使用新的子类可能比整个类层次结构更容易混乱单个类。
将此作为对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对。(记得,目标-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"
注:
- 这是第一近似值,没有任何错误检查
- 这一事实的情况下处理程序是块,需要额外照顾的时候能见度、范围和存储管理的变量引用内
- 如果你忘记了哨兵,你是注定要失败的。
- 你可以使用的布尔的返回值触发一种"默认"的情况下,当没有任何情况下已经匹配
建议用于消除if-else或switch语句的最常见重构是引入多态性(请参阅 http ://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html )。在重复这些条件时,消除这些条件是最重要的。在像您的示例一样进行XML解析的情况下,您实际上是将数据移动到更自然的结构,这样您就不必在其他地方复制条件。在这种情况下,if-else或switch语句可能已经足够好了。
在这种情况下,我不确定你是否可以像Bradley建议的那样轻松地重构类来引入多态,因为它是一个Cocoa本地类。相反,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]
上使用开关(如果您忘记测试其中一个枚举成员等,则获取相关的编译器警告。)。
正如布拉德利指出的那样,如果只在一个地方使用逻辑,这可能不值得。
我们在我们需要的项目中所做的一遍又一遍,就是设置一个静态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。
与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];
此方法可用于切换,也可用于任何(条件)过滤器链应用程序,因为块可以编辑元素并将其传递。