Inheritance and Polymorphism

This section documents modern Objective-C as implemented by Apple Clang in the current Xcode release — Objective-C 2.0 plus ARC, literals and subscripting, instancetype, Clang modules, lightweight generics, nullability and @available — as published at Apple Developer Documentation and the Clang Objective-C specifications, which are the references these pages are written and verified against.

This content was generated with the assistance of AI and should be verified against developer.apple.com before being relied on in production.

This section’s bibliography lists the reference material consulted while preparing these pages.

Objective-C has single inheritance: every class has exactly one superclass. Multiple-inheritance-shaped problems are solved with protocols (interface sharing), categories (behaviour sharing) and composition (implementation sharing) instead — see Protocols and Delegation and Categories and Extensions.

Subclassing and Overriding

A subclass names its superclass in the @interface line and overrides a method simply by redefining it — no virtual, no override, no declaration in the header:

// Shape.h
@interface Shape : NSObject
@property (nonatomic, copy) NSString *label;
- (double)area;
- (NSString *)describe;
@end

// Circle.h
@interface Circle : Shape
@property (nonatomic, assign) double radius;
@end

// Circle.m
@implementation Circle

- (double)area {                       // overrides Shape's -area
    return M_PI * self.radius * self.radius;
}

- (NSString *)describe {               // extends rather than replaces
    return [NSString stringWithFormat:@"%@ with area %.2f", [super describe], self.area];
}

@end

Because dispatch is dynamic, every method is effectively virtual and the override is chosen at run time from the receiver’s actual class — see Messaging and Selectors.

Calling super

super restarts the method lookup at the superclass while leaving self as the receiver. Three rules cover almost every case:

  • Initializers must call `super’s designated initializer first, before touching their own state.

  • dealloc (under MRR) calls [super dealloc] last; under ARC the compiler does it and you must not.

  • Overrides that extend behaviour call super at the point where the inherited work belongs — first when you are adding to a result, last when you are cleaning up. Overrides that replace behaviour omit it deliberately.

- (instancetype)initWithRadius:(double)radius {
    self = [super init];            // first
    if (self) {
        _radius = radius;
    }
    return self;
}

Some superclass methods must be called; Apple’s headers say so per method (-viewDidLoad, -encodeWithCoder:). When in doubt, call super.

A Small Hierarchy

classDiagram class NSObject { +isEqual(id) BOOL +hash() NSUInteger +description() NSString +copy() id } class Shape { +NSString label +area() double +describe() NSString +isEqual(id) BOOL +hash() NSUInteger } class Circle { +double radius +area() double } class Rectangle { +double width +double height +area() double } class Square { +double side } NSObject <|-- Shape Shape <|-- Circle Shape <|-- Rectangle Rectangle <|-- Square class NSCopying { <<protocol>> +copyWithZone(NSZone) id } NSCopying <|.. Shape : conforms note for Shape "Abstract: -area raises via
doesNotRecognizeSelector:
Subclasses must override it."

Shape here is abstract by convention: it declares -area so that callers can rely on it, but has no sensible implementation of its own.

Abstract Classes

Objective-C has no abstract keyword. The idiom is to make the unimplemented method fail loudly:

@implementation Shape

- (double)area {
    [self doesNotRecognizeSelector:_cmd];
    return 0;                         // never reached; silences the compiler
}

@end

doesNotRecognizeSelector: raises NSInvalidArgumentException with a message naming the class and selector — exactly the error the run time itself produces for an unimplemented method. _cmd is the implicit selector argument of the current method, so the message is accurate without hard-coding a name.

Two alternatives appear in real code: NSAssert(NO, @"subclass must override %@", NSStringFromSelector(_cmd)), which compiles out in release builds, and preventing instantiation of the base class outright:

@interface Shape : NSObject
- (instancetype)init NS_UNAVAILABLE;      // a bare Shape cannot be created
@end

A protocol is often the better tool: it expresses "you must implement this" as a compile-time requirement, which doesNotRecognizeSelector: cannot.

The isEqual: and hash Contract

NSObject’s `isEqual: is pointer identity. Any class with value semantics should override it — and must override hash at the same time.

The contract: if [a isEqual:b] is YES, then [a hash] == [b hash] must be true. The converse is not required. Breaking it makes an object silently unfindable in an NSSet or as an NSDictionary key, with no error reported.

@implementation Shape

- (BOOL)isEqual:(id)object {
    if (self == object) {
        return YES;                                   // identity fast path
    }
    if (![object isKindOfClass:[Shape class]]) {
        return NO;                                    // also handles object == nil
    }
    return [self isEqualToShape:(Shape *)object];
}

