Dynamic Messaging: Runtime Lookup, Optional Methods, and Existence Checks
Explain how Objective-C resolves a message send at runtime rather than at compile time, distinguish that dynamic dispatch from C's compile-time function binding, and safely call @optional protocol methods by checking respondsToSelector: and instancesRespondToSelector: before sending them.
Every [receiver message] you have written so far in this course — [person description], [Person personWithName:@"Alice" age:30], [[Person alloc] init] — looks, on the page, like an ordinary function call. It is not one, and the difference is not cosmetic. In C, calling computeTotal(price, tax) is resolved by the compiler at compile time: the compiler looks at the function's declared signature, decides exactly which machine code address to jump to, and bakes that decision permanently into the compiled binary. Objective-C message sends work completely differently — a message like [person description] is resolved at runtime, by asking the actual object what to do with the message description, every single time that line executes. This lesson pulls apart what that difference really means, how it makes certain patterns possible that C simply cannot express, and how to use it safely: checking whether an object actually implements a method before sending it a message that might not exist, which matters enormously once @optional protocol methods enter the picture.
Compile-Time Binding in C vs. Runtime Lookup in Objective-C
When you call a C function, the compiler needs to know, at compile time, exactly one thing: the function's signature (its name, parameter types, and return type), which it gets from a prototype/header. Armed with that, the compiler emits a direct call instruction to a fixed address — decided once, permanently, when the program is built. If the function doesn't exist, or its signature doesn't match how you're calling it, that's a compile error or link error; the program will not even finish building, let alone run. This is compile-time binding, and it's why the C lessons earlier in this course could always reason about function calls as fixed, resolved jumps baked into the binary.
A message send in Objective-C, [person description], compiles to something fundamentally different: a call to the Objective-C runtime function objc_msgSend(person, @selector(description)) (simplified). What actually runs when this executes is decided at runtime, not compile time: objc_msgSend looks at person's actual class, searches that class's method list for something matching the selector description, and if it isn't found there, walks up the class's inheritance chain (superclass, then its superclass, and so on) checking each one in turn, until it either finds an implementation to run or exhausts the chain. This search — runtime lookup — happens fresh on every single message send, not once at build time.
The practical consequence is that Objective-C message sends are far more forgiving, and far more flexible, than C function calls, precisely because the decision of 'what code actually runs' is deferred all the way to the moment the message is sent, using the real, live object at hand rather than a fixed compile-time guess. This is the foundation every other topic in this lesson builds on: dynamic dispatch, optional protocol methods, and forwarding all exist only because message resolution happens at runtime instead of compile time.
• C function calls: resolved at compile time to a fixed jump address, baked permanently into the binary. • Objective-C message sends: resolved at runtime via objc_msgSend, which looks up the method on the receiver's actual class at the moment the message is sent. • Runtime lookup walks the class's method list, then its superclass, then its superclass's superclass, and so on up the inheritance chain. • This lookup happens fresh on every message send, not once at build time — the core reason Objective-C messaging is more dynamic than a C function call.
Dynamic Dispatch: The Same Message, Different Code, Decided at Runtime
Because the method that actually runs is chosen by inspecting the receiver's real class at the moment of the send, the exact same line of source code, [shape draw], can run entirely different implementations depending on what kind of object shape actually points to when that line executes — a Circle's -draw, or a Square's -draw, or any other subclass's own -draw, all reached through the identical message send. This behavior is called dynamic dispatch, and it is the mechanism that makes polymorphism through method overriding work at all (overriding itself is covered fully in the Inheritance module later in this course) — the calling code never needs to know, or check, which concrete subclass it's actually holding; it just sends the message and trusts runtime lookup to find the right implementation.
Contrast this with how you would have to simulate the same idea in plain C: without message sends, you'd typically need a manually maintained function-pointer table or an explicit switch on some kind of type tag, checking 'is this a circle or a square?' before deciding which function to call by hand. Objective-C's dynamic dispatch does this automatically, as an intrinsic part of how every message send works, without you writing any dispatch logic yourself — the object itself effectively carries the knowledge of which code should run in response to a given message.
It's worth being precise about what varies and what doesn't: the selector being sent, draw, is the same text in every case — what varies is only which method implementation actually gets matched to that selector, and that decision is made fresh, per object, per send, based on the object's real class. This is why two variables declared with the exact same static type can still behave completely differently when sent the same message, as long as the objects they actually point to at runtime are different concrete subclasses.
• Dynamic dispatch: the same message send ([shape draw]) can run different code depending on the receiver's actual runtime class. • This is the mechanism underlying overriding/polymorphism — calling code never has to check which subclass it holds. • In plain C, the equivalent behavior would require manual function-pointer tables or type-tag switches; Objective-C does it automatically via runtime lookup. • The selector sent is identical text every time; only the matched implementation varies, based on the receiver's real class at that moment.
Protocols, @optional Methods, and the Problem They Create
A protocol declares a set of method signatures that a class can promise to implement by adopting it, and by default every method listed in a protocol is required — a class that adopts the protocol but doesn't implement a required method gets a compiler warning. Objective-C also lets a protocol mark some of its methods @optional, meaning any given adopter may or may not actually implement them; this is extremely common for delegate protocols, where a delegate typically only cares about, and only implements, a handful of the many possible callback methods a protocol offers.
This flexibility creates a genuine risk: if a protocol declares @optional - (void)connectionDidFinishLoading:(id)connection;, and some class conforms to that protocol but simply never implements this particular optional method, then sending it that message anyway, [delegate connectionDidFinishLoading:self], is not a compile error (the protocol declared the method exists), but it is a runtime crash — 'unrecognized selector sent to instance' — because runtime lookup walks the receiver's actual class hierarchy, finds nothing matching that selector anywhere, and gives up.
This is precisely why calling code can never simply assume an @optional method exists just because the protocol declares it; the protocol only declares that the method is legal to implement, not that any particular object actually did. Before sending an optional protocol method, you must first ask the object, at runtime, whether it actually responds to that selector — which is exactly what the existence-check methods in the next section exist to do.
• Protocol methods are required by default; @optional marks a method an adopter may skip implementing. • Adopting a protocol with an unimplemented @optional method is not a compile error. • Sending an @optional message to an object that never implemented it is a runtime crash ('unrecognized selector'), not a compile error. • Calling code must check whether an @optional method is actually implemented before sending it — never assume it exists just because the protocol lists it.
respondsToSelector: and instancesRespondToSelector:
-respondsToSelector:, defined on NSObject and inherited by essentially every Objective-C object, is the standard instance-level existence check: [delegate respondsToSelector:@selector(connectionDidFinishLoading:)] returns a BOOL — YES if that specific object's class (or something in its inheritance chain) actually has an implementation for that selector, NO otherwise — without ever sending the message itself, so it's completely safe to call even on an object you know nothing about beyond that it conforms to some protocol. The idiomatic pattern for calling an optional method is therefore always a guard: if ([delegate respondsToSelector:@selector(connectionDidFinishLoading:)]) { [delegate connectionDidFinishLoading:self]; } — check first, send only if the check passed.
@selector(...) itself, used inside that check, is the compile-time syntax for referring to a selector (a method name, essentially, as a distinct runtime value of type SEL) without actually sending it as a message — it names the method you're asking about, rather than calling it. This is exactly how respondsToSelector: can ask 'does this object implement this method' as a completely separate step from 'run this method', since @selector(connectionDidFinishLoading:) alone triggers no method execution at all.
+instancesRespondToSelector:, the class-method counterpart, asks the same existence question about a class as a whole rather than about one specific instance: [NSObject instancesRespondToSelector:@selector(description)] asks 'will instances of this class respond to this selector', which is useful when you have a Class reference rather than an actual instance in hand — for example, deciding in advance whether a class you're about to instantiate will support a given optional method, before any instance of it exists yet. In the overwhelming majority of everyday delegate-callback code, though, -respondsToSelector: on the actual delegate instance is the one you'll reach for.
• -respondsToSelector: (instance method, from NSObject): returns YES/NO for whether this specific object implements a given selector, without sending it. • Idiomatic pattern: if ([obj respondsToSelector:@selector(foo:)]) { [obj foo:arg]; } — always check before sending an @optional method. • @selector(methodName:) names a method as a SEL value without invoking it — how the check can ask about a method without calling it. • +instancesRespondToSelector: (class method): asks whether instances of a class respond to a selector, useful when you have a Class but no instance yet.
A Preview of Forwarding
There is one more piece of the runtime-lookup story worth knowing about now, even though its full depth belongs to its own dedicated lesson later in this course: what happens when objc_msgSend walks the entire inheritance chain and still finds nothing matching the selector — the exact situation -respondsToSelector: is meant to help you avoid triggering. Before Objective-C gives up and crashes with 'unrecognized selector', the runtime actually offers the receiving object several last-chance opportunities to redirect that message somewhere else entirely — to itself dynamically, or to a completely different object — rather than failing outright. This mechanism is called message forwarding.
Forwarding is what lets techniques like dynamic proxy objects work: an object that has no compiled implementation for a huge range of selectors can still respond correctly to all of them by forwarding each one, at the moment it arrives, to some other object that does know how to handle it — decided entirely at runtime, with no compile-time knowledge of which selectors will ever be sent. It is, in a real sense, the most dynamic escape hatch runtime lookup offers: even total absence of a compiled method does not have to mean a crash.
The full mechanics — forwardingTargetForSelector:, methodSignatureForSelector:, and forwardInvocation:, the three methods the runtime actually consults during this process — are covered in complete depth in the dedicated Message Forwarding module later in this course. For now, the essential fact to carry forward is simply that runtime lookup failing is not automatically the end of the story; it is one more place where Objective-C defers a decision to runtime that a language like C would have had to settle permanently at compile time.
• When runtime lookup finds no implementation anywhere in the inheritance chain, the runtime does not crash immediately — it offers the object chances to redirect (forward) the message first. • This mechanism is called message forwarding, and it enables patterns like dynamic proxy objects that handle selectors with no compiled implementation at all. • Full mechanics (forwardingTargetForSelector:, methodSignatureForSelector:, forwardInvocation:) are covered in the dedicated Message Forwarding module later in this course. • Forwarding is a separate, later-stage mechanism from respondsToSelector: — the check tells you in advance; forwarding is what the runtime itself can attempt if you don't check and no method is found.
Code Examples
downloadDidReportProgress: is @optional, so Downloader cannot assume any given delegate implements it -- calling it unconditionally would crash for a delegate that skipped it. Guarding the send with respondsToSelector: first makes the call safe regardless of which optional methods a particular delegate chose to implement. downloadDidStart is @required, but even required methods are commonly guarded the same way defensively in real code, since the compiler's @required warning does not guarantee a class actually complied with it.
Every iteration sends the identical message, draw, to a variable statically typed as Shape *. Runtime lookup inspects each object's real, dynamic class at the moment the message is sent -- Circle, then Square, then plain Shape -- and dispatches to whichever -draw implementation actually belongs to that object, without the loop containing any type-checking logic at all. This is dynamic dispatch in action.
instancesRespondToSelector: is sent to the Class object itself (Downloader, obtained via [Downloader class]) and answers whether instances of that class -- generically, before any particular one exists -- implement a selector. respondsToSelector: is sent to an actual instance (d) and answers the same question for that one specific object. In ordinary delegate-callback code you almost always have an instance in hand, so respondsToSelector: is the one used far more often.
[self.delegate downloadDidReportProgress:progress]; // called unconditionally if ([self.delegate respondsToSelector:@selector(downloadDidReportProgress:)]) { [self.delegate downloadDidReportProgress:progress]; } downloadDidReportProgress: is an @optional protocol method. A delegate that adopted the protocol without implementing it has no matching method anywhere in its class hierarchy, so sending it this message unconditionally crashes with 'unrecognized selector sent to instance' at runtime, even though it compiled without error.
// treating a protocol method as guaranteed just because it's declared: - (void)someMethod { [self.delegate optionalCallback]; // assumed to always exist } - (void)someMethod { if ([self.delegate respondsToSelector:@selector(optionalCallback)]) { [self.delegate optionalCallback]; } }
Declaring a method in a protocol (even a @required one) only describes what a conforming class is supposed to implement -- it does not guarantee, at the moment a specific message is sent, that the actual receiver in hand truly implemented it correctly. Guarding with respondsToSelector: before any protocol-declared call the receiver's type doesn't statically confirm is the safe, defensive habit.
// thinking of a message send as identical to a C function call: // "the compiler decides which code runs, just like calling computeTotal()"
// a message send is resolved at runtime by inspecting the receiver's actual class: [shape draw]; // which -draw runs is decided fresh, at this exact send, by shape's real class
Unlike a C function call, which the compiler binds to one fixed address forever, an Objective-C message send is resolved by runtime lookup on every single send, using the receiver's actual dynamic class at that moment. Treating message sends as compile-time-fixed like C calls misses exactly the mechanism that makes dynamic dispatch and optional methods possible.
What is the essential difference between a C function call and an Objective-C message send? A. There is no real difference; both are compiled identically B. A C function call is resolved to a fixed address at compile time; a message send is resolved by runtime lookup on the receiver's actual class at the moment it executes C. Message sends are always slower and never used in performance-critical code D. C function calls can be overridden by subclasses, but message sends cannot
B. A C function call is resolved to a fixed address at compile time; a message send is resolved by runtime lookup on the receiver's actual class at the moment it executes — C binds a function call permanently at compile time based on the function's prototype. Objective-C defers this decision to runtime, where objc_msgSend searches the receiver's actual class (and its superclasses) for a matching method every time the message is sent.
What is dynamic dispatch? A. A way to allocate memory dynamically at runtime B. The mechanism by which the exact same message send can run different method implementations depending on the receiver's actual runtime class C. A special syntax for calling class methods only D. A compiler optimization that removes unused methods
B. The mechanism by which the exact same message send can run different method implementations depending on the receiver's actual runtime class — Dynamic dispatch means the code that actually runs in response to a message like [shape draw] is chosen based on the receiver's real, dynamic class at the moment of the send -- Circle's -draw, Square's -draw, etc. -- which is what makes overriding-based polymorphism work.
Why is it dangerous to send an @optional protocol method to a delegate without checking first? A. It isn't dangerous; @optional methods always have a default no-op implementation B. The compiler blocks the call entirely, so no danger exists C. It compiles fine, but if the delegate never implemented that method, sending it crashes at runtime with an unrecognized-selector error D. It silently does nothing if unimplemented
C. It compiles fine, but if the delegate never implemented that method, sending it crashes at runtime with an unrecognized-selector error — @optional only means a conforming class is not required to implement the method -- it doesn't guarantee any particular object did. Sending the message to an object that never implemented it compiles successfully but crashes at runtime, which is exactly why respondsToSelector: guards are necessary.
What does [delegate respondsToSelector:@selector(foo:)] actually do? A. It calls -foo: on delegate and returns its result B. It returns YES/NO for whether delegate's class (or an ancestor class) implements -foo:, without ever invoking -foo: itself C. It only works on class methods, never instance methods D. It always returns YES for any @required protocol method
B. It returns YES/NO for whether delegate's class (or an ancestor class) implements -foo:, without ever invoking -foo: itself — respondsToSelector: is a pure existence check -- it inspects whether the receiver's class hierarchy contains an implementation matching the given selector and returns a BOOL, without sending that message or running any of its code.
What is the difference between -respondsToSelector: and +instancesRespondToSelector:? A. They are identical and interchangeable in every situation B. -respondsToSelector: is sent to a specific instance and asks about that object; +instancesRespondToSelector: is sent to a Class object and asks whether instances of that class generically implement a selector C. +instancesRespondToSelector: only works on NSObject itself D. -respondsToSelector: can only check @required methods
B. -respondsToSelector: is sent to a specific instance and asks about that object; +instancesRespondToSelector: is sent to a Class object and asks whether instances of that class generically implement a selector — -respondsToSelector: is an instance method answering the question for one specific object you already have. +instancesRespondToSelector: is a class method, useful when you only have a Class reference (e.g. via [SomeClass class]) and want to know in advance whether instances of it would respond, before any instance exists.
According to this lesson, what happens if runtime lookup fails to find any matching method anywhere in the receiver's inheritance chain? A. The program always crashes immediately with no alternative B. The runtime gives the object a last-chance opportunity to redirect the message elsewhere through message forwarding, before finally failing if that also doesn't handle it C. The compiler silently substitutes an empty method body D. The message is simply ignored with no consequence at all
B. The runtime gives the object a last-chance opportunity to redirect the message elsewhere through message forwarding, before finally failing if that also doesn't handle it — Before crashing with 'unrecognized selector', the runtime offers the object forwarding opportunities to redirect the message to itself dynamically or to another object entirely. The full mechanics of this are covered in the dedicated Message Forwarding module later in this course.
@protocol DownloadDelegate <NSObject>
@required
- (void)downloadDidStart;
@optional
- (void)downloadDidReportProgress:(float)fraction; // not every delegate cares about this
@end
@interface Downloader : NSObject
@property (nonatomic, weak) id<DownloadDelegate> delegate;
- (void)start;
@end
@implementation Downloader
- (void)start {
if ([self.delegate respondsToSelector:@selector(downloadDidStart)]) {
[self.delegate downloadDidStart];
}
for (float progress = 0.0; progress <= 1.0; progress += 0.25) {
if ([self.delegate respondsToSelector:@selector(downloadDidReportProgress:)]) {
[self.delegate downloadDidReportProgress:progress];
}
}
}
@end@interface Shape : NSObject
- (void)draw;
@end
@implementation Shape
- (void)draw { NSLog(@"Drawing a generic shape"); }
@end
@interface Circle : Shape
@end
@implementation Circle
- (void)draw { NSLog(@"Drawing a circle"); }
@end
@interface Square : Shape
@end
@implementation Square
- (void)draw { NSLog(@"Drawing a square"); }
@end
NSArray<Shape *> *shapes = @[[[Circle alloc] init], [[Square alloc] init], [[Shape alloc] init]];
for (Shape *shape in shapes) {
[shape draw]; // exact same message send, different code runs each time
}
// Output:
// Drawing a circle
// Drawing a square
// Drawing a generic shapeClass downloaderClass = [Downloader class];
if ([downloaderClass instancesRespondToSelector:@selector(start)]) {
NSLog(@"Downloader instances support -start");
}
// Compare to checking an actual instance:
Downloader *d = [[Downloader alloc] init];
if ([d respondsToSelector:@selector(start)]) {
NSLog(@"This specific instance responds to -start");
}