Class Clusters in Objective-C - The Pattern Behind Foundation
You might think NSString is a class you can subclass and instantiate. But try [[NSString alloc] init] and check what you actually get back—it's not an NSString. It's __NSCFConstantString, or NSTaggedPointerString, or one of several other private classes. Welcome to class clusters.
The Pattern
A class cluster is a public abstract class that returns instances of private concrete subclasses. The public class defines the interface; private subclasses provide optimized implementations for different use cases.
When you call [NSArray arrayWithObjects:@"a", @"b", nil], you don't get an NSArray. You get an __NSArrayI (immutable) or __NSSingleObjectArrayI (single element) or __NSArray0 (empty). The Foundation framework chooses the best implementation for your specific case.
NSArray *empty = @[];
NSArray *single = @[@"one"];
NSArray *multiple = @[@"a", @"b", @"c"];
NSLog(@"Empty: %@", [empty class]); // __NSArray0
NSLog(@"Single: %@", [single class]); // __NSSingleObjectArrayI
NSLog(@"Multiple: %@", [multiple class]); // __NSArrayI
Why Bother?
Performance. An empty array can be a singleton—why allocate memory for something that holds nothing? A single-element array doesn't need the overhead of managing a buffer. By hiding these optimizations behind a common interface, Foundation gets performance benefits without complicating the public API.
Consider NSNumber. Storing a BOOL doesn't need the same representation as storing a double. The class cluster pattern lets NSNumber use completely different internal storage depending on the value type.
NSNumber *boolean = @YES;
NSNumber *integer = @42;
NSNumber *decimal = @3.14159;
// These are likely three different concrete classes internally
Subclassing Challenges
Class clusters make subclassing tricky. If you subclass NSString, your subclass needs to handle all the work that the private concrete classes normally do. You can't just override one method and inherit the rest—there's no concrete superclass implementation to inherit.
The primitive method approach is Foundation's solution. Each cluster defines a small set of methods that subclasses must implement. All other methods are built on top of these primitives.
For NSArray, the primitives are:
- (NSUInteger)count;
- (id)objectAtIndex:(NSUInteger)index;
Every other NSArray method—firstObject, containsObject:, enumerateObjectsUsingBlock:—is implemented in terms of these two. Override just the primitives, and you get the entire NSArray interface for free.
Implementing Your Own
Say you want a number type that can be backed by either a primitive or arbitrary-precision math:
// Public interface
@interface FlexNumber : NSObject
+ (instancetype)numberWithInteger:(NSInteger)value;
+ (instancetype)numberWithString:(NSString *)value; // For huge numbers
- (NSString *)stringValue;
- (NSComparisonResult)compare:(FlexNumber *)other;
@end
// Private primitive-backed subclass
@interface _FlexNumberPrimitive : FlexNumber
@property (nonatomic) NSInteger value;
@end
// Private string-backed subclass for arbitrary precision
@interface _FlexNumberBig : FlexNumber
@property (nonatomic, copy) NSString *digits;
@end
The factory methods decide which concrete class to return:
@implementation FlexNumber
+ (instancetype)numberWithInteger:(NSInteger)value {
_FlexNumberPrimitive *num = [[_FlexNumberPrimitive alloc] init];
num.value = value;
return num;
}
+ (instancetype)numberWithString:(NSString *)value {
// Check if it fits in an NSInteger
NSInteger primitive = [value integerValue];
if ([[NSString stringWithFormat:@"%ld", primitive] isEqualToString:value]) {
return [self numberWithInteger:primitive];
}
_FlexNumberBig *num = [[_FlexNumberBig alloc] init];
num.digits = value;
return num;
}
@end
Callers never see _FlexNumberPrimitive or _FlexNumberBig. They work entirely with FlexNumber.
Recognizing Class Clusters
Most Foundation collection and value types are clusters:
NSString/NSMutableStringNSArray/NSMutableArrayNSDictionary/NSMutableDictionaryNSSet/NSMutableSetNSNumberNSData/NSMutableData
A telltale sign is when -[object class] returns something unexpected. If you see double underscores or "CF" in class names, you're likely looking at a private concrete class from a cluster.
isKindOfClass: vs. Class Equality
This is why isKindOfClass: exists. Never check class clusters with direct class comparison:
// Wrong - this might fail for certain strings
if ([string class] == [NSString class]) { ... }
// Right - works for any concrete class in the cluster
if ([string isKindOfClass:[NSString class]]) { ... }
The concrete class is an implementation detail that can change between OS versions. Your code should treat it as opaque.
Mutable vs. Immutable
Class clusters typically have paired mutable and immutable versions. When you call mutableCopy on an NSArray, you get a concrete NSMutableArray subclass. When you call copy on an NSMutableArray, you might get back an immutable concrete class.
This separation exists because immutable collections can share data and optimize more aggressively. An immutable array can be a singleton for empty arrays or share its backing store with other arrays. Mutable arrays can't make these assumptions.
The class cluster pattern is everywhere in Foundation, quietly making your code faster without requiring you to think about internal representations. Once you recognize it, you'll see it throughout Apple's frameworks.