NSNumber: Boxing Primitive Values into Objects
Understand why Foundation collections require NSNumber to hold primitive C values, learn the standard ways to create and unbox an NSNumber, and avoid the classic comparison and type-extraction mistakes that trip up new Objective-C developers.
NSArray, NSDictionary, and NSSet — every Foundation collection covered in this course — can only hold Objective-C objects, never raw C primitives like int, float, double, or BOOL. That is a real limitation: a plain int is just a handful of bits sitting on the stack or in a struct, with no retain count, no isa pointer, and none of the machinery objects need to participate in a collection. NSNumber exists to bridge that gap. It is a Foundation class whose entire job is to wrap — box — a single primitive numeric or Boolean value inside a genuine Objective-C object, so that value can be stored in an NSArray, used as a dictionary value, or passed anywhere Foundation's object-only APIs expect an id.
Boxing: Turning a Primitive Into an Object
Boxing means wrapping a primitive value inside an object so it can be treated like any other object — retained, stored in a collection, compared with isEqual:, and so on. When you write NSNumber *n = @(42);, the compiler generates code equivalent to [NSNumber numberWithInt:42]: it takes the primitive int value 42 and allocates a small, genuinely immutable NSNumber object whose entire purpose is to remember that one value. From that point forward, n is a normal object with a retain count and an isa pointer, just like an NSString or a Person instance, and it can be stored directly in an NSArray, which a raw int never could be.
It's important to be clear about what boxing is not: it is not a cast, and it does not change how the primitive value behaves arithmetically. NSNumber *n = @(42); n + 1 does not compile as ordinary addition — n is a pointer to an object, not a numeric value, and the compiler will reject arithmetic performed directly on it. To get back to doing arithmetic, you must first unbox the value by calling one of NSNumber's numeric conversion methods, covered below.
The @() boxed-expression literal syntax works for any C numeric or Boolean expression, not just literal constants: @(someIntVariable), @(width * height), and @(isEnabled) (for a BOOL) are all valid and each produces the appropriately-typed NSNumber. This is the idiomatic, modern way to box a value in Objective-C; the older, more verbose class methods like numberWithInt: still work identically and appear frequently in existing code, but @() is preferred in code you write today.
Creating NSNumber Instances
The boxed literal syntax @(expression) is the standard, concise way to create an NSNumber: @(42) boxes an int, @(3.14) boxes a double, and @(YES) boxes a BOOL. The compiler infers the correct NSNumber factory method to call based on the expression's C type, so @(42) becomes numberWithInt:, while @(42L) (with the long suffix) becomes numberWithLong:, and @(42.0f) becomes numberWithFloat:.
The equivalent explicit class methods spell out the same operations without the shorthand: [NSNumber numberWithInt:42], [NSNumber numberWithDouble:3.14], [NSNumber numberWithBool:YES], and similarly numberWithFloat:, numberWithLong:, numberWithLongLong:, numberWithUnsignedInteger:, and so on for every C numeric type Foundation supports. Both spellings produce an identical kind of NSNumber object; @() is simply the terser, more modern way to write the same call, and this course uses it by default while still showing you the class-method form so you can recognize it in code you didn't write.
Numeric Conversion: Getting the Primitive Back Out
Once a value is boxed inside an NSNumber, you extract it back into a usable primitive with one of NSNumber's accessor methods: intValue, doubleValue, floatValue, boolValue, longValue, longLongValue, unsignedIntegerValue, and their siblings for every C numeric type Foundation boxes. NSNumber *score = @(95); int rawScore = score.intValue; unboxes the value back into a plain int that ordinary arithmetic works on again.
A subtlety worth understanding is that NSNumber will convert between numeric representations on demand, even if you ask for a different type than the one it was boxed with. NSNumber *n = @(3.9); int truncated = n.intValue; is completely legal and returns 3 — the fractional part is simply truncated, exactly as an ordinary C cast from double to int would behave — because internally, NSNumber stores its value with enough precision to answer any of these conversion methods and performs the appropriate C-level conversion when asked. This flexibility is convenient, but it means the compiler cannot catch a logically wrong extraction for you: calling doubleValue on an NSNumber that was boxed from a BOOL, for example, compiles and runs without error, silently producing 1.0 or 0.0 rather than any kind of warning that you might be asking for the wrong kind of value.
Comparison: isEqualToNumber:, compare:, and the == Trap
NSNumber provides two purpose-built comparison methods. isEqualToNumber: answers a simple yes/no question — do these two NSNumber objects represent the same numeric value? — and correctly compares across differently-boxed types, so [@(5) isEqualToNumber:@(5.0)] returns YES even though one was boxed as an int and the other as a double, because the comparison is done on the underlying numeric value, not the boxing type. compare: instead returns an NSComparisonResult (NSOrderedAscending, NSOrderedSame, or NSOrderedDescending), which is exactly what you need for ordering NSNumber values — for example, as the comparator passed to sortedArrayUsingSelector:@selector(compare:) when sorting an array of NSNumber objects numerically.
The classic, commonly-tested gotcha is using == directly on two NSNumber objects instead of either of these methods. NSNumber *a = @(5); NSNumber *b = @(5); if (a == b) tests whether a and b are literally the same object in memory — pointer equality — not whether they hold the same numeric value. Because a and b here were each created by a separate @(5) expression, they are, in general, two distinct NSNumber objects with different addresses, so a == b typically evaluates to NO even though both clearly represent the number 5. (In practice, the runtime does cache and reuse NSNumber instances for a small range of common small integer values as an internal optimization, which can make a == b appear to work by coincidence for values like @(5) — but relying on that behavior is exactly the trap, because it silently breaks the moment the values fall outside whatever range happens to be cached, or on a different runtime version.) The only reliable way to compare NSNumber values for numeric equality is isEqualToNumber: (or the general-purpose isEqual:, which NSNumber overrides to do the same numeric comparison).
• isEqualToNumber: — correct way to test numeric equality between two NSNumber objects. • compare: — returns NSOrderedAscending/Same/Descending, for ordering NSNumber values. • == on two NSNumber objects — pointer identity, not numeric value. Do not use it for equality.
Primitive Extraction Pitfalls
Because NSNumber converts freely between numeric representations on request, it is easy to accidentally extract the wrong kind of value without any compiler warning. A very concrete version of this mistake: NSNumber *flag = @(YES); float f = flag.floatValue; compiles cleanly and runs, producing 1.0f — technically 'correct' in the sense that YES is internally represented as the integer 1 and 1 converts cleanly to 1.0f, but almost certainly not what the calling code actually intended if it meant to test flag.boolValue as a true/false condition instead.
The safest habit is to extract values using the accessor that matches how the NSNumber was conceptually boxed — if you boxed a BOOL with @(isEnabled), extract it with .boolValue, not .intValue or .floatValue, even though those would technically 'work' and return a numerically plausible-looking result. This matters most when an NSNumber arrives from a source you don't fully control — parsed from JSON, for instance, where a value that looks like a small integer in the source data might have been boxed as either a BOOL or a genuine number depending on the parser's own conventions — since guessing the wrong accessor there produces a value that compiles, runs, and looks numerically reasonable while being conceptually wrong for how your code is about to use it.
NSNumber Boxing and Unboxing
@() infers the correct NSNumber factory method from the C type of its argument expression. Accessor methods (.intValue, .doubleValue, etc.) always succeed and convert on demand, even across mismatched types. Use isEqualToNumber: or compare:, never ==, to compare two NSNumber objects.
NSNumber *i = @(42);
NSNumber *d = @(3.14);
NSNumber *b = @(YES);
int rawInt = i.intValue;
double rawDouble = d.doubleValue;
BOOL rawBool = b.boolValue;
BOOL sameValue = [i isEqualToNumber:@(42)];
NSComparisonResult order = [i compare:d];Code Examples
NSArray can only hold objects, so score, gpa, and isHonorRoll are each boxed into an NSNumber with @() before being placed into the array literal. Reading them back out requires re-extracting the primitive with the matching *Value accessor. Expected result: Score: 95
NSArray *record = @[@(score), @(gpa), @(isHonorRoll)];
a and b are potentially distinct objects in memory even though both represent the value 5, so == compares object identity and is not a reliable equality test. isEqualToNumber: compares the actual boxed numeric values and is always correct.
if ([a isEqualToNumber:b]) { Common mistake: Relying on == happening to return YES for small cached integers is fragile -- it silently fails for larger values or non-integer types.
sortedArrayUsingSelector: needs a comparison method that returns an NSComparisonResult for each pair of elements; NSNumber's compare: method does exactly that, ordering the boxed values numerically rather than by object address. Expected result: ( 42, 67, 88, 95 )
[scores sortedArrayUsingSelector:@selector(compare:)];
int score = 95;
float gpa = 3.7f;
BOOL isHonorRoll = YES;
NSArray *record = @[@(score), @(gpa), @(isHonorRoll)];
NSNumber *scoreBox = record[0];
NSLog(@"Score: %d", scoreBox.intValue);NSNumber *a = @(5);
NSNumber *b = @(5);
if (a == b) {
NSLog(@"Pointer-equal (do not rely on this)");
}
if ([a isEqualToNumber:b]) {
NSLog(@"Numerically equal -- this is the correct check");
}NSArray<NSNumber *> *scores = @[@(88), @(42), @(95), @(67)];
NSArray<NSNumber *> *sorted =
[scores sortedArrayUsingSelector:@selector(compare:)];
NSLog(@"%@", sorted);Important Rules
Foundation collections (NSArray, NSDictionary, NSSet) can only hold objects -- primitives must be boxed into NSNumber first.
@(expression) is the standard, modern way to box any C numeric or BOOL expression into an NSNumber.
*Value accessors (intValue, doubleValue, boolValue, ...) always convert on demand, even across mismatched original types -- there is no compiler check that you're extracting the 'right' kind of value.
Use isEqualToNumber: (or isEqual:) to compare NSNumber values for numeric equality; use compare: for ordering. Never rely on == for value equality.
Extract values with the accessor that matches how the value was conceptually boxed (boolValue for a boxed BOOL, not floatValue or intValue), even though the mismatched accessor would technically still run.
JSON Parsing and Property Lists
NSNumber is the type you'll meet constantly when working with JSON parsed via NSJSONSerialization or with property lists: JSON numbers and booleans both come back as NSNumber instances inside the resulting NSDictionary/NSArray tree, indistinguishable at the type level from each other except by which *Value accessor you choose to call. Getting comfortable with boxing, unboxing, and the boolValue-vs-intValue distinction here is directly the skill you'll use every time you parse a server response or a plist configuration file.
if (numberA == numberB) { /* equal? */ } if ([numberA isEqualToNumber:numberB]) { /* equal */ }
== compares whether two NSNumber pointers refer to the literal same object in memory, not whether they represent the same numeric value. Two separately boxed NSNumbers holding 5 are not guaranteed to be the same object.
NSNumber *flag = @(YES); float f = flag.floatValue; if (f) { ... }
NSNumber *flag = @(YES); BOOL isSet = flag.boolValue; if (isSet) { ... }
Extracting a boxed BOOL with floatValue technically compiles and runs, but it obscures intent and invites subtle bugs; use the accessor matching how the value was conceptually boxed.
int total = numberArray + 1; // numberArray holds NSNumber objects int total = [numberArray[0] intValue] + 1;
An NSNumber is an object, not a primitive value; arithmetic cannot be performed directly on it. It must first be unboxed with an accessor like intValue before ordinary C arithmetic applies.
Why must a primitive int be boxed into an NSNumber before it can be stored in an NSArray? A. NSArray only accepts values smaller than 64 bits B. Foundation collections can only hold Objective-C objects, and a raw primitive has no isa pointer or retain count C. int values are always converted to NSString automatically D. Boxing is only required for negative numbers
B. Foundation collections can only hold Objective-C objects, and a raw primitive has no isa pointer or retain count — NSArray, NSDictionary, and NSSet all store objects (id), never raw C primitives. NSNumber wraps a primitive value inside a genuine object so it can participate in these collections.
What does if (numberA == numberB) actually test when numberA and numberB are both NSNumber objects? A. Whether the two NSNumber objects hold the same numeric value B. Whether the two pointers refer to the literal same object in memory (identity), not numeric equality C. A compile-time error, since == cannot be used on objects D. Whether both numbers are non-nil
B. Whether the two pointers refer to the literal same object in memory (identity), not numeric equality — == on two object pointers compares identity, not value. Two separately created NSNumber instances holding the same number are not guaranteed to be the same object; use isEqualToNumber: for value comparison.
What happens when you call .intValue on an NSNumber that was boxed from a double like @(3.9)? A. It raises a runtime exception because the types don't match B. It returns 3 -- the fractional part is truncated, just like an ordinary C cast from double to int C. It rounds to the nearest integer, returning 4 D. It returns 0 because NSNumber cannot convert between types
B. It returns 3 -- the fractional part is truncated, just like an ordinary C cast from double to int — NSNumber's accessor methods convert on demand between numeric representations; converting a boxed 3.9 to an int truncates the fractional part, giving 3, exactly as a plain C (int) cast would.
Which method should be used to sort an NSArray of NSNumber objects numerically in ascending order using sortedArrayUsingSelector:? A. @selector(isEqualToNumber:) B. @selector(compare:) C. @selector(intValue) D. @selector(description)
B. @selector(compare:) — compare: returns an NSComparisonResult (ascending/same/descending) appropriate for ordering, which is exactly the signature sortedArrayUsingSelector: needs.
An NSNumber was boxed as @(isEnabled) where isEnabled is a BOOL. Which extraction best reflects the original intent when reading it back? A. flag.floatValue, since it technically returns a numerically valid result B. flag.boolValue, matching how the value was conceptually boxed C. flag.description, since it returns a human-readable string D. Any accessor works identically and the choice doesn't matter
B. flag.boolValue, matching how the value was conceptually boxed — While NSNumber will convert to any requested type without error, extracting with the accessor matching the original conceptual type (boolValue for a boxed BOOL) keeps intent clear and avoids subtle bugs.
Given NSArray<NSNumber *> *temperatures = @[@(72), @(68), @(75), @(70)];, write code that computes the average temperature as a double by unboxing each NSNumber, and also find the highest temperature using compare: rather than manual arithmetic comparison.
double sum = 0; for (NSNumber *t in temperatures) { sum += t.doubleValue; } double average = sum / temperatures.count; NSNumber *highest = temperatures[0]; for (NSNumber *t in temperatures) { if ([highest compare:t] == NSOrderedAscending) { highest = t; } }
Loop with fast enumeration, accumulate a running sum using .doubleValue, and divide by temperatures.count for the average. For the max, track the current highest NSNumber and use [current compare:candidate] == NSOrderedAscending to decide whether to replace it.