Swift & Objective-C

Class Clusters: Why NSArray Is an Abstract Class

Running this code in Objective-C produces a rather surprising result.

4 min read
Cover image for Class Clusters: Why NSArray Is an Abstract Class

Running this code in Objective-C produces a rather surprising result.

NSArray *array = [NSArray arrayWithObjects:@1, @2, nil];
NSLog(@"%@", [array class]);
// Output: __NSArrayI

We clearly created an NSArray, yet the class name is __NSArrayI. For an empty array, it may be __NSArray0, while an array with one element may produce __NSSingleObjectArrayI.

This is not a bug. It is the Class Cluster pattern.

NSArray, NSString, NSNumber, and NSDictionary are all abstract classes.

The actual instance is created as a hidden subclass suited to the situation.

This is covered in Item 9 of Effective Objective-C 2.0, and it is unavoidable as long as you use Foundation.


The key takeaways first (3 points)

  1. Foundation collections such as NSArray are abstract classes that expose only a public interface.
  2. The initializer selects and returns an instance of a private subclass (a kind of factory pattern).
  3. That makes [array class] comparisons dangerous, and subclassing subject to strict rules.

Why was it designed this way?

Even a single array can use several optimization strategies.

  • Immutable array with zero elements: one singleton is enough; there is no need to create one every time
  • One or two elements: storing the pointers inline in the structure is faster
  • Large mutable array: a separate implementation with a reallocation strategy

Instead of cramming all of this into one NSArray implementation with if statements, Foundation provides dedicated subclasses for each situation and lets a factory method choose the appropriate one. Callers need to know only the NSArray interface, while the internal implementation can be swapped freely between OS versions.

In practice, [NSArray alloc] returns __NSPlaceholderArray, a temporary object, which is replaced by the real implementation during the subsequent init-family call. We conventionally write alloc and init together, but in a class cluster these two stages actually move between different objects.

The factory chooses an implementation based on the element count
The factory chooses an implementation based on the element count

Two practical traps

Trap 1: Comparing classes directly

// Dangerous code
if ([object class] == [NSArray class]) {
    // This can never be reached. The actual classes are  __NSArrayI and others.
}

// Correct code
if ([object isKindOfClass:[NSArray class]]) {
    // Checks the entire cluster
}

Because class-cluster members subclass the public class, use isKindOfClass: to ask whether they belong to the class family rather than comparing for equality.

Trap 2: Naive subclassing

It may seem that subclassing NSArray and overriding one method is enough, but such a subclass does not even inherit storage. As an abstract class, NSArray has no storage. Properly integrating a subclass into the cluster requires following its rules.

  • Provide your own storage (typically by containing a real NSArray internally)
  • Implement every designated primitive method (for NSArray, count and objectAtIndex:)

All other methods are designed to operate on these primitives, so implementing just the two brings the cluster’s full functionality to life. However, Effective Objective-C offers clear advice: in most cases, composition (a wrapper class containing an NSArray) is better than inheritance.


Why Swift developers should care too

Swift’s Array is a structure, so this pattern may seem unrelated, but there are two points of contact.

First, bridging. Even when an NSArray from an Objective-C API becomes a Swift Array, an actual implementation such as _ContiguousArrayStorage or __NSArrayI remains underneath. When investigating bridging performance issues, you eventually encounter these cluster implementations.

Second, the idea itself carried over to Swift. The notion that “a public type exposes only an interface while a hidden type handles the actual implementation” leads to type-erasing wrappers such as Swift’s AnySequence and the some keyword (opaque types). There is one return type, while the concrete type is known only to the compiler and implementation—a direct descendant of the class-cluster philosophy.

The __NS classes shown in the debugger are the real protagonists
The __NS classes shown in the debugger are the real protagonists

Wrapping up

  • NSArray·NSString·NSNumber·NSDictionary are abstract classes; actual instances are hidden subclasses
  • Type checks must use isKindOfClass:; never compare classes for equality
  • Class-cluster subclassing requires implementing the primitive methods, and composition is usually preferable
  • This long-standing design separates interface from implementation and extends all the way to Swift’s opaque types

Even if you encounter a class with an unfamiliar __NS prefix in the debugger, there is no reason to panic. You are simply seeing Foundation’s nearly 30 years of optimization made visible.