Can Objective-C switch on NSString? - objective-c

Is there a more intelligent way to rewrite this?
if ([cardName isEqualToString:#"Six"]) {
[self setValue:6];
} else if ([cardName isEqualToString:#"Seven"]) {
[self setValue:7];
} else if ([cardName isEqualToString:#"Eight"]) {
[self setValue:8];
} else if ([cardName isEqualToString:#"Nine"]) {
[self setValue:9];
}

Unfortunately they cannot. This is one of the best and most sought after utilizations of switch statements, so hopefully they hop on the (now) Java (and others) bandwagon!
If you are doing card names, perhaps assign each card object an integer value and switch on that. Or perhaps an enum, which is considered as a number and can therefore be switched upon.
e.g.
typedef enum{
Ace, Two, Three, Four, Five ... Jack, Queen, King
} CardType;
Done this way, Ace would be be equal to case 0, Two as case 1, etc.

You could set up a dictionary of blocks, like this:
NSString *lookup = #"Hearts"; // The value you want to switch on
typedef void (^CaseBlock)();
// Squint and this looks like a proper switch!
NSDictionary *d = #{
#"Diamonds":
^{
NSLog(#"Riches!");
},
#"Hearts":
^{
self.hearts++;
NSLog(#"Hearts!");
},
#"Clubs":
^{
NSLog(#"Late night coding > late night dancing");
},
#"Spades":
^{
NSLog(#"I'm digging it");
}
};
((CaseBlock)d[lookup])(); // invoke the correct block of code
To have a 'default' section, replace the last line with:
CaseBlock c = d[lookup];
if (c) c(); else { NSLog(#"Joker"); }
Hopefully Apple will teach 'switch' a few new tricks.

For me, a nice easy way:
NSString *theString = #"item3"; // The one we want to switch on
NSArray *items = #[#"item1", #"item2", #"item3"];
int item = [items indexOfObject:theString];
switch (item) {
case 0:
// Item 1
break;
case 1:
// Item 2
break;
case 2:
// Item 3
break;
default:
break;
}

Unfortunately, switch statements can only be used on primitive types. You do have a few options using collections, though.
Probably the best option would be to store each value as an entry in an NSDictionary.
NSDictionary *stringToNumber = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithInt:6],#"Six",
[NSNumber numberWithInt:7],#"Seven",
[NSNumber numberWithInt:8],#"Eight",
[NSNumber numberWithInt:9],#"Nine",
nil];
NSNumber *number = [stringToNumber objectForKey:cardName];
if(number) [self setValue:[number intValue]];

A bit late but for anyone in the future I was able to get this to work for me
#define CASE(str) if ([__s__ isEqualToString:(str)])
#define SWITCH(s) for (NSString *__s__ = (s); ; )
#define DEFAULT

Here is the more intelligent way to write that. It's to use an NSNumberFormatter in the "spell-out style":
NSString *cardName = ...;
NSNumberFormatter *nf = [[NSNumberFormatter alloc] init];
[nf setNumberStyle:NSNumberFormatterSpellOutStyle];
NSNumber *n = [nf numberFromString:[cardName lowercaseString]];
[self setValue:[n intValue]];
[nf release];
Note that the number formatter wants the string to be lowercased, so we have to do that ourselves before passing it in to the formatter.

There are other ways to do that, but switch isn't one of them.
If you only have a few strings, as in your example, the code you have is fine. If you have many cases, you could store the strings as keys in a dictionary and look up the corresponding value:
NSDictionary *cases = #{#"Six" : #6,
#"Seven" : #7,
//...
};
NSNumber *value = [cases objectForKey:cardName];
if (value != nil) {
[self setValue:[value intValue]];
}

BY FAR.. my FAVORITE "ObjC Add-On" is ObjectMatcher
objswitch(someObject)
objcase(#"one") { // Nesting works.
objswitch(#"b")
objcase(#"a") printf("one/a");
objcase(#"b") printf("one/b");
endswitch // Any code can go here, including break/continue/return.
}
objcase(#"two") printf("It's TWO."); // Can omit braces.
objcase(#"three", // Can have multiple values in one case.
nil, // nil can be a "case" value.
[self self], // "Case" values don't have to be constants.
#"tres", #"trois") { printf("It's a THREE."); }
defaultcase printf("None of the above."); // Optional default must be at end.
endswitch
AND it works with non-strings, TOO... in loops, even!
for (id ifNumericWhatIsIt in #[#99, #0, #"shnitzel"])
objswitch(ifNumericWhatIsIt)
objkind(NSNumber) printf("It's a NUMBER.... ");
objswitch([ifNumericWhatIsIt stringValue])
objcase(#"3") printf("It's THREE.\n");
objcase(#"99") printf("It's NINETY-NINE.\n");
defaultcase printf("some other Number.\n");
endswitch
defaultcase printf("It's something else entirely.\n");
endswitch
It's a NUMBER.... It's NINETY-NINE.
It's a NUMBER.... some other Number.
It's something else entirely.
Best of all, there are SO few {...}'s, :'s, and ()'s

Objective-c is no different from c in this aspect, it can only switch on what c can (and the preproc def's like NSInteger, NSUInteger, since they ultimately are just typedef'd to an integral type).
Wikipedia:
c syntax:
The switch statement causes control to be transferred to one of several statements depending on the value of an expression, which must have integral type.
Integral Types:
In computer science, an integer is a datum of integral data type, a
data type which represents some finite subset of the mathematical
integers. Integral data types may be of different sizes and may or may
not be allowed to contain negative values.

I'm kind of late to the party, but to answer the question as stated, there's a more intelligent way:
NSInteger index = [#[#"Six", #"Seven", #"Eight", #"Nine"] indexOfObject:cardName];
if (index != NSNotFound) [self setValue: index + 6];
Note that indexOfObject will look for the match using isEqual:, exactly as in the question.

Building on #Graham Perks idea posted earlier, designed a simple class to make switching on strings fairly simple and clean.
#interface Switcher : NSObject
+ (void)switchOnString:(NSString *)tString
using:(NSDictionary<NSString *, CaseBlock> *)tCases
withDefault:(CaseBlock)tDefaultBlock;
#end
#implementation Switcher
+ (void)switchOnString:(NSString *)tString
using:(NSDictionary<NSString *, CaseBlock> *)tCases
withDefault:(CaseBlock)tDefaultBlock
{
CaseBlock blockToExecute = tCases[tString];
if (blockToExecute) {
blockToExecute();
} else {
tDefaultBlock();
}
}
#end
You would use it like this:
[Switcher switchOnString:someString
using:#{
#"Spades":
^{
NSLog(#"Spades block");
},
#"Hearts":
^{
NSLog(#"Hearts block");
},
#"Clubs":
^{
NSLog(#"Clubs block");
},
#"Diamonds":
^{
NSLog(#"Diamonds block");
}
} withDefault:
^{
NSLog(#"Default block");
}
];
The correct block will execute according to the string.
Gist for this solution

You can use macros approach to achieve it:
#define CASE(str) if ([__s__ isEqualToString:(str)])
#define SWITCH(s) for (NSString *__s__ = (s); ; )
#define DEFAULT
SWITCH (string) {
CASE (#"TestString") {
break;
}
CASE (#"YetAnotherString") {
break;
}
CASE (#"Test") {
break;
}
DEFAULT {
break;
}
}

I can't Comment on cris's answer on #Cris answer but i would like to say that:
There is an LIMITATION for #cris's method:
typedef enum will not take alphanumeric values
typedef enum
{
12Ace, 23Two, 23Three, 23Four, F22ive ... Jack, Queen, King
} CardType;
So here is another One:
Link Stack over flow Go to this user answer "user1717750"

typedef enum
{
Six,
Seven,
Eight
} cardName;
- (void) switchcardName:(NSString *) param {
switch([[cases objectForKey:param] intValue]) {
case Six:
NSLog(#"Six");
break;
case Seven:
NSLog(#"Seven");
break;
case Eight:
NSLog(#"Eight");
break;
default:
NSLog(#"Default");
break;
}
}
Enjoy Coding.....

Related

Better way to observe in ReactiveCocoa

I'm still new to ReactiveCocoa. I wanted to simply add an observer to a field, so did it like this:
[_countryPicker rac_observeKeyPath:#"value" options:nil observer:self block:^(VBCountry* value, NSDictionary* change)
{
if ([_mobileField.textField.text length] == 0)
{
[_mobileField.textField setText:[NSString stringWithFormat:#"+%i", value.dialCode]];
}
}];
With block callback, and no need to explicitly detach the observer, this is already better than old-style KVO.
However, is this a low-level method to which there's a higher level of abstraction? If so, is it OK to call this method directly? And what's the better/higher way to do it?
I'd advise against depending on the direct KVO methods. They're really an implementation detail.
Let's progressively re-write that with idiomatic RAC operators.
First we'll just replace the direct KVO code with RACObserve:
[RACObserve(_countryPicker, value) subscribeNext:^(VBCountry *value) {
if ([_mobileField.textField.text length] == 0)
{
[_mobileField.textField setText:[NSString stringWithFormat:#"+%i", value.dialCode]];
}
}];
Then we'll replace the if and string formatting with -filter: and -map::
[[[RACObserve(_countryPicker, value) filter:^(id _) {
return [_mobileField.textField.text length] > 0;
}] map:^(VBCountry *value) {
return [NSString stringWithFormat:#"+%i", value.dialCode];
}] subscribeNext:^(NSString *text) {
[_mobileField.textField setText:text];
}];
Finally we'll use the RAC macro to make the assignment over time explicit:
RAC(_mobileField.textField, text) = [[RACObserve(_countryPicker, value) filter:^(id _) {
return [_mobileField.textField.text length] > 0;
}] map:^(VBCountry *value) {
return [NSString stringWithFormat:#"+%i", value.dialCode];
}];

NSString "nil or empty" check -- is this complete?

I was writing a small Category on NSString, and I wanted to know if this method is accurately handles all potential use cases:
Update: to clarify -- I wanted to make sure I'm not missing some oddball case involving character encodings, etc..
#implementation NSString (Helpers)
+(BOOL)stringIsNilOrEmpty:(NSString*)aString {
if (!aString)
return YES;
return [aString isEqualToString:#""];
}
#end
Sample usage:
-(void) sampleUsage {
NSString *emptyString = #"";
NSString *nilString = nil;
NSAssert([NSString stringIsNilOrEmpty:nilString] == YES, #"String is nil/empty");
NSAssert([NSString stringIsNilOrEmpty:emptyString] == YES, #"String is nil/empty");
}
#end
I only use the next conditional and do not even need a category:
if (!aString.length)
{
...
}
Using Objective-C theory, a message to NIL will return nil or zero, so basically you do not have to test for nil.
You can simplify the code by removing conditional:
+(BOOL)stringIsNilOrEmpty:(NSString*)aString {
return !(aString && aString.length);
}
#dasblinkenlight's answer is fine, but a much more readable conditional check I would use is:
NSString *string = ...; // define the string
if ([string length] == 0) {
// Do stuff with the string
} else {
// The string is empty or nil here
}
Very concise and does not require a separate convenience function definition. It's easy enough to remember.
EDIT: #Michael G. Emmons posted this as the last comment to that answer... credit to him but I'm listing this as an answer in its own right.
Some examples of this sort of "is not empty or blank" tests as a category on NSString.
// Please note that in general I advocate using a prefix on category methods
// to avoid category collisions. I've not done this here for clarity.
// The #interface is also excluded from this example for brevity.
#implementation NSString (MyAdditions)
- (BOOL)isNotEmpty
{
return [self length] != 0;
}
- (BOOL)isNotBlank
{
if ([self isNotEmpty])
{
NSCharacterSet *nonWhitespaceSet = [[NSCharacterSet whitespaceAndNewlineCharacterSet] invertedSet];
NSRange range = [self rangeOfCharactersFromSet:nonWhitespaceSet];
return range.location != NSNotFound;
}
return NO;
}
#end
Simply Check your string length
> if (!yourString.length){
> //your code } a
message to NIL will return nil or 0, so no need to test for nil :).
Happy coding ...
Make sure to check for spaces, trim white spaces before calculating length.
+(BOOL)stringIsNilOrEmpty:(NSString*)aString {
return !aString || [[aString stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceCharacterSet]] length] == 0;
}

Objective-C:How to Converting int to id for selector's object?

I received a return value from SQLite fetch
int primaryKey = sqlite3_column_int(statement, 0);
and I'm going to use it as a selector's object:
[[ABC alloc] performSelector:#selector(abcWithAAA:) withObject:[NSNumber numberWithInt:primaryKey]];
the NSLog result for primarykey is a number 4:
NSLog(#"primaryKey:%i",primaryKey);
4
but the NSLog result for [NSNumber numberWithInt:primaryKey] is 131628896.
why? and how do i convert the int value correctly?
Thanks!
I solved the problem using an adapter method that does the cast for the withObject method.
My problem was that I wanted to use a typedef enum and pass it as value to the withObject.
I wanted to call this method using the performSelect message:
-(void) requestInfosAndPersistByMonsterType:(MonsterTypes)monsterType {
}
As you see it request a MonsterTypes typedef defined like this:
typedef enum
{
MonsterTypeIWerwolf = 0,
MonsterTypeITempler = 1,
MonsterTypeIUndefined,
} MonsterTypes;
Actually to be able to call the method above I build this adapter that calls it then:
-(void)monsterTypeFromObject:(id)_monsterType {
if ([_monsterType respondsToSelector:#selector(intValue)]) {
int _t = [_monsterType intValue];
switch (_t) {
case MonsterTypeIWerwolf:
_t = MonsterTypeIWerwolf;
break;
case MonsterTypeITempler:
_t = MonsterTypeITempler;
break;
default:
_t = MonsterTypeIUndefined;
break;
}
[self requestInfosAndPersistByMonsterType:_t];
}
}
It is used this way:
[self performSelector:#selector(monsterTypeFromObject:) withObject:[NSNumber numberWithUnsignedInt:monsterType] afterDelay:5.0f];
You can find it explained in more detail here:
http://kerkermeister.net/objective-c-adapter-from-nsinteger-to-id-when-using-performselector-withobject/
When you log [NSNumber numberWithInt:primaryKey], you're logging the address of an NSNumber object. If you want to see what's inside it should be [[NSNumber numberWithInt:primaryKey] intValue].
In other words, there's nothing in that to suggest your conversion is a problem.
[NSNumber numberWithInt:primaryKey] is object.
use %# for objects.
NSLog(#"%#", [NSNumber numberWithInt:primaryKey]);
131628896 is the memory address of the NSNumber object.
use:
- (void)abcWithAAA: (NSNumber *)number {
int primaryKey = [number intValue];
NSLog("%i", primaryKey);
}

get type of NSNumber

I want to get the type of NSNumber instance.
I found out on http://www.cocoadev.com/index.pl?NSNumber this:
NSNumber *myNum = [[NSNumber alloc] initWithBool:TRUE];
if ([[myNum className] isEqualToString:#"NSCFNumber"]) {
// process NSNumber as integer
} else if ([[myNum className] isEqualToString:#"NSCFBoolean"]) {
// process NSNumber as boolean
}
Ok, but this doesn't work, the [myNum className] isn't recognized by the compiler.
I'm compiling for iPhone.
I recommend using the -[NSNumber objCType] method.
It allows you to do:
NSNumber * n = [NSNumber numberWithBool:YES];
if (strcmp([n objCType], #encode(BOOL)) == 0) {
NSLog(#"this is a bool");
} else if (strcmp([n objCType], #encode(int)) == 0) {
NSLog(#"this is an int");
}
For more information on type encodings, check out the Objective-C Runtime Reference.
You can get the type this way, no string comparisons needed:
CFNumberType numberType = CFNumberGetType((CFNumberRef)someNSNumber);
numberType will then be one of:
enum CFNumberType {
kCFNumberSInt8Type = 1,
kCFNumberSInt16Type = 2,
kCFNumberSInt32Type = 3,
kCFNumberSInt64Type = 4,
kCFNumberFloat32Type = 5,
kCFNumberFloat64Type = 6,
kCFNumberCharType = 7,
kCFNumberShortType = 8,
kCFNumberIntType = 9,
kCFNumberLongType = 10,
kCFNumberLongLongType = 11,
kCFNumberFloatType = 12,
kCFNumberDoubleType = 13,
kCFNumberCFIndexType = 14,
kCFNumberNSIntegerType = 15,
kCFNumberCGFloatType = 16,
kCFNumberMaxType = 16
};
typedef enum CFNumberType CFNumberType;
If all you want is to differentiate between booleans and anything else, you can make use of the fact that boolean NSNumbers always return a shared instance:
NSNumber *num = ...;
if (num == (void*)kCFBooleanFalse || num == (void*)kCFBooleanTrue) {
// num is boolean
} else {
// num is not boolean
}
NSNumber explicitly doesn't guarantee that the returned type will match the method used to create it, so doing this at all is probably a bad idea.
However, you could probably do something like this (you could also compare to objc_getClass("NSCFNumber") etc., but this is arguably more portable):
Class boolClass = [[NSNumber numberWithBool:YES] class];
/* ... */
if([myNum isKindOfClass:boolClass]) {
/* ... */
}
In Swift:
let numberType = CFNumberGetType(answer)
switch numberType {
case .charType:
//Bool
case .sInt8Type, .sInt16Type, .sInt32Type, .sInt64Type, .shortType, .intType, .longType, .longLongType, .cfIndexType, .nsIntegerType:
//Int
case .float32Type, .float64Type, .floatType, .doubleType, .cgFloatType:
//Double
}
Use the method -[NSNumber objCType] method to get the type.
If the type's equal to #encode(BOOL), or the number itself is kCFBooleanFalse, or kCFBooleanTrue, it's a boolean.
If it's anything else but 'c', it's a number.
If it's 'c', what appears to be the only way supported way, without checking against private class names, or comparing against undocumented singletons, is to turn make an array of one element, the number, and then use NSJSONSerialization to get the string representation. Finally, check if the string representation contains the string "true" or "false". Here is the full code for checking if an NSNumber is a BOOL:
-(BOOL)isBool
{
if(!strcmp(self.objCType, #encode(BOOL)) ||
self == (void*)kCFBooleanFalse ||
self == (void*)kCFBooleanTrue)
{
return YES;
}
if(strcmp(self.objCType, "c"))
{
return NO;
}
NSString * asString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:#[self] options:kNilOptions error:nil] encoding:NSUTF8StringEncoding];
return [asString containsString:#"true"] || [asString containsString:#"false"];
}
Note that using NSJSONSerialization is slow and if #NO/#YES ever stops always equalling kCFBooleanFalse/kCFBooleanTrue, then this method probably shouldn't be used in a tight loop.
The reason the compiler warns you and it doesn't work is because -[NSObject className] is declared in a category on NSObject on Mac OS X (in NSScriptClassDescription.h) and not declared on iPhone. (It doesn't support AppleScript, obviously.) NSStringFromClass([myNum class]) is what you should use to be safe across all platforms. Odds are that -className is declared as a simple wrapper around NSStringFromClass() anyway...
NSString *classString = NSStringFromClass([myNum class]);
That should ger the string you want.
To check that NSNumber contains a bool value Try this:
if (strcmp([myNumber objCType], [#(YES) objCType]) == 0)
NSLog(#"%#", [myNumber boolValue] ? #"true" : #"false");
objCType documentation states that The returned type does not necessarily match the method the number object was created with
Secondly, other methods of comparing the class of number to a given class type or assuming boolean number instances to be shared singletons are not documented behaviour.
A more(not completely though) reliable way is to depend on NSJSONSerialisation as it correctly recognises number instances created with bool and outputs true/false in json. This is something we can expect Apple to take care of while moving with new SDKs and on different architectures. Below is the code:
+(BOOL) isBoolType:(NSNumber*) number {
NSError* err;
NSData* jsonData = [NSJSONSerialization dataWithJSONObject:#{#"key":number}
options:0
error:&err];
NSString* jsonString = [[NSString alloc]
initWithData:jsonData
encoding:NSUTF8StringEncoding];
return [jsonString containsString:#"true"]
|| [jsonString containsString:#"false"];
}
Swift Version
NSNumber is a class-cluster so each underlying type can be figured from the instance. This code avoids hard-coding the different NSNumber types by creating an instance of the expected type, and then comparing it against the unknown type.
extension NSNumber {
var isBool: Bool {
return type(of: self) == type(of: NSNumber(booleanLiteral: true))
}
}
check object is of NSNumber type :
if([obj isKindOfClass:NSClassFromString(#"__NSCFNumber")])
{
//NSNumber
}

Objective-C switch using objects?

I'm doing some Objective-C programming that involves parsing an NSXmlDocument and populating an objects properties from the result.
First version looked like this:
if([elementName compare:#"companyName"] == 0)
[character setCorporationName:currentElementText];
else if([elementName compare:#"corporationID"] == 0)
[character setCorporationID:currentElementText];
else if([elementName compare:#"name"] == 0)
...
But I don't like the if-else-if-else pattern this produces. Looking at the switch statement I see that i can only handle ints, chars etc and not objects... so is there a better implementation pattern I'm not aware of?
BTW I did actually come up with a better solution for setting the object's properties, but I want to know specifically about the if-else vs switch pattern in Objective-C
You should take advantage of Key-Value Coding:
[character setValue:currentElementText forKey:elementName];
If the data is untrusted, you might want to check that the key is valid:
if (![validKeysCollection containsObject:elementName])
// Exception or error
I hope you'll all forgive me for going out on a limb here, but I would like to address the more general question of parsing XML documents in Cocoa without the need of if-else statements. The question as originally stated assigns the current element text to an instance variable of the character object. As jmah pointed out, this can be solved using key-value coding. However, in a more complex XML document this might not be possible. Consider for example the following.
<xmlroot>
<corporationID>
<stockSymbol>EXAM</stockSymbol>
<uuid>31337</uuid>
</corporationID>
<companyName>Example Inc.</companyName>
</xmlroot>
There are multiple approaches to dealing with this. Off of the top of my head, I can think of two using NSXMLDocument. The first uses NSXMLElement. It is fairly straightforward and does not involve the if-else issue at all. You simply get the root element and go through its named elements one by one.
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]];
The next one uses the more general NSXMLNode, walks through the tree, and directly uses the if-else structure.
// 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]];
}
}
}
}
This is a good candidate for eliminating the if-else structure, but like the original problem, we can't simply use switch-case here. However, we can still eliminate if-else by using performSelector. The first step is to define the a method for each element.
- (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;
}
The magic happens in the invokeMethodForNode:prefix: method. We generate the selector based on the name of the element, and perform that selector with aNode as the only parameter. Presto bango, we've eliminated the need for an if-else statement. Here's the code for that method.
- (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;
}
Now, instead of our larger if-else statement (the one that differentiated between companyName and corporationID), we can simply write one line of code
NSXMLNode* aNode = [xmlDocument rootElement];
while(aNode = [aNode nextNode]){
aNode = [self invokeMethodForNode:aNode prefix:#"parse_"];
}
Now I apologize if I got any of this wrong, it's been a while since I've written anything with NSXMLDocument, it's late at night and I didn't actually test this code. So if you see anything wrong, please leave a comment or edit this answer.
However, I believe I have just shown how properly-named selectors can be used in Cocoa to completely eliminate if-else statements in cases like this. There are a few gotchas and corner cases. The performSelector: family of methods only takes 0, 1, or 2 argument methods whose arguments and return types are objects, so if the types of the arguments and return type are not objects, or if there are more than two arguments, then you would have to use an NSInvocation to invoke it. You have to make sure that the method names you generate aren't going to call other methods, especially if the target of the call is another object, and this particular method naming scheme won't work on elements with non-alphanumeric characters. You could get around that by escaping the XML element names in your method names somehow, or by building an NSDictionary using the method names as the keys and the selectors as the values. This can get pretty memory intensive and end up taking a longer time. performSelector dispatch like I described is pretty fast. For very large if-else statements, this method may even be faster than an if-else statement.
If you want to use as little code as possible, and your element names and setters are all named so that if elementName is #"foo" then setter is setFoo:, you could do something like:
SEL selector = NSSelectorFromString([NSString stringWithFormat:#"set%#:", [elementName capitalizedString]]);
[character performSelector:selector withObject:currentElementText];
or possibly even:
[character setValue:currentElementText forKey:elementName]; // KVC-style
Though these will of course be a bit slower than using a bunch of if statements.
[Edit: The second option was already mentioned by someone; oops!]
Dare I suggest using a macro?
#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
One way I've done this with NSStrings is by using an NSDictionary and enums. It may not be the most elegant, but I think it makes the code a little more readable. The following pseudocode is extracted from 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;
}
The if-else implementation you have is the right way to do this, since switch won't work with objects. Apart from maybe being a bit harder to read (which is subjective), there is no real downside in using if-else statements this way.
Although there's not necessarily a better way to do something like that for one time use, why use "compare" when you can use "isEqualToString"? That would seem to be more performant since the comparison would halt at the first non-matching character, rather than going through the whole thing to calculate a valid comparison result (though come to think of it the comparison might be clear at the same point) - also though it would look a little cleaner because that call returns a BOOL.
if([elementName isEqualToString:#"companyName"] )
[character setCorporationName:currentElementText];
else if([elementName isEqualToString:#"corporationID"] )
[character setCorporationID:currentElementText];
else if([elementName isEqualToString:#"name"] )
There is actually a fairly simple way to deal with cascading if-else statements in a language like Objective-C. Yes, you can use subclassing and overriding, creating a group of subclasses that implement the same method differently, invoking the correct implementation at runtime using a common message. This works well if you wish to choose one of a few implementations, but it can result in a needless proliferation of subclasses if you have many small, slightly different implementations like you tend to have in long if-else or switch statements.
Instead, factor out the body of each if/else-if clause into its own method, all in the same class. Name the messages that invoke them in a similar fashion. Now create an NSArray containing the selectors of those messages (obtained using #selector()). Coerce the string you were testing in the conditionals into a selector using NSSelectorFromString() (you may need to concatenate additional words or colons to it first depending on how you named those messages, and whether or not they take arguments). Now have self perform the selector using performSelector:.
This approach has the downside that it can clutter-up the class with many new messages, but it's probably better to clutter-up a single class than the entire class hierarchy with new subclasses.
Posting this as a response to Wevah's answer above -- I would've edited, but I don't have high enough reputation yet:
unfortunately the first method breaks for fields with more than one word in them -- like xPosition. capitalizedString will convert that to Xposition, which when combined with the format give you setXposition: . Definitely not what was wanted here. Here is what I'm using in my code:
NSString *capName = [elementName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[elementName substringToIndex:1] uppercaseString]];
SEL selector = NSSelectorFromString([NSString stringWithFormat:#"set%#:", capName]);
Not as pretty as the first method, but it works.
I have come up with a solution that uses blocks to create a switch-like structure for objects. There it goes:
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;
}
As you can see, this is an oldschool C function with variable argument list. I pass the object to be tested in the first argument, followed by the case_value-case_block pairs. (Recall that Objective-C blocks are just objects.) The while loop keeps extracting these pairs until the object value is matched or there are no cases left (see notes below).
Usage:
NSString* str = #"stuff";
switch_object(str,
#"blah", ^{
NSLog(#"blah");
},
#"foobar", ^{
NSLog(#"foobar");
},
#"stuff", ^{
NSLog(#"stuff");
},
#"poing", ^{
NSLog(#"poing");
},
nil); // <-- sentinel
// will print "stuff"
Notes:
this is a first approximation without any error checking
the fact that the case handlers are blocks, requires additional care when it comes to visibility, scope and memory management of variables referenced from within
if you forget the sentinel, you are doomed :P
you can use the boolean return value to trigger a "default" case when none of the cases have been matched
The most common refactoring suggested for eliminating if-else or switch statements is introducing polymorphism (see http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html). Eliminating such conditionals is most important when they are duplicated. In the case of XML parsing like your sample you are essentially moving the data to a more natural structure so that you won't have to duplicate the conditional elsewhere. In this case the if-else or switch statement is probably good enough.
In this case, I'm not sure if you can easily refactor the class to introduce polymorphism as Bradley suggests, since it's a Cocoa-native class. Instead, the Objective-C way to do it is to use a class category to add an elementNameCode method to 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
In your code, you could now use a switch on [elementName elementNameCode] (and gain the associated compiler warnings if you forget to test for one of the enum members etc.).
As Bradley points out, this may not be worth it if the logic is only used in one place.
What we've done in our projects where we need to so this sort of thing over and over, is to set up a static CFDictionary mapping the strings/objects to check against to a simple integer value. It leads to code that looks like this:
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;
}
If you're targeting Leopard only, you could use an NSMapTable instead of a CFDictionary.
Similar to Lvsti I am using blocks to perform a switching pattern on objects.
I wrote a very simple filter block based chain, that takes n filter blocks and performs each filter on the object.
Each filter can alter the object, but must return it. No matter what.
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
Now we can set up n FilterBlocks to test for the different cases.
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;
};
Now we stick those block we want to test as a filter chain in an array:
NSArray *filters = #[caseYES, caseNO];
and can perform it on an object
id obj1 = #"YES";
id obj2 = #"NO";
[obj1 processByPerformingFilterBlocks:filters];
[obj2 processByPerformingFilterBlocks:filters];
This approach can be used for switching but also for any (conditional) filter chain application, as the blocks can edit the element and pass it on.