10

I have an object called Settings that inherits from NSMutableDictionary. When I try to initialize this object using

Settings *settings = [[Settings alloc] initWithContentsOfFile: @"someFile"]

it returns an object of type NSCFDictionary. As a result, it doesn't recognize my additional methods. For example, when I call the selector "save", it objects:

[NSCFDictionary save]: unrecognized selector sent to instance 0x524bc0

Of course, it's OK when I initialize using the garden variety

Settings *settings = [[Settings alloc] init]

I tried to cast it again to Settings but that didn't work. This seems really simple - what am I missing?

Thanks

John Rasch
  • 62,489
  • 19
  • 106
  • 139
farhadf
  • 1,918
  • 3
  • 19
  • 27
  • When I read the first sentence I immediately thought that maybe, instead of creating a Settings class, you could use NSUserDefaults? Of course I don't know whether NSUserDefaults would suit your needs, but it's designed to store settings, so there might be no need to build your own. Just a thought. – Thomas Müller Jul 28 '09 at 05:51
  • Thanks. This does sound like it'll do what I want. It's not a "default" value per se that I'm storing, but this will probably do just fine. – farhadf Jul 30 '09 at 19:02

5 Answers5

18

NSDictionary is a class cluster. This means that the value returned from its init methods is not strictly an NSDictionary, but a subclass that implements the actual functionality. In almost every case, it is better to give your class an NSDictionary as an instance variable or to simply define a category on NSDictionary.

Chuck
  • 234,037
  • 30
  • 302
  • 389
  • 1
    Other "classes" like this are (most notably) NSString, NSArray, and NSSet. I occasionally come across log statements referring to an "NSBigMutableString" that used to boggle me until I learned about class clusters. =) – Dave DeLong Jul 28 '09 at 02:07
  • I agree with this. Best would probably be to use a has-a relationship, instead of an is-a relationship. (prefer composition over subclassing). You can make your class perform like a dictionary by overriding `-setValue:forKey:` and `-valueForKey:`, etc. – nielsbot Jul 24 '13 at 18:45
16

Chuck is correct about NSDictionary (and Dave, by extension, about NSArray/Set/String) and class clusters. Odds are that -[NSDictionary initWithContentsOfFile:] calls down to a different initializer than -init does, which is why it swaps out your allocated Settings instance for another subclass of NSMutableDictionary. (The initialization action when reading from a file may select a particular known subclass of NSDictionary which performs well for loading from a file, etc.)

I'll echo Chuck's guidance that it is almost always better to use composition or categories than inheritance for an NSDictionary. It's highly likely that you could accomplish what you're doing with categories in a much simpler way, and expose yourself to fewer potential bugs in the process. Consider yourself warned before deciding to subclass.

That being said, both NSDictionary and NSMutableDictionary have been designed to support subclassing, and on rare occasions that's the right thing to do. Think long and hard about it before trying it. If you find it's the right choice for your design, here are some key points to know and do as needed:

  1. Override the following primitive methods from NSDictionary:
    • -count
    • -objectForKey:
    • -keyEnumerator
    • -initWithObjects:forKeys:count: (designated initializer)
  2. Override the following primitive methods from NSMutableDictionary:
    • -setObject:forKey:
    • -removeObjectForKey:
  3. If you're supporting NSCoding, be aware of classForKeyedArchiver and replacementObjectForKeyedArchiver: (both instance methods from NSObject) — they can totally change how your class responds, and you often unintentionally inherit some odd behavior from NS(Mutable)Dictionary. (You can verify if they are the culprit by setting a breakpoint on them, or implementing them to call super and breaking on your own code.)

I've implemented a number of these points in an NSMutableDictionary subclass of my own. You can check it out and use the code however may be helpful to you. One that particularly helped me (and could be the solution for your problem) was overloading the designated initializer, which is currently undocumented (Radar #7046209).

The thing to remember is that even though these bullets cover most common uses, there are always edge cases and less common functionality to account for. For example, -isEqual: and -hash for testing equality, etc.

Paul Beusterien
  • 27,542
  • 6
  • 83
  • 139
Quinn Taylor
  • 44,553
  • 16
  • 113
  • 131
  • Thanks - you have all sufficiently scared me from sub-classing NSDictionary, at least until I'm more seasoned. So I am going to either use NSUserDefaults, or else use the composition pattern. – farhadf Jul 30 '09 at 19:01
3

If you actually read the spec for NSDictionary (a rash action, I know) you'll find a section named "Subclassing Notes". In it you will read:

If you do need to subclass NSDictionary, you need to take into account that is represented by a Class cluster—there are therefore several primitive methods upon which the methods are conceptually based:

initWithObjects:forKeys:

count

objectForKey:

keyEnumerator

In a subclass, you must override all these methods.

Hot Licks
  • 47,103
  • 17
  • 93
  • 151
1

From https://stackoverflow.com/a/1191351/467588, this is what I did to make a subclass of NSDictionary works. I just declare an NSDictionary as an instance variable of my class and add some more required methods. I don't know what to call them though.

I posted my code sample here https://stackoverflow.com/a/10993594/467588.

Community
  • 1
  • 1
Hlung
  • 13,850
  • 6
  • 71
  • 90
  • This is similar to what Apple is recommending - a composite object: https://developer.apple.com/library/mac/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html#//apple_ref/doc/uid/TP40010810-CH4-SW81 – mahboudz Nov 10 '13 at 10:10
  • @mahboudz, Why not simply returning from the method if it's not valid, and if it is, change `self`, instead of relying on a separate array? – Iulian Onofrei Nov 08 '18 at 16:40
1

This question is very old, and since most of these answers were posted, Apple has introduced object subscripting, which allows you to make your own classes behave more like NSMutableArray or NSMutableDictionary. This is simpler than the alternatives discussed above.

At a minimum, you have to override these methods:

//Array-style
- (id)objectAtIndexedSubscript:(NSUInteger)idx;
- (void)setObject:(id)obj atIndexedSubscript:(NSUInteger)idx;

//Dictionary-style
- (id)objectForKeyedSubscript:(id <NSCopying>)key;
- (void)setObject:(id)obj forKeyedSubscript:(id <NSCopying>)key;

Here's a nice tutorial on how to do just that.

shmim
  • 729
  • 9
  • 21