Objects as keys NSMutableDictionary
-
I'm having a little problem with NSMutableDictionary and would like
to find out what "best practice" for this is.
Here's a contrived example: A class "exam" should store the result
for each student. So we have 20 students and an NSNumber for each.
I'd like to store them like this:
in init:
NSMutableDictionary *result = [NSMutableDictionary dictionary];
- (void)setResult:(NSNumber *)aResult forStudent:(Student *)aStudent
{
[results setObject:aResult forKey:aStudent];
}
Now firstly, NSDictionary likes NSStrings as keys for key-value
coding. I don't depend on that, so I'm willing to ignore it. However,
it also requires the keys to be immutable, so I'd have to copy the
student object. I don't want to do that, since I want to later be
able to go back and retrieve the result for that specific student.
I guess I could also store the results as an NSMutableArray of Result-
objects, but NSDictionary seems to be less overhead.
Thanks for any ideas,
m -
On Jul 9, 2006, at 06:11, Matthias Winkelmann wrote:
> I'm having a little problem with NSMutableDictionary and would like
> to find out what "best practice" for this is.
>
> Here's a contrived example: A class "exam" should store the result
> for each student. So we have 20 students and an NSNumber for each.
> I'd like to store them like this:
>
> in init:
> NSMutableDictionary *result = [NSMutableDictionary dictionary];
>
>
> - (void)setResult:(NSNumber *)aResult forStudent:(Student *)aStudent
> {
> [results setObject:aResult forKey:aStudent];
> }
>
> Now firstly, NSDictionary likes NSStrings as keys for key-value
> coding. I don't depend on that, so I'm willing to ignore it.
> However, it also requires the keys to be immutable, so I'd have to
> copy the student object. I don't want to do that, since I want to
> later be able to go back and retrieve the result for that specific
> student.
You could implement -[Student copyWithZone:] to return [self retain],
but that may have side effects you don't want.
Another solution would be to create a CFMutableDictionary with custom
callbacks, where you can specify the dictionary's retain/release/
equality/hash behavior explicitly. A recent post to this list from
Mike Ash notes that you have to use the CF functions to add values to
the dictionary if you do this, but that's not too painful.
-- Adam -
Matthias Winkelmann wrote on Sunday, July 9, 2006:
> I'm having a little problem with NSMutableDictionary and would like
> to find out what "best practice" for this is.
>
> Here's a contrived example: A class "exam" should store the result
> for each student. So we have 20 students and an NSNumber for each.
> I'd like to store them like this:
>
> in init:
> NSMutableDictionary *result = [NSMutableDictionary dictionary];
>
>
> - (void)setResult:(NSNumber *)aResult forStudent:(Student *)aStudent
> {
> [results setObject:aResult forKey:aStudent];
> }
Should work just fine, with a few caveats...
> Now firstly, NSDictionary likes NSStrings as keys for key-value
> coding. I don't depend on that, so I'm willing to ignore it.
NSDictionay doesn't care. I use all kinds of objects as keys, NSNumber being very popular. What NSDictionary is doesn't like is key objects that don't follow the required contract. Specifically, the isEqual: and hash contract must be valid and hash should return something "hashy", not some constant of a value likely to be the same as other objects. This is very important for the efficiency of the dictionary. See the documentation for the contract for hash: <file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/Reference/Foundation/Protocols/NSObject_Protocol/Reference/Reference.html#//apple_ref/occ/intfm/NSObject/hash>
To encode a dictionary as a property list the property list framework requires that the keys to a dictionary be strings. But that's a property list encoding requirement, not an NSDictionary requirement.
> However,
> it also requires the keys to be immutable, so I'd have to copy the
> student object. I don't want to do that, since I want to later be
> able to go back and retrieve the result for that specific student.
Technically, NSDictionary and NSSet only require that the comparison and hash behavior of the objects being used as keys be followed. NSDictionary really doesn't care if the key objects change as long as the results of [key isEqual:x], [key hash], and [key compare:x] don't change over time. Cocoa's sets (like most collection implementations) rely on the fact that the keys are stable in terms of sorting and comparison.
Taking the Student object as an example again, if you implemented -[Student compare:], -[Student isEqual:], and -[Student hash] so that they operated on a unique and immutable property of Student (like the student's ID) and ignored everything else in the Student object, then you could modify Student without changing its comparison behavior and it would be perfectly suitable for use as a key in a dictionary.
On the other hand, if the comparisons and hash methods of Student would return different values if you changed any property of the student (for example, the student's grade average), then you would not want to use them as keys. What I typically do in this situation is create a proxy object that is stable. Something like a StudentRef class that used only some immutable property of the student to perform comparisons. Then you would add and retrieve values like this:
[results setObject:aResult forKey:[[[StudentRef alloc] referenceWithStudent:aStudent] autorelease]];
...
result = [results objectForKey:[[[StudentRef alloc] referenceWithStudent:aStudent] autorelease]];
--
James Bucanek -
> Technically, NSDictionary and NSSet only require that the
> comparison and hash behavior of the objects being used as keys be
> followed.
From file:///Developer/ADC%20Reference%20Library/documentation/Cocoa/
Conceptual/Collections/Concepts/Dictionaries.html#//apple_ref/doc/uid/
20000134 :
"Methods that add entries to dictionaries—whether as part of
initialization (for all dictionaries) or during modification (for
mutable dictionaries)— don’t add each value object to the dictionary
directly, but copy each key argument and add the copy to the
dictionary. In Objective-C, the dictionary copies each key argument
(keys must conform to the NSCopying protocol) and adds the copies to
the dictionary. Each corresponding value object receives a retain
message to ensure that it won’t be deallocated before the dictionary
is through with it."
i.e. your keys need to support NSCopying. The original poster,
Matthias, more or less knew this. NSDictionary doesn't require keys
to be immutable as such, just that they don't mutate while in the
dictionary - that would obviously pose problems with any internal
mechanisms it uses like hash tables and the like. If your object is
in fact immutable, you can implement your copy methods to just return
[self retain]. But as Adam noted, this can potentially have unwanted
side effects (although I myself wouldn't think them likely to crop up
for 99% of cases).
What offends Matthias is the idea of copying a unique item, like a
Student instance, where he in a nutshell wants to maintain pointer
equality, disregarding isEqual: and such things. There are various
respectable reasons for doing this; I'm sure Matthias has his. I've
had to deal with this issue myself a few times in past. Assuming you
don't want to implement NSCopying as [self retain], there are many
other options..
As James noted, one solution is to use a proxy object which can be
copied, who's hash value could, for example, be the address of the
proxied Student instance.
Another alternative is to just use the CF collections directly with
custom functions for the retain/release/hash/compare/whatever stuff.
That's more efficient, and probably the better solution - you then
don't have to worry about things like all those extra proxies
retaining your Student instances and whatnot. Less memory usage as
well, and so forth.
But perhaps the most straightforward way is to just use a different
key. For example, it's pretty typical for students to have a unique
numerical value associated with them. You could use this as the key
in all relevant dictionaries, instead of the Student instance itself,
as NSNumbers can happily be copied and so forth without any worries.
You'd need an extra dictionary somewhere to map these NSNumbers to
their proper Student instances (or some other such lookup method),
but that's probably not a big deal. And if you're ever interfacing
with databases, even something like CoreData, using a primitive type
like a number is probably very handy anyway.
I wouldn't be surprised to find Student implemented the hash method
to simply return the student ID number anyway.
Wade Tregaskis
ICQ: 40056898
AIM, Yahoo & Skype: wadetregaskis
MSN: <wjtregaskis...>
iChat & email: <wadetregaskis...>
Jabber: <wadetregaskis...>
Google Talk: <wadetregaskis...>
http://homepage.mac.com/wadetregaskis/
-- Sed quis custodiet ipsos custodes? -
No biggy...
NSMutableDictionary*
happyDict=[(NSMutableDictionary*)CFDictionaryCrateMutable(NULL, 0,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)
autorelease];
Now you can treat happyDict like any other NSMutableDicitonary and
put anything as the keys. Toll free bridging at work.
the default implementation of -hash just returns the pointer value,
IIRC, so it's fine to use mutable objects as keys since their pointer
value does not change in relation to their content.
Ack, at 7/10/06, <wadeslists...> said:
> "Methods that add entries to dictionaries-whether as part of
> initialization (for all dictionaries) or during modification (for
> mutable dictionaries)- don't add each value object to the dictionary
> directly, but copy each key argument and add the copy to the
> dictionary. In Objective-C, the dictionary copies each key argument
> (keys must conform to the NSCopying protocol) and adds the copies to
> the dictionary. Each corresponding value object receives a retain
> message to ensure that it won't be deallocated before the dictionary
> is through with it."
--
Sincerely,
Rosyna Keller
Technical Support/Holy Knight/Always needs a hug
Unsanity: Unsane Tools for Insanely Great People
It's either this, or imagining Phil Schiller in a thong. -
On Jul 9, 2006, at 22:16, Rosyna wrote:
> No biggy...
>
> NSMutableDictionary* happyDict=[(NSMutableDictionary*)
> CFDictionaryCrateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks,
> &kCFTypeDictionaryValueCallBacks) autorelease];
>
> Now you can treat happyDict like any other NSMutableDicitonary and
> put anything as the keys. Toll free bridging at work.
When I suggested a CFDictionary in my reply, I added a caveat that
values should be added with CF functions. Here's why:
#import <Foundation/Foundation.h>
@interface TestObject : NSObject <NSCopying> @end
@implementation TestObject
- (id)copyWithZone:(NSZone *)aZone
{
[NSException raise:NSInternalInconsistencyException
format:@"Must not copy objects of class %@", [self class]];
return nil; // make the compiler happy...
}
@end
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSMutableDictionary* cfDict = [(NSMutableDictionary*)
CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0,
&kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)
autorelease];
TestObject *obj = [[TestObject alloc] init];
CFDictionaryAddValue((CFMutableDictionaryRef)cfDict, obj, CFSTR
("a CFStringRef"));
[cfDict setObject:@"Value" forKey:obj];
[pool release];
return 0;
}
Using the CFDictionaryAddValue function, things work. Using
setObject:forKey:, an exception is raised when the key is copied.
#0 0x9295f120 in _NSRaiseError
#1 0x9295ee5c in +[NSException raise:format:]
#2 0x00027c7c in -[TestObject copyWithZone:] at keyTest.m:9
#3 0x92925490 in -[NSCFDictionary setObject:forKey:]
>
> the default implementation of -hash just returns the pointer value,
> IIRC, so it's fine to use mutable objects as keys since their
> pointer value does not change in relation to their content.
Yes. In that case, though, isEqual: must also be based on pointer
equality (which is also the default).
Adam
>
> Ack, at 7/10/06, <wadeslists...> said:
>
>> "Methods that add entries to dictionaries-whether as part of
>> initialization (for all dictionaries) or during modification (for
>> mutable dictionaries)- don't add each value object to the
>> dictionary directly, but copy each key argument and add the copy
>> to the dictionary. In Objective-C, the dictionary copies each key
>> argument (keys must conform to the NSCopying protocol) and adds
>> the copies to the dictionary. Each corresponding value object
>> receives a retain message to ensure that it won't be deallocated
>> before the dictionary is through with it."
>
-
As, I see. NSCFDictionary explicitly calls copyWithZone: before
setting the key using CFDictionarySetValue(). I wonder why.
Ack, at 7/10/06, Adam R. Maxwell said:
> Using the CFDictionaryAddValue function, things work. Using
> setObject:forKey:, an exception is raised when the key is copied.
--
Sincerely,
Rosyna Keller
Technical Support/Holy Knight/Always needs a hug
Unsanity: Unsane Tools for Insanely Great People
It's either this, or imagining Phil Schiller in a thong. -
On Jul 10, 2006, at 06:43, Rosyna wrote:
> As, I see. NSCFDictionary explicitly calls copyWithZone: before
> setting the key using CFDictionarySetValue(). I wonder why.
Yes, and that wasn't expected behavior in my case; perhaps it does
this because Keys Are Always NSStrings? My bug report disappeared
into the black hole of "Duplicate" so I'll never know, but I'm glad
Mike Ash pointed this out a few weeks ago.
Adam -
Well, if it assumed the keys were always strings, then it would just
use kCFCopyStringDictionaryKeyCallBacks when creating the internal
CFDictionary. In which case this would make the copyWithZone: a
useless extra step. It's also why I mistakenly assumed that creating
the NSDictionary with the CF call using different callbacks would
address this issue.
Well, at least the documentation for setObject:forKey: explicitly
says that the key must respond to copyWithZone:. In these cases, it
might just be best to return [self retain] or something.
Ack, at 7/10/06, Adam R. Maxwell said:
> On Jul 10, 2006, at 06:43, Rosyna wrote:
>
>> As, I see. NSCFDictionary explicitly calls copyWithZone: before
>> setting the key using CFDictionarySetValue(). I wonder why.
>
> Yes, and that wasn't expected behavior in my case; perhaps it does
> this because Keys Are Always NSStrings? My bug report disappeared
> into the black hole of "Duplicate" so I'll never know, but I'm glad
> Mike Ash pointed this out a few weeks ago.
--
Sincerely,
Rosyna Keller
Technical Support/Holy Knight/Always needs a hug
Unsanity: Unsane Tools for Insanely Great People
It's either this, or imagining Phil Schiller in a thong. -
On 7/10/06, Adam R. Maxwell <amaxwell...> wrote:
>
> On Jul 10, 2006, at 06:43, Rosyna wrote:
>
>> As, I see. NSCFDictionary explicitly calls copyWithZone: before
>> setting the key using CFDictionarySetValue(). I wonder why.
>
> Yes, and that wasn't expected behavior in my case; perhaps it does
> this because Keys Are Always NSStrings?
It does the copy because it doesn't want the key object to change
while being used as a key. Of course the general assumption is that
the objects you are using as a key are not using pointer comparison in
isEqual: or when generating hash.
-Shawn -
On Monday, July 10, 2006, at 07:19AM, Rosyna <rosyna...> wrote:
> Well, if it assumed the keys were always strings, then it would just
> use kCFCopyStringDictionaryKeyCallBacks when creating the internal
> CFDictionary. In which case this would make the copyWithZone: a
> useless extra step. It's also why I mistakenly assumed that creating
> the NSDictionary with the CF call using different callbacks would
> address this issue.
It's an inconsistent situation (at least in my view), since the dictionary /will/ use any custom retain/release/equal/hash callbacks you define (so using objectForKey: works fine, and uses pointer equality), but setObject:forKey: will first create a copy...and the copy is then retained using your custom retain callback.
Of course, if you use the CF functions, only the callbacks are used, so it's easy to make a KVC-compliant wrapper for it. I used it to make an NSCountedSet subclass that uses case-insensitive string comparison and stores integers directly in the dictionary; very handy.
> Well, at least the documentation for setObject:forKey: explicitly
> says that the key must respond to copyWithZone:. In these cases, it
> might just be best to return [self retain] or something.
I knew someone would point that out :). It would be nice to know if this callback situation is expected behavior for toll-free bridged classes, though, since I don't want to make assumptions based on current implementation (will my callbacks always work when getting values?).
-- Adam
>
> Ack, at 7/10/06, Adam R. Maxwell said:
>
>> On Jul 10, 2006, at 06:43, Rosyna wrote:
>>
>>> As, I see. NSCFDictionary explicitly calls copyWithZone: before
>>> setting the key using CFDictionarySetValue(). I wonder why.
>>
>> Yes, and that wasn't expected behavior in my case; perhaps it does
>> this because Keys Are Always NSStrings? My bug report disappeared
>> into the black hole of "Duplicate" so I'll never know, but I'm glad
>> Mike Ash pointed this out a few weeks ago.
>
> --
>
>
> Sincerely,
> Rosyna Keller
> Technical Support/Holy Knight/Always needs a hug
>
> Unsanity: Unsane Tools for Insanely Great People
>
> It's either this, or imagining Phil Schiller in a thong.
>
>
-
On 7/10/06, Adam Maxwell <amaxwell...> wrote:
>
> On Monday, July 10, 2006, at 07:19AM, Rosyna <rosyna...> wrote:
>
>> Well, if it assumed the keys were always strings, then it would just
>> use kCFCopyStringDictionaryKeyCallBacks when creating the internal
>> CFDictionary. In which case this would make the copyWithZone: a
>> useless extra step. It's also why I mistakenly assumed that creating
>> the NSDictionary with the CF call using different callbacks would
>> address this issue.
>
> It's an inconsistent situation (at least in my view), since the dictionary /will/ use any custom retain/release/equal/hash callbacks you define (so using objectForKey: works fine, and uses pointer equality), but setObject:forKey: will first create a copy...and the copy is then retained using your custom retain callback.
>
> Of course, if you use the CF functions, only the callbacks are used, so it's easy to make a KVC-compliant wrapper for it. I used it to make an NSCountedSet subclass that uses case-insensitive string comparison and stores integers directly in the dictionary; very handy.
In case anybody is wondering, Apple believes that none of this is
wrong. I just noticed an update to my original bug (4350677) filed on
this. The followup states:
"Engineering has determined that this issue behaves as intended based
on the following information:
"Please note that the callbacks are still respected, however,
-[NSMutableDictionary setObject:forKey:] is documented to copy the
key."
So I guess incorrect behavior still wins because The Docs Said So.
I'm done with this particular battle, it's just not that important. If
anybody else wants to take it up with Apple, though, feel free.
Mike -
Another option is to pass string keys around using some two-way
conversion method. For example, the object's address concatenated with
its creation time is a good one. Using -description could also work.
-M
On Jul 9, 2006, at 9:51 AM, Adam R. Maxwell wrote:
>
> On Jul 9, 2006, at 06:11, Matthias Winkelmann wrote:
>
>> I'm having a little problem with NSMutableDictionary and would like
>> to find out what "best practice" for this is.
>>
>> Here's a contrived example: A class "exam" should store the result
>> for each student. So we have 20 students and an NSNumber for each.
>> I'd like to store them like this:
>>
>> in init:
>> NSMutableDictionary *result = [NSMutableDictionary dictionary];
>>
>>
>> - (void)setResult:(NSNumber *)aResult forStudent:(Student *)aStudent
>> {
>> [results setObject:aResult forKey:aStudent];
>> }
>>
>> Now firstly, NSDictionary likes NSStrings as keys for key-value
>> coding. I don't depend on that, so I'm willing to ignore it. However,
>> it also requires the keys to be immutable, so I'd have to copy the
>> student object. I don't want to do that, since I want to later be
>> able to go back and retrieve the result for that specific student.
¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬
AgentM
<agentm...>
¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬ ¬



