Skip to content

Instantly share code, notes, and snippets.

@swillits
Last active May 2, 2023 13:48
Show Gist options
  • Save swillits/40621a80fef63b5f8552 to your computer and use it in GitHub Desktop.
Save swillits/40621a80fef63b5f8552 to your computer and use it in GitHub Desktop.
Obj-C NSDictionary Literal-like syntax allowing nil values
/*
---------------------------------------------------------------------------------------
Obj-C Literal Dictionary Syntax - Multiple reasons for allowing nil values
Radar 19747372
---------------------------------------------------------------------------------------
The obj-c literal syntax for dictionaries does not allow nils.
@{key : nil}; // Exception (and compiler error)
This makes it impossible to conveniently create a dictionary where a value may more may
not be nil.
- (NSDictionary *)doSomething
{
NSData * data = ....;
NSString * metadata1 = ...;
NSString * metadata2 = ...;
return @{
@"data" : data,
@"metadata1" : metadata1,
@"metadata2" : metadata2
};
}
If data, metadata1, or metadata2 is nil, there'll be a runtime exception. Thus we have
to rewrite it, the most brief form of which being:
- (NSDictionary *)doSomething
{
NSData * data = ....;
NSString * metadata1 = ...;
NSString * metadata2 = ...;
NSMutableDictionary * mdict = [NSMutableDictionary dictionary];
if (data) [mdict setObject:data forKey:@"data"];
if (metadata1) [mdict setObject:metadata1 forKey:@"metadata1"];
if (metadata2) [mdict setObject:metadata2 forKey:@"metadata2"];
return [mdict copy];
}
The above case happens **frequently**, which alone makes it really strange that the
Obj-C literal syntax does not allow nil values.
Another case where accepting nil values would be useful, is in dynamically excluding a
k/v pair based on some flag, while maintaining code brevity. For instance:
- (NSDictionary *)settingsForSomethingWithOptions:(BOOL)option
{
Foo * foo = ...;
NSMutableDictionary * mdict = [NSMutableDictionary dictionary];
[mdict setObject:foo.something forKey:@"something"];
[mdict setObject:foo.something2 forKey:@"something2"];
[mdict setObject:foo.something3 forKey:@"something3"];
[mdict setObject:foo.something4 forKey:@"something4"];
[mdict setObject:whoKnows forKey:@"whatever"];
if (option) {
[mdict setObject:foo.blah forKey:@"blah"];
}
return [mdict copy];
}
The above could be rewritten far more briefly if the literal syntax supported accepting
nil values:
- (NSDictionary *)settingsForSomethingWithOptions:(BOOL)option
{
Foo * foo = ...;
return @{
@"something" : foo.something,
@"something2" : foo.something2,
@"something3" : foo.something3,
@"something4" : foo.something4,
@"whatever" : whoKnows,
@"blah" : (option ? foo.blah : nil)
};
}
This is less code, more flexible, and easier to understand.
There's an argument to be made that doing @{key : nil}.allKeys logically must be
expected to be @[key], but we're all adults here, and to anyone with a brain, it'd be
easily understood that this wouldn't be the case, as the dictionary simply wouldn't
have any entry for the key.
Why does the Obj-C literal not accept nil values? We all know what it would mean, so
why doesn't it work that way?
Changing it now wouldn't break any existing code because all previously written valid
code is still valid. The only things that would change are:
1) Exceptions on nil values are no longer thrown (and developers all over the world
would rejoice).
2) A likely negligible performance impact from having to test if the value is nil.
3) To implement this with maximum performance, either:
a) NSDictionary would need a new method akin to initWithObjects:forKeys:count:
that accepted nil values,. The impact of having a new method would mean
support for nil values would be limited to applications with a deployment
target that has an implementation for it.
b) OR, if the above method did not exist, the compiler would provide a built-in
implementation for it.
These are all reasonable things, and the improvement in actually being able to use the
literal syntax for almost every purpose, would well make up for it.
Furthermore, I still don't understand why there isn't support for a mutable variants:
@m{} @m[]
=======================================================================================
*/
// As a workaround, here's one solution. It's a lot of code, but it's performant, at
// only ~10% slower than a literal when using <= 16 kv pairs, and 20% when using more
// than 16. It's 3x *faster* than using a mutable dictionary.
#define NSDICT(firstKey, ...) NSDictionaryWithKeysAndValues(firstKey, __VA_ARGS__)
static __attribute__ ((sentinel)) NSDictionary * NSDictionaryWithKeysAndValues(id firstKey, ...)
{
va_list kvl;
va_start(kvl, firstKey);
size_t dyn_capacity;
size_t dyn_count;
__unsafe_unretained id * dyn_keys;
__unsafe_unretained id * dyn_values;
id key, value;
// Do up to 16 using static allocation
{
NSUInteger static_capacity = 16;
__unsafe_unretained id keys[static_capacity];
__unsafe_unretained id values[static_capacity];
NSUInteger count = 0;
key = firstKey;
value = va_arg(kvl, id);
do {
if (value) {
keys[count] = key;
values[count] = value;
count++;
}
key = va_arg(kvl, id);
if (!key) {
va_end(kvl);
return [NSDictionary dictionaryWithObjects:values forKeys:keys count:count];
}
value = va_arg(kvl, id);
if (count == static_capacity) {
dyn_capacity = static_capacity * 2;
dyn_count = static_capacity;
dyn_keys = (__unsafe_unretained id *)malloc(dyn_capacity * sizeof(id));
dyn_values = (__unsafe_unretained id *)malloc(dyn_capacity * sizeof(id));
memcpy(dyn_keys, keys, static_capacity * sizeof(id));
memcpy(dyn_values, values, static_capacity * sizeof(id));
break;
}
} while (1);
}
// For > 16 entries
do {
if (value) {
dyn_keys[dyn_count] = key;
dyn_values[dyn_count] = value;
dyn_count++;
if (dyn_count == dyn_capacity) {
dyn_capacity += 16;
dyn_keys = (__unsafe_unretained id *)realloc(dyn_keys, dyn_capacity * sizeof(id));
dyn_values = (__unsafe_unretained id *)realloc(dyn_values, dyn_capacity * sizeof(id));
}
}
key = va_arg(kvl, id);
if (!key) break;
value = va_arg(kvl, id);
} while (1);
va_end(kvl);
NSDictionary * dict = [NSDictionary dictionaryWithObjects:dyn_values forKeys:dyn_keys count:dyn_count];
free(dyn_keys);
free(dyn_values);
return dict;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
id value = nil;
id dict = NSDICT(@"key", value, @"key2", @"value2", nil);
NSLog(@"%@", dict); // { key2 : value2 }
}
return 0;
}
@outis
Copy link

