4

I recently submitted a bug report to Apple about this, but I thought I would ask the question anyways in case I'm missing something obvious. In Objective-C, the following call works fine on a 64-bit system but throws an NSInvalidArgumentException on a 32-bit system:

[self setValue:@"true" forKey:@"flag"];

The "flag" property is a BOOL:

@property BOOL flag;

Further, the call works fine in Swift/32-bit, where the property is a Bool:

var flag: Bool = false

Similarly, this call works fine in Swift on a 64-bit system but throws NSInvalidArgumentException on the 32-bit system ("index" is an Int):

setValue("2", forKey: "index")

Yet it works fine in Objective-C/32-bit, where the property is an NSInteger.

I would have expected these calls to work correctly regardless of language or processor architecture. Does anyone have any insight into why they might not?

Greg Brown
  • 3,168
  • 1
  • 27
  • 37
  • The code you posted is not using a `BOOL` at all but two strings. The code you posted would not cause any error as you describe. – rmaddy Aug 21 '15 at 16:45
  • Can you give some more code examples where it works and doesn't work? As @rmaddy points out, your code shows nothing about BOOLs. – brandonscript Aug 21 '15 at 16:48
  • The "flag" property is defined as a BOOL. Are you not familiar with KVC? – Greg Brown Aug 21 '15 at 16:52
  • 1
    But in the code you posted, you are passing the value as the `NSString` of `@"true"`. That's not a `BOOL`. You can't pass an `NSString` to a `BOOL` property. That's the problem. – rmaddy Aug 21 '15 at 16:55
  • That's not correct. KVC handles the conversion from string to boolean just fine on 64-bit systems as well as in Swift/32-bit. It only fails in Objective-C/32-bit. – Greg Brown Aug 21 '15 at 16:57
  • 3
    No, it doesn't work. If it did it would work for both 32 and 64-bit systems. What I stated is correct. You need to pass (at least for Objective-C) a `BOOL` value, not an `NSString`. But you can't pass a `BOOL` since it isn't an object type. Try passing the equivalent `NSNumber` value for `YES` which would be: `[self setValue:@YES forKey:@"flag"];`. – rmaddy Aug 21 '15 at 16:59
  • You can try to tell me that it doesn't work on 64-bit systems, but it does. :-) Does anyone want to offer a helpful response? Because this one doesn't seem to be going anywhere. – Greg Brown Aug 21 '15 at 17:00
  • Did you try using `@YES` (note that is an `NSNumber`, not an `NSString`) instead of `@"true"? And explain why you think passing an `NSString` to a `BOOL` is correct? – rmaddy Aug 21 '15 at 17:04
  • Oh, and incidentally, KVC also handles string to number conversions transparently, on both 64 and 32 bit systems. It only fails for BOOLs, possibly because BOOL is defined as a char on 32-bit systems and a bool on 64-bit systems. – Greg Brown Aug 21 '15 at 17:07
  • In my example code, I could pass @YES instead of "true", but in my actual application I only have a string. And again, this works fine for every data type except for BOOL on 32-bit systems. – Greg Brown Aug 21 '15 at 17:10
  • 3
    The KVC docs talk all about how primitive numeric types (including `BOOL`) are handled using `NSNumber`. If you want your code to work properly, convert your true/false string to the proper `NSNumber`. – rmaddy Aug 21 '15 at 17:11
  • Based on my experience, I don't think that converting the string to an NSNumber should be required. It isn't required for any other data types. I guess I will have to wait for a response from Apple. Thanks for your help. – Greg Brown Aug 21 '15 at 17:12
  • 2
    See http://stackoverflow.com/questions/3663266/kvc-string-conversion-not-working-for-bool-value – rmaddy Aug 21 '15 at 17:23
  • 1
    I don't think that KVC does any string to number or string to bool conversions transparently. If that worked then by pure chance. As rmaddy said, you have to pass a NSNumber. In *Swift* you could also pass a Bool or Int because that would be converted to NSNumber. – Martin R Aug 21 '15 at 17:31
  • I understand what you are saying. The KVC docs don't explicitly state that setValue:forKey: performs type coercion. Yet, based on my own empirical observations, this is exactly what happens. Also see https://www.bignerdranch.com/blog/inside-the-bracket-part-6-using-the-runtime-api/ ("Experimentally, it looks like KVC is converting between strings and numbers transparently"). I am attempting to confirm if this is a documentation oversight or not. – Greg Brown Aug 21 '15 at 17:34
  • @MartinR - I highly doubt that this sort of type conversion simply happens by accident. :-) Someone at Apple wrote code to do this. It just isn't documented. Again, it works for other data types, just not BOOL on 32-bit systems. Try it for yourself if you won't take my word for it. – Greg Brown Aug 21 '15 at 17:35
  • @remus So you think it's an "accident" that KVC will happily convert, say, the string "4.0" to a CGFloat with value 4.0 for me (which it, in fact does)? Again, that seems extremely unlikely. KVC is DEFINITELY performing string-to-number conversions. It just isn't documented. I'm planning to submit a documentation bug to Apple to get clarification on this. – Greg Brown Aug 21 '15 at 17:48
  • @remus Again, with the exception of BOOL on 32-bit systems, it doesn't SOMETIMES work this way - it ALWAYS works this way. It isn't an accident. It's undocumented. – Greg Brown Aug 21 '15 at 17:50
  • @remus Specifying @"false" sets the value to false, as expected. – Greg Brown Aug 21 '15 at 18:09

2 Answers2

5

The answer is there in the comments if you combine them all...

setValue:forKey: does not require an NSNumber/NSValue for primitive-typed properties, but you would normally pass one.

The observed issued is not down to 64-bit vs. 32-bit either, the example code can fail on 64-bit systems as well.

It is all down to the nature of BOOL and whether it is a char or a bool,as a comment suggests - and that depends on a number of things (from Xcode 6.4 running on 10.10.5):

/// Type to represent a boolean value.
#if !defined(OBJC_HIDE_64) && TARGET_OS_IPHONE && __LP64__
typedef bool BOOL;
#else
typedef signed char BOOL; 
// BOOL is explicitly signed so @encode(BOOL) == "c" rather than "C" 
// even if -funsigned-char is used.
#endif

setValue:forKey when setting a primitive typed property of <type> calls - <type>Value on whatever object it is passed.

If BOOL is a char it calls - charValue, and NSString has no such method - so fail.

If BOOL is bool it calls - boolValue, and NSString has that so all is good.

Simple fix:

@property bool flag;

and that should work everywhere and have the added bonus that flag will always be true/false, YES/NO, 1/0 and not one of the other 254 possibilities that char can be.

Just think how different things would be if the designers of C didn't skimp and actually included a real boolean type from the start...

CRD
  • 52,522
  • 5
  • 70
  • 86
  • "setValue:forKey when setting a primitive typed property of calls - Value on whatever object it is passed" - is this from the KVC docs? – Greg Brown Aug 21 '15 at 18:05
  • This does appear to be correct. From Apple's documentation: "...setValue:forKey: determines the data type required by the appropriate accessor or instance variable for the specified key. If the data type is not an object, then the value is extracted from the passed object using the appropriate -Value method." – Greg Brown Aug 21 '15 at 18:13
  • So this explains perfectly why this fails when BOOL is defined as a char. I'm still not sure why it fails for the Swift Int though. I'll have to look into that a bit more (unless someone already knows the answer and is willing to share it). – Greg Brown Aug 21 '15 at 18:18
  • Answer to Swift issue: Swift's Int type appears to be a long (4 bytes) on 32-bit systems and a long long (8 bytes) on 64-bit systems. But NSString doesn't define a longValue method, so this fails. – Greg Brown Aug 21 '15 at 18:31
1

As @CRD mention above, BOOL is a typedef of char on 32bit devices, and NSString has no charValue method, so we can add a simple NSString category to fix it, like this.

#import "NSString+DCCharValue.h"

@implementation NSString (DCCharValue)

#if !defined(OBJC_HIDE_64) && TARGET_OS_IPHONE && __LP64__
#else
- (BOOL)charValue {
    return [self boolValue];
}
#endif

@end
yxjxx
  • 21
  • 4