There was an interesting question on StackOverflow this morning: Why doesn’t NSOrderedSet inherit from NSSet? It’s interesting because the reason is so easy to miss. I thought it would make a good blog post because it turned out to be a nice, real-life example of the Liskov substitution principle (herein abbreviated to LSP).
The (Winding) Answer
When I saw the question, the first thing I thought of was old mate Liskov.
I opened up the documentation for NSSet and went through every method,
trying to find one that would have its contract violated by NSOrderedSet.
Every single method looked fine to me. They could all be implemented in
NSOrderedSet without any problems.
Upon closer inspection, there is one little method that violates the LSP in a
very subtle way. That method is mutableCopy. The return value of
mutableCopy would have to be an NSMutableSet, but NSMutableOrderedSet
should inherit from NSOrderedSet. It turns out that you can’t have both.
Let me explain with some code. First, let’s look at the correct behaviour of
NSSet and NSMutableSet:
NSSet* immutable = [NSSet set];
NSMutableSet* mutable = [immutable mutableCopy];
[mutable isKindOfClass:[NSSet class]]; // YES
[mutable isKindOfClass:[NSMutableSet class]]; // YES
Now, let’s pretend NSOrderedSet does inherit from NSSet, and
NSMutableOrderedSet inherits from NSOrderedSet as expected:
NSSet* immutable = [NSOrderedSet orderedSet];
NSMutableSet* mutable = [immutable mutableCopy];
[mutable isKindOfClass:[NSSet class]]; // YES
[mutable isKindOfClass:[NSMutableSet class]]; // NO (problem)
The last line of the example above shows the problem. You wouldn’t be able to
pass an NSOrderedSet into a function expecting an NSSet because the
behaviour is different. It’s a violation of the LSP, which also makes it a
backwards compatibility problem.
What if NSMutableOrderedSet inherited from NSMutableSet instead? Then we
get a different problem:
NSSet* immutable = [NSOrderedSet orderedSet];
NSMutableSet* mutable = [immutable mutableCopy];
[mutable isKindOfClass:[NSSet class]]; // YES
[mutable isKindOfClass:[NSMutableSet class]]; // YES
[mutable isKindOfClass:[NSOrderedSet class]]; // NO (problem)
Now, NSMutableOrderedSet doesn’t inherit from NSOrderedSet. In Foundation,
every mutable class inherits from the immutable class. To break this rule would
be a bad design decision. Imagine if you had to allocate a new NSArray every
time you wanted to pass an NSMutableArray into a function that takes
immutable arrays as parameters. That would suck.
Update: Michael Tsai correctly points out another LSP violation. If
NSOrderedSet inherits from NSSet, then isEqual: can not correctly compare
an ordered set to an unordered set, for example:
NSSet* unordered = [NSSet setWithObjects:@"a", @"b", @"c", nil];
NSSet* ordered1 = [NSOrderedSet orderedSetWithObjects:@"a", @"b", @"c", nil];
NSSet* ordered2 = [NSOrderedSet orderedSetWithObjects:@"c", @"b", @"a", nil];
[ordered1 isEqual:ordered2]; //NO, because the order is different
// Should this be NO because the order may be different,
// or should it be YES for backwards compatibility, because
// NSSet never used to care about order?
[unordered isEqual:ordered1]; //problem
Conclusion
This all boils down to the fact that NSMutableOrderedSet can’t inherit from
both NSMutableSet and NSOrderedSet because Objective-C doesn’t have
multiple inheritance. The usual way to get around this is to make protocols for
NSMutableSet and NSOrderedSet, because then NSMutableOrderedSet can
implement both protocols.
I guess the Apple developers just thought it was simpler without the extra
protocols, and I agree with them. The addition of the new protocols would
cascade into the API of other areas. You would have to change method signatures
to take id<NSMutableSet> instead of NSMutableSet*. It’s just cleaner
to use [myOrderedSet set] when you really need to use the object as an
NSSet.
Funnily enough, choosing not to inherit from NSSet has already caused at
least one bug in Core Data. The error message is a dead giveaway: -[NSSet
intersectsSet:]: set argument is not an NSSet. Designing an API always
involves trade-offs.