tags:

views:

385

answers:

2

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

+6  A: 

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
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
+2  A: 

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.

Quinn Taylor
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.
LeftHem