Search code examples
objective-cios8nsnumber

ObjC NSNumber stores pointer to Object instead of returned Value


I wrote my own Objective C class for dealing with 2D Vectors.

@interface Vector2D : NSObject {
    struct _vecdata {
        double x;
        double y;
        double x_norm;
        double y_norm;
    } _data;
}

@property(nonatomic) double x; //custom getter + setter
@property(nonatomic) double y; //custom getter + setter
@property(nonatomic, readonly) double xNormed; //custom getter
@property(nonatomic, readonly) double yNormed; //custom getter

+ (id)vectorWithX:(double)x_ext andY:(double)y_ext;
- (id)initWithX:(double)x_ext andY:(double)y_ext;

[...]
- (double)length;

@end

#import <math.h>
@implementation Vector2D

+ (id)vectorWithX:(double)x_ext andY:(double)y_ext{
    return [[self alloc] initWithX:x_ext andY:y_ext];
}

- (id)initWithX:(double)x_ext andY:(double)y_ext {
    if ((self = [super init])) {
        _data.x = x_ext;
        _data.y = y_ext;
        [self normalize];
    }
    return self;
}

- (void)setX:(double)x {
    _data.x = x;
    [self normalize];
}

- (double)x {
    return _data.x;
}

- (double)xNormed {
    return _data.x_norm;
}

//setters and getter for y omitted (same as for x)

[...]

- (void)normalize {
    double abs = [self length];
    if (abs != 0) {
        _data.x_norm = _data.x/abs;
        _data.y_norm = _data.y/abs;
    }
}

- (double)length {
    return sqrt(_data.x*_data.x + _data.y*_data.y);
}

@end

Now I need to wrap the result of a call to an Vector2D instances length call to an NSNumber.

NSNumber* aNSNum = @([[Vector2D vectorWithX:someXValue andY:someYValue] length]);

Since I display the value of aNSNum later on, I noticed, that all the values (of all NSNumbers created that way) are around 1.40537252E+14 (since i call [aNSNum floatValue] later on). So I recompiled and rerun the app and suddenly all the values were around 4.73280427E+14.

So I wondered, since the length of the Vectors should be in the range 0 to 20000, but not more. I started playing around in the debugger to understand whats happening and got the following results:

(lldb) po [[Vector2D vectorWithX:someXValue andY:someYValue] length]
0x0000000000000001

(lldb) po (double)[[Vector2D vectorWithX:someXValue andY:someYValue] length]
84.693565280958623

(lldb) po (unsigned long)[[Vector2D vectorWithX:someXValue andY:someYValue] length]
1

(lldb) po @([[Vector2D vectorWithX:someXValue andY:someYValue] length])
84.69356528095862

(lldb) po @((double)[[Vector2D vectorWithX:someXValue andY:someYValue] length])
84.69356528095862

(lldb) po @((unsigned long)[[Vector2D vectorWithX:someXValue andY:someYValue] length])
1

(lldb) po aNSNum
140537282189312

(lldb) po [aNSNum floatValue]
1.40537286E+14

(lldb) po (unsigned long)[aNSNum doubleValue]
<Vector2D: 0x7fd162c85800>

(lldb) po (double)[(unsigned long)[aNSNum doubleValue] length]
84.693565280958623

So the really intresting part is about the second last line. So why is the pointer adress to the Vector2D Object stored in the NSNumber and not the value of the return value of the call to -length?

Since then I tried to change the bogus line of code to the following variants: (with no sucess)

  • NSNumber* aNSNum = @((double)[[Vector2D vectorWithX:someXValue andY:someYValue] length]);
  • NSNumber* aNSNum = @([((Vector2D *)[Vector2D vectorWithX:someXValue andY:someYValue]) length]);
  • NSNumber* aNSNum = @((double)[((Vector2D *)[Vector2D vectorWithX:someXValue andY:someYValue]) length]);

What worked so far was the following:

static SEL lengthSEL;
static IMP lengthIMP;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    lengthSEL = @selector(length);
    lengthIMP = class_getMethodImplementation([Vector2D class], lengthSEL);
});
Vector2D* vec = [Vector2D vectorWithX:someXValue andY:someYValue];
double len = ((double (*) (id,SEL))lengthIMP)(vec,lengthSEL);
NSNumber* aNSNum = @(len);

But hope someone could help me to bring it back again to a one-liner. Or has a clue where it gets wrong...


Solution

  • Edit: The compiler gets confused because the return type of

    + (id)vectorWithX:(double)x_ext andY:(double)y_ext;

    is id; It does not know til runtime which length method it will get. Change the declaration to

    + (instancetype )vectorWithX:(double)x_ext andY:(double)y_ext;

    and watch how much nicer things get. Learn more at apple docs


    You are relying on the literal syntax @(someNumber) to detect the type of someNumber and handle it properly, in this case, the (double) return value of your length method. The rules for number literals are here.

    I think the safe thing to do, rather than typecast your variable and hope that the compiler picks up on it, is to create the number with explicit typing, that is,

    NSNumber* aNSNum = [NSNumber numberWithDouble:[[Vector2D vectorWithX:someXValue andY:someYValue] length]];    
    

    NSNumber literals are great shorthand for constants and the like, but this seems a case where they make the code harder to understand instead of easier.