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, 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
superat 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
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:, notisMemberOfClass:, 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
copyof an immutable object may legitimately returnself(retained), which is why copying anNSStringis 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, useNSKeyedArchiver-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 — anNSStringis almost never exactly anNSString. UseisKindOfClass:. -
They are effectively closed to subclassing. Doing it properly means implementing the cluster’s full set of primitive methods and overriding
alloc/initto return your own instance — rarely worth it. -
An initializer may return a different object than
allocproduced, which is precisely whyself = [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 ( |
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
-
Classes and Objects —
super, designated initializers anddealloc. -
Protocols and Delegation — the usual alternative to subclassing.
-
Categories and Extensions — adding behaviour to classes you do not own.
-
Collections and Fast Enumeration — the cluster classes in practice.