outis commented Mar 3, 2019

Note this is a limitation of NSDictionary, not dictionary literals, as the former doesn't allow nil values or keys. As mentioned in the documentation for NSDictionary, you can use NSNull. Combine this with the null coalescing operator (aka the Elvis operator) in dictionary literals:

- (NSDictionary *)doSomething
{
    NSData * data = ....;
    NSString * metadata1 = ...;
    NSString * metadata2 = ...;

    return @{
        @"data" : data,
        @"metadata1" : metadata1 ?: [NSNull null],
        @"metadata2" : metadata2 ?: [NSNull null]
    };
}

For NSUserDefaults, you'd still have to use the test-and-set approach rather than literals using NSNull, as NSUserDefaults only accepts a limited number of value types, and NSNull isn't one of them.

@schmidt9
Copy link

schmidt9 commented May 2, 2023

NSDictionary category version allowing NSNull values

/**
 Implementation allowing @c nil values which will be replaced with @c NSNull
 */
+ (instancetype)dictionaryWithKeysAndValues:(id)firstKey, ... NS_REQUIRES_NIL_TERMINATION;
{
    va_list kvl;
    va_start(kvl, firstKey);
    
    size_t dyn_capacity;
    size_t dyn_count;
    __unsafe_unretained id * dyn_keys;
    __unsafe_unretained id * dyn_values;
    id key, value;
    
    // Do up to 16 using static allocation
    {
        NSUInteger static_capacity = 16;
        __unsafe_unretained id keys[static_capacity];
        __unsafe_unretained id values[static_capacity];
        NSUInteger count = 0;
        
        key = firstKey;
        value = va_arg(kvl, id);
        
        do {
            if (!value) {
                value = [NSNull null];
            }
            
            keys[count] = key;
            values[count] = value;
            count++;
            
            key = va_arg(kvl, id);
            
            if (!key) {
                va_end(kvl);
                return [NSDictionary dictionaryWithObjects:values forKeys:keys count:count];
            }
            
            value = va_arg(kvl, id);
        
            if (count == static_capacity) {
                dyn_capacity = static_capacity * 2;
                dyn_count  = static_capacity;
                dyn_keys   = (__unsafe_unretained id *)malloc(dyn_capacity * sizeof(id));
                dyn_values = (__unsafe_unretained id *)malloc(dyn_capacity * sizeof(id));
                
                memcpy(dyn_keys, keys, static_capacity * sizeof(id));
                memcpy(dyn_values, values, static_capacity * sizeof(id));
                break;
            }
            
        } while (1);
    }
    
    // For > 16 entries
    do {
        if (!value) {
            value = [NSNull null];
        }
        
        dyn_keys[dyn_count] = key;
        dyn_values[dyn_count] = value;
        dyn_count++;
        
        if (dyn_count == dyn_capacity) {
            dyn_capacity += 16;
            dyn_keys = (__unsafe_unretained id *)realloc(dyn_keys, dyn_capacity * sizeof(id));
            dyn_values = (__unsafe_unretained id *)realloc(dyn_values, dyn_capacity * sizeof(id));
        }

        key = va_arg(kvl, id);
        
        if (!key) {
            break;
        }
        
        value = va_arg(kvl, id);
    } while (1);
    
    va_end(kvl);
    
    NSDictionary * dict = [NSDictionary dictionaryWithObjects:dyn_values forKeys:dyn_keys count:dyn_count];
    free(dyn_keys);
    free(dyn_values);
    
    return dict;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment