42

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.

okami
  • 2,093
  • 7
  • 28
  • 40
  • 11
    This is potentially fragile. NSCFNumber and NSCFBoolean are private and there's no guarantee that those will continue being the class names in the future. – Chuck Mar 25 '10 at 19:51
  • How about if([myNum class] == [[NSNumber numberWithBool:YES] class]) – Glenn Howes Apr 04 '13 at 19:16
  • @GlennHowes No guarantee that that will yield the correct result. You make the same assumption as @okami; that private interfaces will never change. – Berik Jul 16 '13 at 14:25

12 Answers12

75

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.

Dave DeLong
  • 242,470
  • 58
  • 448
  • 498
  • +1 cool tip! Strange that they don't have a method to get this out of NSNumber much easier. – jpswain Dec 22 '11 at 20:35
  • 17
    This doesn't work - at least on iOS: (lldb) p (char *)[[NSNumber numberWithBool:YES] objCType] - it encodes the bool as a char internally (which is correct at the machine level, but not at the intentional level) – th_in_gs May 23 '12 at 21:39
  • 10
    This information is dated, it won't work on 64-bit iOS devices and simulators and therefore it should not be used. It can lead to very hard to find issues which will only occur on 64-bit iOS devices. – kylef Jul 07 '14 at 15:11
  • @kylef In what way does it fail on 64-bit devices? – Ethan Holshouser Aug 18 '14 at 17:53
  • 3
    The documentation notes a very important special consideration: "The returned type does not necessarily match the method the number object was created with." – zneak Oct 21 '14 at 18:55
  • @EthanHolshouser On 32-bit systems, both `[n objCType]` and `@encode(BOOL)` return "c", but on 64-bit systems, `@encode(BOOL)` returns "B". This seems like a bug in `NSNumber` to me. – Greg Brown Sep 09 '15 at 14:32
  • It's because of how the `BOOL` type is defined: `#if (TARGET_OS_IPHONE && __LP64__) || TARGET_OS_WATCH #define OBJC_BOOL_IS_BOOL 1 typedef bool BOOL; #else #define OBJC_BOOL_IS_CHAR 1 typedef signed char BOOL; // BOOL is explicitly signed so @encode(BOOL) == "c" rather than "C" // even if -funsigned-char is used. #endif ` – Dmitry Makarenko Aug 31 '16 at 11:43
50

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;
Chris
  • 39,719
  • 45
  • 189
  • 235
  • 17
    Unfortunately, there is no kCFNumberBoolType to distinguish boolean values from characters, so this does not work for all cases. – Michael Manner Jul 07 '12 at 10:50
  • Both kCFNumberCharType == CFNumberGetType((__bridge CFNumberRef)nsValue) and 0 == strcmp([nsValue objCType], "c") work on 32 and 64bit systems, but if you're unfortunate enough to support code that cares, I think the former "feels" marginally safer. – Rhythmic Fistman Sep 23 '14 at 01:30
  • @MichaelManner you should be using Unichar and/or NSValue for storing characters, not NSNumber. – Cœur Aug 24 '18 at 09:37
