Is there a clean way to use pointers (ids) as keys in an NSMutableDictionary?
Asked Answered
C

5

5

I'm using NSMutableDictionary to store what is effectively a descriptor for certain classes, because I'd rather not waste the memory of adding the descriptor to every instance of classes, since only a very small subset of 1000s of objects will have the descriptor.

Unfortunately, given:

MyClass* p = [MyClass thingy];
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
NSString* description = @"blah"; //does this work? If not I'm just simplifying for this example.
[dict setObject:description forKey:p]; // BZZZZT a copy of p is made using NSCopying

MyClass* found = [dict objectForKey:p]; //returns nil, as p becomes a different copy.

So that doesn't work.

I can hack it by passing in an NSNumber like so:

[dict setObject:description forKey:[NSNumber numberWithInt:(int)p]]; // this is cool

But that's not only ugly, but error prone since it is nonstandard.

With that in mind, is there a clean way to do this?

Cyclorama answered 27/4, 2011 at 4:5 Comment(1)
Use NSMapTable instead. See: nshipster.com/nshashtable-and-nsmaptableAkee
W
7

NSValue confirms NSCopying as per NSValue Class Reference. So you can use an instance of NSValue as a key.

Use +valueWithPointer: to wrap the pointer value up.

NSString* foo = @"foo";
id bar = @"bar";

NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
[dict setObject:@"forKey:foo" forKey:foo];
[dict setObject:@"forKey:bar" forKey:bar];
[dict setObject:@"forKey:[NSValue... foo]" forKey:[NSValue valueWithPointer:foo]];
[dict setObject:@"forKey:[NSValue... bar]" forKey:[NSValue valueWithPointer:bar]];

NSLog(@"%@", [dict objectForKey:foo]); 
NSLog(@"%@", [dict objectForKey:bar]); 
NSLog(@"%@", [dict objectForKey:[NSValue valueWithPointer:foo]]); 
NSLog(@"%@", [dict objectForKey:[NSValue valueWithPointer:bar]]); 

Gives

2013-01-24 04:42:14.051 a.out[67640:707] forKey:foo
2013-01-24 04:42:14.052 a.out[67640:707] forKey:bar
2013-01-24 04:42:14.053 a.out[67640:707] forKey:[NSValue... foo]
2013-01-24 04:42:14.053 a.out[67640:707] forKey:[NSValue... bar]
Wedekind answered 23/1, 2013 at 19:34 Comment(2)
In iOS 6 with ARC I have to do this in order to use NSValue: [NSValue valueWithPointer:(__bridge const void *)(obj)]. It won't let me use the object without casting it.Surge
How about: @((NSInteger)obj) ?Exaction
P
2

Your problem is that NSDictionary copies its keys. Either make your class implement NSCopying or use a CFMutableDictionary with a key callback that doesn't copy (it's toll-free bridged with NSMutableDictionary, so you can use it exactly the same after it's created).

Potful answered 27/4, 2011 at 6:12 Comment(0)
P
1

You may find it easier to use objc_setAssociatedObject/objc_getAssociatedObject. They're described here.

Painkiller answered 27/4, 2011 at 6:18 Comment(3)
I would rather stick within the constraints of how the class would normally function, or simply not use it at all. I don't want my API to use an NSDictionary that has to be used in a completely nonstandard fashion to function.Cyclorama
Are you responding to the wrong answer? My suggestion doesn't involve NSDictionary at all.Painkiller
Oops, sorry, I skimmed the first time and misunderstood. This, while an improvement, doesn't help me because I want the ability to store the object based on context, since multiple pieces of code can potentially refer to the same object, and I don't want the 100% association, as I want the other code to be able to see if an object is associated, rather than it being in lockstep. I'm not sure I explained this well.Cyclorama
B
0

My personal solution:

override - (BOOL) isEqual:(id)object in your custom class.

in this method, compare each property inselfand object. if they are the same, return YES

I'm not sure how you implemented your - (id) copyWithZone:(NSZone *)zone method, but for my test project, it works fine with me.

attachment:

- (id) copyWithZone:(NSZone *)zone{
    testObject *copy = [[testObject allocWithZone:zone] init];
    NSString *tempCopyString = [NSString stringWithString:self.string]
    copy.string = tempCopyString;
    return copy;
}

