There's a really strange behavior going on with float / double / CGFloat casting on the result of performSelector:
Why does this work?
BOOL property = (BOOL)[self.object performSelector:@selector(boolProperty)];
NSInteger property = (NSInteger) [self.object performSelector:@selector(integerProperty)];
And this doesn't
CGFloat property = (CGFloat) [self.object performSelector:@selector(floatProperty)];
At first I tried to do this:
CGFloat property = [[self.object performSelector:@selector(floatProperty)] floatValue];
But I ended up getting an EXC_BAD_ACCESS runtime error. I already figured out a hack to solve this issue but I would like to understand why it works with Integer and Bool and not with floating point types.
My hack:
@implementation NSObject (AddOn)
-(CGFloat)performFloatSelector:(SEL)aSelector
{
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[[self class] instanceMethodSignatureForSelector:aSelector]];
[invocation setSelector:aSelector];
[invocation setTarget:self];
[invocation invoke];
CGFloat f = 0.0f;
[invocation getReturnValue:&f];
return f;
}
@end
Then:
CGFloat property = [self.object performFloatSelector:@selector(floatProperty)];
The performSelector
method only supports methods that return nothing or an object. This is covered in its documentation where it states:
For methods that return anything other than an object, use
NSInvocation
.
Your "hack" is just the correct way of calling a selector which returns a non-object.
The reason why it appears to work for integer and boolean methods is to do with the way values are returned by methods. The return type of performSelector
is id
, an object pointer type. Integer-like values; which includes NSInteger
, BOOL
and object pointers; are often returned in a general purpose register, however floating point values are usually returned in a floating point register. The compiler will always load the result from the register used for returning id
values and then perform any action required by the cast. For integer and boolean values the loaded value is probably correct and the cast is a no-op in those cases, so everything (appears to) work. For floating point values the loaded value is incorrect - it is not the value the called method has returned as that is in a different register.
Note calling non-object, non-void, selectors with performSelector
can cause memory issues under ARC - the compiler will usually warn if this might be the case.
The NSInvocation
way of calling a selector works for non-object return types as one of its arguments is the method signature itself. Using the signature NSInvocation
is able to determine the type of the return value and how it is returned, and therefore correctly return it to the caller.
HTH
If you don't want to use NSInvocation
, you can use the objc_msgSend
functions directly (this is what the compiler translates message calls to):
BOOL (*f)(id, SEL) = (BOOL (*)(id, SEL))objc_msgSend;
BOOL property = f(self.object, @selector(boolProperty));
NSInteger (*f)(id, SEL) = (NSInteger (*)(id, SEL))objc_msgSend;
NSInteger property = f(self.object, @selector(integerProperty));
CGFloat (*f)(id, SEL) = (CGFloat (*)(id, SEL))objc_msgSend;
CGFloat property = f(self.object, @selector(floatProperty));
Note that you must cast it to a function pointer with the exact signature of the method to call it correctly; here the methods take no parameters, so the function only has the 2 "hidden" parameters, self
, and _cmd
. If the methods took parameters, you would have to add more parameters to the function pointer type.
Also, note that for struct
returns, you will need to use objc_msgSend_stret
in place of objc_msgSend
, but everything else exactly the same as above.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With