30

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
}
Jakob Egger
  • 11,981
  • 4
  • 38
  • 48
  • 2
    +1 for the ingenuity, but I wouldn't recommend it for production code (the solution is waaaay too fragile...) – Rick77 Apr 27 '15 at 08:20
  • 1
    It depends what the failure mode is. I use it to display "TRUE" or "FALSE" for Booleans; in the unlikely case Apple changes this implementation detail, my app would display "1" or "0" instead; I can live with that. (Especially since there is no alternative besides writing your own NSNumber subclass that keeps track of the type it was created with) – Jakob Egger Apr 27 '15 at 14:33
  • Granted: your solution is the best so far and, as you correctly point out, the only one (about the NSNumber subclass: I came here because I wanted to distinguish booleans vs integers in a plist, much good it would do to me...). Also granted that in case of failure, the solution would return correct truthy and falsy values. Point is, either you care whether a value is a boolean (and you can't accept false negatives), or you don't (so why bother? :) ) – Rick77 Apr 28 '15 at 09:41
  • if(num == [NSNumbler numberWithBool:YES] || ... also works – malhal Jul 27 '15 at 00:44
  • 2
    You could even use `if (num==@(YES) || num==@(NO))`. But using Core Foundation constants looks really sophisticated, while comparing Objective C objects with `==` looks like a newbie mistake ;) – Jakob Egger Jul 28 '15 at 06:22
  • @JakobEgger Where is it documented that `NSNumber` always returns a shared instance for boolean values? I haven't been able to verify that. However, `[num isEqual:@YES] || [num isEqual:@NO]` should work in either case. – Greg Brown Sep 09 '15 at 14:40
  • 1
    @GregBrown To my knowledge it is not officially documented. But it is officially documented that NSNumber is toll-free bridged to CFNumberRef, and if you look at [CFNumber.c](http://www.opensource.apple.com/source/CF/CF-1151.16/CFNumber.c) it's obvious that all the CFBoolean* functions just compare against a static pointer (kCFBooleanTrue). Therefore NSNumber must return the shared instance, otherwise it would not be compatible with CFBoolean. – Jakob Egger Sep 10 '15 at 14:46
  • 1
    @GregBrown [num isEqual:@YES] will not work, because [@1 isEqual:@YES] will return true! – Jakob Egger Sep 10 '15 at 14:50
  • 1
    @JakobEgger I just discovered that. It's annoying. A number should never be logically equal to a boolean. In any case, I'm reluctant to rely on an undocumented solution. In fact, I tried your approach earlier today and Xcode generates this warning: "warning: direct comparison of a numeric literal has undefined behavior". – Greg Brown Sep 10 '15 at 14:52
  • 1
    The original `num == (void*)kCFBooleanFalse` code does not generate a warning. If you need a distinct Boolean type, and don't want to rely on an implementation detail, I recommend creating a custom class to represent boolean values. – Jakob Egger Sep 10 '15 at 14:57
  • @JakobEgger A custom class won't work for me - I need to support native Swift Bool types. The original solution may be OK for my needs, but your second suggestion does generate a warning. – Greg Brown Sep 10 '15 at 14:59
9

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]) {
  /* ... */
}
user303014
  • 99
  • 1
7

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
}
ChikabuZ
  • 10,031
  • 5
  • 63
  • 86
  • Could be correct, but please note that Swift can distinguish between `Float32` (aka `Float`), `Float64` (aka `Double`) and `Float80`, so maybe, _just maybe_, in case of .float32Type and .floatType, it is a `Float` instead. We would need a demonstration or a reference to verify. – Cœur Jan 03 '18 at 06:06
  • Well, seeing that there are only three literal initializers for NSNumber (Int, Double, Bool), I guess that this answer is as accurate as Apple Swift developers want it to be. – Cœur Aug 24 '18 at 08:01
5

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.

cncool
  • 1,015
  • 11
  • 12
4

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...

Quinn Taylor
  • 44,553
  • 16
  • 113
  • 131
3
NSString *classString = NSStringFromClass([myNum class]);

That should ger the string you want.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • isn't there a way to get the type of the NSNumber without comparing with strings? I know that there is the "objCType" method, but each time I retrieve it, it returns a different value. – okami Mar 25 '10 at 19:44
0

To check that NSNumber contains a bool value Try this:

if (strcmp([myNumber objCType], [@(YES) objCType]) == 0)
NSLog(@"%@", [myNumber boolValue] ? @"true" : @"false");
-1

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

}
mickeymoon
  • 4,820
  • 5
  • 31
  • 56
  • Having the overhead of a JSON tokenizer and a long arbitrary String is not a way I would recommend to deal with Boolean detection. Jakob Egger answer is more elegant and is backed up by documentation that NSNumber is toll-free bridged to CFNumberRef, itself directly comparable to two static values. – Cœur Aug 24 '18 at 09:03
-1

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))
    }
}
Community
  • 1
  • 1
kgaidis
  • 14,259
  • 4
  • 79
  • 93
  • This will only work to distinguish boolean from non-boolean. For instance, an NSNumber initialized from an integerLiteral and from a floatLiteral will share the same class-cluster, so your `NSNumber(floatLiteral: 0.1).isInt` is `true`. And there is a well-known NSNumber subclass that will not compare well: `NSDecimalNumber(integerLiteral: 0).isInt` is `false`. – Cœur Aug 24 '18 at 08:54
  • Apple and people's frameworks are free to create additional subclasses of NSNumber supporting a boolean, so this solution shouldn't claim it's future-proof. – Cœur Aug 24 '18 at 09:06
  • @Cœur thank you for the note, what would you suggest as the best way to do this? – kgaidis Sep 05 '18 at 13:04
  • ChikabuZ answer (with `CFNumberGetType`) is currently my favourite: most accurate info we can get in the general case. – Cœur Sep 05 '18 at 13:25
-2

check object is of NSNumber type :

if([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]) { //NSNumber }

torap
  • 656
  • 6
  • 15