可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I'm setting values for properties of my NSManagedObject
, these values are coming from a NSDictionary
properly serialized from a JSON file. My problem is, that, when some value is [NSNull null]
, I can't assign directly to the property:
fight.winnerID = [dict objectForKey:@"winner"];
this throws a NSInvalidArgumentException
"winnerID"; desired type = NSString; given type = NSNull; value = <null>;
I could easily check the value for [NSNull null]
and assign nil
instead:
fight.winnerID = [dict objectForKey:@"winner"] == [NSNull null] ? nil : [dict objectForKey:@"winner"];
But I think this is not elegant and gets messy with lots of properties to set.
Also, this gets harder when dealing with NSNumber
properties:
fight.round = [NSNumber numberWithUnsignedInteger:[[dict valueForKey:@"round"] unsignedIntegerValue]]
The NSInvalidArgumentException
is now:
[NSNull unsignedIntegerValue]: unrecognized selector sent to instance
In this case I have to treat [dict valueForKey:@"round"]
before making an NSUInteger
value of it. And the one line solution is gone.
I tried making a @try @catch block, but as soon as the first value is caught, it jumps the whole @try block and the next properties are ignored.
Is there a better way to handle [NSNull null]
or perhaps make this entirely different but easier?
回答1:
It might be a little easier if you wrap this in a macro:
#define NULL_TO_NIL(obj) ({ __typeof__ (obj) __obj = (obj); __obj == [NSNull null] ? nil : obj; })
Then you can write things like
fight.winnerID = NULL_TO_NIL([dict objectForKey:@"winner"]);
Alternatively you can pre-process your dictionary and replace all NSNull
s with nil
before even trying to stuff it into your managed object.
回答2:
Ok, I've just woke up this morning with a good solution. What about this:
Serialize the JSON using the option to receive Mutable Arrays and Dictionaries:
NSMutableDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:_receivedData options:NSJSONReadingMutableContainers error:&error]; ...
Get a set of keys that have [NSNull null]
values from the leafDict:
NSSet *nullSet = [leafDict keysOfEntriesWithOptions:NSEnumerationConcurrent passingTest:^BOOL(id key, id obj, BOOL *stop) { return [obj isEqual:[NSNull null]] ? YES : NO; }];
Remove the filtered properties from your Mutable leafDict:
[leafDict removeObjectsForKeys:[nullSet allObjects]];
Now when you call fight.winnerID = [dict objectForKey:@"winner"];
winnerID is automatically going to be (null)
or nil
as opposed to <null>
or [NSNull null]
.
Not relative to this, but I also noticed that it is better to use a NSNumberFormatter
when parsing strings to NSNumber, the way I was doing was getting integerValue
from a nil string, this gives me an undesired NSNumber of 0
, when I actually wanted it to be nil.
Before:
// when [leafDict valueForKey:@"round"] == nil fight.round = [NSNumber numberWithInteger:[[leafDict valueForKey:@"round"] integerValue]] // Result: fight.round = 0
After:
__autoreleasing NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init]; fight.round = [numberFormatter numberFromString:[leafDict valueForKey:@"round"]]; // Result: fight.round = nil
回答3:
I wrote a couple of category methods to strip nulls from a JSON-generated dictionary or array prior to use:
@implementation NSMutableArray (StripNulls) - (void)stripNullValues { for (int i = [self count] - 1; i >= 0; i--) { id value = [self objectAtIndex:i]; if (value == [NSNull null]) { [self removeObjectAtIndex:i]; } else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) { if (![value respondsToSelector:@selector(setObject:forKey:)] && ![value respondsToSelector:@selector(addObject:)]) { value = [value mutableCopy]; [self replaceObjectAtIndex:i withObject:value]; } [value stripNullValues]; } } } @end @implementation NSMutableDictionary (StripNulls) - (void)stripNullValues { for (NSString *key in [self allKeys]) { id value = [self objectForKey:key]; if (value == [NSNull null]) { [self removeObjectForKey:key]; } else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) { if (![value respondsToSelector:@selector(setObject:forKey:)] && ![value respondsToSelector:@selector(addObject:)]) { value = [value mutableCopy]; [self setObject:value forKey:key]; } [value stripNullValues]; } } } @end
It would be nice if the standard JSON parsing libs had this behaviour by default - it's almost always preferable to omit null objects than to include them as NSNulls.
回答4:
Another method is
-[NSObject setValuesForKeysWithDictionary:]
In this scenario you could do
[fight setValuesForKeysWithDictionary:dict];
In the header NSKeyValueCoding.h it defines that "Dictionary entries whose values are NSNull result in -setValue:nil forKey:key
messages being sent to the receiver.
The only downside is you will have to transform any keys in the dictionary to keys that are in the receiver. i.e.
dict[@"winnerID"] = dict[@"winner"]; [dict removeObjectForKey:@"winner"];
回答5:
I was stuck with the same problem, found this post, did it in a slightly different way.Using category only though -
Make a new category file for "NSDictionary" and add this one method -
@implementation NSDictionary (SuperExtras) - (id)objectForKey_NoNSNULL:(id)aKey { id result = [self objectForKey:aKey]; if(result==[NSNull null]) { return nil; } return result; } @end
Later on to use it in code, for properties that can have NSNULL in them just use it this way -
newUser.email = [loopdict objectForKey_NoNSNULL:@"email"];
Thats it