What is a class cluster, and what does it change for subclassing and for type checks?

4.3.2 · L3 · Interview

Predict first

Which classes does the runtime report? (Names vary by OS version; these are current.)

print(type(of: NSArray(array: [1, 2, 3])), type(of: NSMutableArray()), type(of: NSArray()))
  • NSArray NSMutableArray NSArray
  • __NSArrayI __NSArrayM __NSArray0
  • __NSArrayI __NSArrayI __NSArrayI

Mental model. A class cluster is a shop with one front counter and several back rooms. You always ask the counter, and the shop decides which room fills the order.

A class cluster is a public abstract class with several private concrete subclasses chosen by the initialiser. NSArray, NSString, NSNumber, NSData and NSDictionary are clusters. When you write NSArray(array: …) the initialiser returns an instance of a different class: __NSArrayI for an immutable array, __NSArrayM for a mutable one, __NSArray0 for the empty one, and so on. The names are private and vary by OS version; the design hides storage and performance variants behind one stable API.

Consequences: type(of:) and isMember(of:) reveal the private class, so a check like “is this exactly NSArray” fails; use isKind(of:) or is instead. Subclassing a cluster is hard: you must provide its primitive methods (for NSArray, count and object(at:)) and often hold a real instance inside yourself (composition), because the superclass has no storage of its own. In Swift you meet clusters mostly as surprising runtime class names in logs and the memory graph.

The initialisers hand back private subclasses: immutable, mutable and empty. All of them are NSArray in the is sense.

Common traps

  • Checking type(of: x) == NSArray.self. The runtime class is a private subclass, so it is false. Use is NSArray.
  • Subclassing NSArray like a normal class. It has no storage; you must implement the primitives and wrap a real array.

They might follow up with

  • Why use the pattern? It lets Foundation pick compact or fast representations (empty, single-element, mutable) without changing the API.
  • Is this the same as tagged pointers? No, but both let small values avoid an ordinary allocation. See the runtime layout section.

Related questions

  • 4.3.1 What does NSObject provide, and when should a Swift class inherit from it?
  • 2.9.3 What does bridging cost, and where does it show up in a profile?

← What does NSObject provide, and when should a Swift class inherit…   How does NSCopying work, and why use a copy property for strings and… →