// The typed companion, following Foundation's isEqualToString:/isEqualToArray: convention.
- (BOOL)isEqualToShape:(Shape *)other {
    if (!other) {
        return NO;
    }
    BOOL sameLabel = (self.label == other.label) || [self.label isEqualToString:other.label];
    return sameLabel && self.area == other.area;
}

- (NSUInteger)hash {
    return [self.label hash] ^ (NSUInteger)self.area;
}

@end

Three practical rules:

  • Hash only on immutable state. If an object’s hash changes while it sits in a set or dictionary, it becomes unreachable. Prefer to make the properties that participate in equality readonly.

  • Use isKindOfClass:, not isMemberOfClass:, unless subclasses genuinely must be unequal to their parents.

  • Provide the typed variant (isEqualToShape:) as well: it is faster, clearer at the call site, and the house style throughout Foundation.

NSCopying and NSMutableCopying

copy and mutableCopy are NSObject methods that simply forward to the two protocols. To make your class copyable, adopt NSCopying and implement copyWithZone::

@interface Shape : NSObject <NSCopying, NSMutableCopying>
@end

@implementation Shape

- (id)copyWithZone:(NSZone *)zone {
    // `[self class]` -- not `[Shape class]` -- so subclasses copy correctly.
    Shape *copy = [[[self class] allocWithZone:zone] init];
    if (copy) {
        copy->_label = [_label copy];
        // Nothing to copy for `area`: it is a method each subclass computes
        // from its own state, not stored state on `Shape`.
    }
    return copy;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
    return [[MutableShape allocWithZone:zone] initWithShape:self];
}

@end

NSZone is a historical parameter; the modern run time ignores it, but the signature keeps it. Two points to get right:

  • A copy of an immutable object may legitimately return self (retained), which is why copying an NSString is nearly free. Only do this if your class is genuinely immutable.

  • Copies are shallow by default. [array copy] produces a new array holding the same element objects. For a deep copy, use NSKeyedArchiver-based archiving or copy the elements yourself.

The relationship between copy and the copy property attribute is direct: a copy property’s synthesised setter sends copy to its argument, so any type used with a copy property must conform to NSCopying.

Class Clusters

Several of Foundation’s most-used classes are class clusters: the name you write is an abstract public facade, and the object you actually get is a private subclass chosen for the data:

NSString *a = @"literal";
NSString *b = [NSString stringWithFormat:@"%d", 42];
NSArray  *c = @[];
NSNumber *d = @42;

NSLog(@"%@", NSStringFromClass([a class]));   // __NSCFConstantString
NSLog(@"%@", NSStringFromClass([b class]));   // __NSCFString
NSLog(@"%@", NSStringFromClass([c class]));   // __NSArray0
NSLog(@"%@", NSStringFromClass([d class]));   // __NSCFNumber

NSString, NSArray, NSDictionary, NSSet, NSNumber, NSData and NSDate are all clusters. The consequences for your code:

  • isMemberOfClass: is useless on them — an NSString is almost never exactly an NSString. Use isKindOfClass:.

  • They are effectively closed to subclassing. Doing it properly means implementing the cluster’s full set of primitive methods and overriding alloc/init to return your own instance — rarely worth it.

  • An initializer may return a different object than alloc produced, which is precisely why self = [super init] must assign the result.

Extending a Cluster Class

Two good options, neither of them subclassing:

A category adds methods to the existing class — the right choice for pure behaviour:

@interface NSString (MyValidation)
- (BOOL)my_isValidEmailAddress;
@end

A composite object wraps an instance and forwards to it — the right choice when you need extra state:

@interface ValidatedString : NSObject
@property (nonatomic, copy, readonly) NSString *value;      // the wrapped object
@property (nonatomic, readonly) BOOL isValid;               // the added state
@end

Message forwarding can make the wrapper transparent, so unrecognised messages go to the wrapped object automatically — see Dynamic Method Resolution and Forwarding.

Subclass or Compose?

Subclass when Compose when

The relationship is genuinely "is-a" and the Liskov substitution holds

The relationship is "has-a" or "uses-a"

The framework expects it (UIViewController, NSOperation, XCTestCase)

You need only part of the other class’s behaviour

You are overriding documented extension points

The superclass is a class cluster, or is otherwise closed

The base class is yours and you control both sides

The base class is someone else’s and may change

Objective-C’s culture leans towards composition and delegation far more than, say, classic Java’s: Cocoa’s own architecture is built on delegates, data sources and target-action rather than deep hierarchies. Prefer a protocol plus a delegate over a subclass whenever the customisation is behavioural. And prefer a category when all you want is to add a method to a class you do not own.

See Also