The Last (I Hope) Word on NSMutableCharacterSet

Edited to add 2014-09-05: Xcode 6 beta 7 emits a warning for assigning the NSCharacterSet to a NSMutableCharacterSet *. I've never received feedback from Apple on the matter but since it's caught now I marked the radar as resolved.

Edited to add 2014-03-07: Filed a Radar for this bug. I can't link to that, but I also filed it on [Open Radar](https://openradar.appspot.com/radar?id=5874203416854528).

Edited to add: Reading the code snippets in here is difficult. I can't find a quick & easy way to make them have horizontal scrollbars if I use a pre tag around it. Rather that have them be truncated I removed the pre tags for now so they word wrap. I'll bug Squarespace about it later.

This NSMutableCharacterSet bug! I keep poking at it, going "OK I guess that makes sense" and then a day later going "Wait. That can't be right." Quick review. The following line of code:

NSMutableCharacterSet* fakeMutableSet = [NSCharacterSet decimalDigitCharacterSet];

seems to work. But although the object you get will respond to NSMutableCharacterSet selectors it won't actually do anything. Specifically you can write this:

[fakeMutableSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];

formUnionWithCharacterSet returns a void so you can't tell if it worked or not, and in fact it will not work. Conceptually now I should have a set with the decimal digits and whitespace. What I have (as far as I can tell, you can't probe much into NSCharacterSets) is the exact same set I had before this call. If you want a mutable copy you have to call mutableCopy like so:

NSMutableCharacterSet* mutableSet = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];

So far so good. I understand why I need mutableCopy, but at first I didn't understand why A ) fakeMutableSet wasn't mutable and B ) given that it wasn't mutable calling formUnionWithCharacterSet didn't cause an unrecognized selector assert. Seems like either it should be mutable or it shouldn't respond to the selector.

I finally got around to writing some test code and I've found enough that I'm convinced there is a real bug here and it's not at all what I suspected at first. I'll write this up in a radar after I finish this post. I've placed the source on GitHub if you want to follow along.

The full test code does a bunch of introspection on a variety of NSCharacterSet and NSMutableCharacterSet objects. But here's the smoking gun part:

NSCharacterSet* decimalSet = [NSCharacterSet decimalDigitCharacterSet];
NSMutableCharacterSet* fakeMutableSet = [NSCharacterSet decimalDigitCharacterSet];
NSMutableCharacterSet* mutableSet = [[NSCharacterSet decimalDigitCharacterSet] mutableCopy];

All three of these end up pointing at an instance of an internal class called _NSCFCharacterSet. So yeah. The docs say NSMutableCharacterSet is derived from NSCharacterSet but not really in the sense I think of it. And guess what: _NSCFCharacterSet responds to the NSMutableCharacterSet methods! See:

// So here's the catch right? _NSCFCHaracterSet can respond to formUnionWithCharacterSet but

// *DOESN'T WORK* if the underlying data is not mutable.

if ([[decimalSet class] instancesRespondToSelector:@selector(formUnionWithCharacterSet:)]) {

NSLog(@"Attempting union with decimal set");

// Have to cast this call to compile, but note no cast is needed for fakeMutableSet

// Also note this is NOT an unrecognizedSelector: we respond to this method

[(id)decimalSet formUnionWithCharacterSet:[NSCharacterSet whitespaceCharacterSet]];

}

Admittedly, yes I have cast decimalSet so the compiler does not complain but I expected the runtime to skip the call because the instances shouldn't respond to the selector. Somewhere up inside formUnionWithCharacterSet there is code that says "if I'm not mutable just sort of fail silently." It would have saved me a ton of time if that had at least logged a line that says "this call didn't work".

So yeah, if NSMutableCharacterSet objects were really a different class than NSCharacterSet objects I would have had a compiler error when I wrote the offending line without the mutableCopy. If NSCharacterSet objects didn't have methods to responds to selectors from NSMutableCharacterSet then I would have had a runtime assert. The NSCharacterSet and NSMutableCharacterSet API's sort of falsely represent what the data does. I don't see anyway to catch this happening from my code. So if you're using NSMutableCharacterSet be careful.

Does this extend to other NSMutable* classes? I don't know. These are the only ones I use where there are a lot of "return a pre-populated instances" method so I'd have more trouble making a real world example using NSMutableArray or NSMutableDictionary.