- (BOOL) isEqual:(id)object{
    testObject *newObject = (testObject*)object;
    if ([newObject.string isEqualToString:object.string]) {
        return YES;
    }
    else {
        return NO;
    }
}
Bloodstain answered 27/4, 2011 at 6:26 Comment(1)
My class isn't lightweight, I don't want to create copies at all -- doing so would probably cost more than simply adding a pointer to every class that could possibly have a 'description'.Cyclorama
F
0

As this is a problem that can occur often, I have created the following class to associate objects to other objects without copying of the key. Identity is determined on the key object address. It's a subclass of NSMutableDictionary, so all methods of that class are available.

ObjectMap.h

//
//  ObjectMap.h
//
//  Created by René Dekker on 07/03/2012.
//  Copyright (c) 2012 Renevision.
//

#import <Foundation/Foundation.h>

@interface ObjectMap : NSMutableDictionary

+ (ObjectMap *) objectMap;

- (bool) containsObject:(id)aKey;

// use the following instead of setObject:forKey: in IOS 6, to avoid warnings about NSCopying
- (void) setObject:(id)anObject forObjectKey:(id)aKey;


@end

ObjectMap.m

//
//  ObjectMap.m
//
//  Created by René Dekker on 07/03/2012.
//  Copyright (c) 2012 Renevision.
//

#import "ObjectMap.h"
#import <CoreFoundation/CoreFoundation.h>

@interface ObjectMapEnumerator : NSEnumerator 

@end

@implementation ObjectMapEnumerator {
    NSUInteger size;
    NSUInteger currentIndex;
    id *keysArray;
}

- (NSArray *) allObjects
{
    return [NSArray arrayWithObjects:keysArray count:size];
}

- (id) nextObject
{
    if (currentIndex >= size) {
        return nil;
    }
    return keysArray[currentIndex++];
}

- (id) initWithDict:(CFDictionaryRef)dict
{
    if (!(self = [super init])) {
        return nil;
    }
    size = CFDictionaryGetCount(dict);
    keysArray = malloc( size * sizeof(id) );
    currentIndex = 0;
    CFDictionaryGetKeysAndValues(dict, (const void **)keysArray, NULL);
    return self;
}

- (void) dealloc
{
    free(keysArray);
    [super dealloc];
}

@end

@implementation ObjectMap {
    CFMutableDictionaryRef theDictionary;
}

- (void) setObject:(id)anObject forKey:(id)aKey
{
    CFDictionarySetValue(theDictionary, aKey, anObject);
}

- (void) setObject:(id)anObject forObjectKey:(id)aKey
{
    CFDictionarySetValue(theDictionary, aKey, anObject);
}

- (id) objectForKey:(id)aKey
{
    return CFDictionaryGetValue(theDictionary, aKey);
}

- (void) removeObjectForKey:(id)aKey
{
    CFDictionaryRemoveValue(theDictionary, aKey);
}

- (void) removeAllObjects
{
    CFDictionaryRemoveAllValues(theDictionary);
}

- (bool) containsObject:(id)aKey
{
    return CFDictionaryContainsKey(theDictionary, aKey);
}

- (NSUInteger) count
{
    return CFDictionaryGetCount(theDictionary);
}

- (NSEnumerator *)keyEnumerator
{
    return [[[ObjectMapEnumerator alloc] initWithDict:theDictionary] autorelease];
}

#pragma - Object Life Cycle

static void dictionaryRelease(CFAllocatorRef allocator, const void* value) {
    if (0 != value) {
        CFRelease(value);
    }
}

static const void *dictionaryRetain(CFAllocatorRef allocator, const void* value) {
    if (0 != value) {
        return CFRetain(value);
    }
    return 0;
}

static CFDictionaryKeyCallBacks callbacks = { 0, &dictionaryRetain, &dictionaryRelease, &CFCopyDescription, NULL, NULL };

- (id) init
{
    if (!(self = [super init])) {
        return nil;
    }
    theDictionary = CFDictionaryCreateMutable(NULL, 0, &callbacks, &kCFTypeDictionaryValueCallBacks);
    return self;
}

- (void) dealloc
{
    CFRelease(theDictionary);
    [super dealloc];
}

+ (ObjectMap *) objectMap
{
    return [[[ObjectMap alloc] init] autorelease];
}

@end
Forewent answered 16/11, 2012 at 15:41 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.