1

I tried to figure out this code referencing: Cocoa: Dictionary with enum keys?

+ (NSValue*)valueWithReference:(id)target
{
    return [NSValue valueWithBytes:&target objCType:@encode(id*)];
}

And,

[table setObject:anObject forKey:[NSValue valueWithReference:keyObject]];

But it feels something not good. Any recommendations?

Community
  • 1
  • 1
eonil
  • 83,476
  • 81
  • 317
  • 516

3 Answers3

9

You're absolutely right it's not good.

For one, you're encoding the wrong type (it should be @encode(id), not @encode(id*)), but in most cases this shouldn't cause a big problem.

The bigger problem is that this completely ignores memory management. The object won't be retained or copied. If some other code releases it, it could just disappear, and then your dictionary key will be a boxed pointer to garbage or even a completely different object. This is basically the world's most advanced dangling pointer.

You have two good options:

  1. You could either add NSCopying to the class or create a copyable subclass.

    • This option will only work for objects that can meaningfully be copied. This is most classes, but not necessarily all (e.g. it might be bad to have multiple objects representing the same input stream)
    • Implementing copying can be a pain even for classes where it makes sense — not difficult, per se, but kind of annoying
  2. You could instead create the dictionary with the CFDictionary API. Since Core Foundation types don't have a generic copy function, CFDictionary just retains its keys by default (though you can customize its behavior however you like). But CFDictionary is also toll-free bridged with NSDictionary, which means that you can just cast a CFDictionaryRef to an NSDictionary* (or NSMutableDictionary*) and then treat it like any other NSDictionary.

    • This means that the object you're using as a key must not change (at least not in a way that affects its hash value) while it's in the dictionary — ensuring this doesn't happen is why NSDictionary normally wants to copy its keys
Chuck
  • 234,037
  • 30
  • 302
  • 389
  • CFDictionary option great! Thanks! – eonil Aug 18 '10 at 06:15
  • If I use `[NSValue valueWithBytes:&&target objCType:@encode(id*)];` to make something like `NSNotificationCenter` (doesn't retain/copy observer), will it be fine? (I added 1 more `&` operator) – eonil Aug 18 '10 at 06:19
  • @Eonil: `&&target` isn't a valid C expression. If you were going to do that, you'd want to use `&target` and `@encode(id)`. But yes, you could do that. Still not sure it's the ideal structure, but it ought to work. – Chuck Aug 18 '10 at 06:35
  • Chuck: I posted a different (and, in my humble opinion, simpler for people who doesn't know about the CF API) solution for this issue. Could you kindly have a look to see if you see any problems with my alternative? – Ricardo Sanchez-Saez Feb 23 '12 at 20:44
  • if the CFDictionaryRef is based on hash, we could use – JakubKnejzlik Feb 27 '13 at 14:05
1

For the later reference.

Now I know that there are some more options.

  1. Override methods in NSCopying protocol, and return the self instead of copying itself. (you should retain it if you are not using ARC) Also you ensure the object to always return same value for -hash method.

  2. Make a copyable simple container class holds strong reference to the original key object. The container is copyable but, it just passes original key when it being copied. Override equality/hash methods also to match semantics. Even just an instance of NSArray contains only the key object works well.

Method #1 looks pretty safe but actually I'm not sure that's safe. Because I don't know internal behavior of NSDictionary. So I usually use #2 way which is completely safe in Cocoa convention.

Update

Now we Have NSHashTable and NSMapTable also in iOS since version 6.0.

eonil
  • 83,476
  • 81
  • 317
  • 516
0

I'm not 100% sure about the correctness of this solution, but I'm posting it just in case.

If you do not want to use a CFDictionary, maybe you could use this simple category:

@implementation NSMutableDictionary(NonCopyableKeys)
- (void)setObject:(id)anObject forNonCopyableKey:(id)aKey {
    [self setObject:anObject forKey:[NSValue valueWithPointer:aKey]];
}

- (id)objectForNonCopyableKey:(id)aKey {
    return [self objectForKey:[NSValue valueWithPointer:aKey]];
}

- (void)removeObjectForNonCopyableKey:(id)aKey {
    [self removeObjectForKey:[NSValue valueWithPointer:aKey]];
}
@end

This is a generalization of a similar method I saw online (can't find the original source) for using an NSMutableDictionary that can store objects with UITouch keys.

The same restriction as in Chuck's answer applies: the object you're using as a key must not change in a way that affects its hash value and must not be freed while it's in the dictionary .

Also make sure you don't mix -(void)setObject:(id)anObject forNonCopyableKey:(id)aKey and - (id)objectForKey:(id)aKey methods, as it won't work (the latter will return nil).

This seems to work fine, but there might be some unwanted side effects that I am not thinking of. If anybody finds out that this solution has any additional problems or caveats, please comment.

Ricardo Sanchez-Saez
  • 9,466
  • 8
  • 53
  • 92
  • 1
    This won't work right, because objects have special memory management semantics. Since this treats the objects as opaque pointers, this this potentially leaves you with a dictionary full of dangling pointer values. For example, `[dict setObject:@"something" forNonCopyableKey:[NSPipe pipe]]` will eventually lead to your dictionary either working incorrectly or flat-out crashing your program. – Chuck Feb 23 '12 at 20:57
  • Actually, having reread the question now, this is exactly the same technique Eonil was asking about (`valueWithPointer:` is essentially a convenience wrapper around the `valueWithBytes:objCType:` method Eonil used). – Chuck Feb 24 '12 at 03:48
  • Chuck: Thanks for your clarifications. I acknowledge this won't work with autoreleased objects. But, wouldn't it work with retained objects? (e.g., `NSPipe *pipe = [[NSPipe pipe] retain]; [dict setObject:@"something" forNonCopyableKey:pipe]`. It is my intuition that this could work properly as long as you don't do `[pipe release]` while your are using the `NSDictionary`. – Ricardo Sanchez-Saez Feb 27 '12 at 11:13