performSelector: Dynamic Invocation and Its Real Limitations

Learn the performSelector: family of methods for invoking a message when the method name isn't known until runtime, and understand its genuine limitations — no compile-time type checking, restricted argument support, and ARC memory-management hazards — that make it a poor default choice compared to typed calls or blocks.

The previous lesson established that a SEL is a first-class value: something you can store, pass around, and build dynamically from a string via NSSelectorFromString. But having a SEL value sitting in a variable is not, by itself, enough to actually send a message with it — ordinary bracket syntax like [person sayHello] requires the selector to be written literally in your source code. performSelector: closes that gap, letting you send a message using a SEL value determined entirely at runtime. This lesson covers how performSelector: works, why it exists, and — just as importantly — the substantial list of real limitations that mean it should be reached for deliberately, as a specific tool for a specific problem, rather than as a general substitute for ordinary message sends.

Why performSelector: Exists: Invocation When the Method Isn't Known Until Runtime

Ordinary message syntax, [person sayHello], requires the selector sayHello to be written directly in your source code, where the compiler can see it. But the previous lesson's Button example showed a real scenario where that isn't possible: a SEL value stored in a property, configured by other code, possibly built from a string read from a configuration file or determined by some runtime condition. In that situation, you have a SEL value in a variable, but no way to write [receiver <the selector variable>] — bracket syntax doesn't accept a variable in the selector position.

performSelector: solves exactly this problem: it's an ordinary method, declared on NSObject (and therefore available on essentially every object you'll work with), that takes a SEL value as an ordinary argument and, internally, sends that selector to the receiver on your behalf. [button performSelector:button.action]; takes whatever SEL happens to be stored in button.action at that moment and sends it to button — accomplishing what plain bracket syntax structurally cannot, because bracket syntax needs the selector as literal text, not as a runtime value.

This capability is genuinely useful in specific situations: implementing something like the Button/target-action pattern from the previous lesson, building a dispatch mechanism that maps external string commands to method invocations, or writing infrastructure code (like parts of a testing framework, or a plugin system) that needs to invoke methods it cannot possibly know about in advance, because they're supplied by code that doesn't exist yet when the infrastructure itself is written.

• Bracket syntax [receiver selector] requires the selector to be written literally in source code — it cannot accept a SEL stored in a variable directly. • performSelector: is an ordinary NSObject method that accepts a SEL as an argument and sends it to the receiver on your behalf. • [button performSelector:button.action]; sends whatever selector happens to be stored in that property at the moment it runs. • Genuinely useful for target-action patterns, dispatch tables, and infrastructure code that must invoke methods it cannot know about in advance.

The performSelector: Family: With and Without Arguments

NSObject actually provides three closely related methods, differing in how many arguments they let you pass along with the dynamically-determined selector. performSelector: takes only the selector itself, for invoking a no-argument method: [person performSelector:@selector(sayHello)]; behaves, at runtime, essentially the same as [person sayHello];, just with the selector supplied as a value rather than written literally.

performSelector:withObject: adds a single object argument: [person performSelector:@selector(sayHelloTo:) withObject:@"World"]; is the dynamic equivalent of [person sayHelloTo:@"World"];. And performSelector:withObject:withObject: extends this to two object arguments, for a two-argument selector like setName:age: — except, as the next section covers, age being a primitive NSInteger rather than an object is already where this family's limitations begin to show.

It's worth noting the ceiling here explicitly: NSObject's built-in performSelector: family stops at two arguments, and both of those arguments must be object pointers (or nil). There is no performSelector:withObject:withObject:withObject: for three arguments, and there is no direct way to pass a primitive value like an int or a struct through any of these three methods.

• performSelector: — invokes a no-argument selector dynamically. • performSelector:withObject: — invokes a one-argument selector, passing a single object argument. • performSelector:withObject:withObject: — invokes a two-argument selector, passing two object arguments. • The family stops at two arguments, and both must be object pointers (or nil) — this is a hard ceiling on what it can directly express.

Limitation One: No Compile-Time Type Checking

When you write [person sayHelloTo:@"World"] directly, the compiler checks, as best it can given person's declared type, whether sayHelloTo: is actually a method that class is known to implement, and whether @"World" is a reasonable argument type for it — catching a surprising number of typos and mismatches before the program ever runs. performSelector: gives almost none of that up front: the selector is just a SEL value, possibly built from a string at runtime via NSSelectorFromString, and the compiler has no way to verify, while compiling your code, that the receiver's class actually implements that selector at all.

The practical consequence is that a mistake — a typo'd selector name in a string, a selector for a method that was renamed or removed, a selector that genuinely exists but on some other, unrelated class — surfaces only when that exact line of code actually executes, typically producing a runtime crash (an 'unrecognized selector' error) rather than a compiler error caught immediately, at the moment you introduced the mistake, long before the program ever ran.

This isn't a defect in performSelector: so much as an unavoidable consequence of what it's for: dynamic invocation, by definition, works with information the compiler cannot see in advance. But it does mean that every use of performSelector: trades away a genuine safety net that ordinary message sends provide for free, and that tradeoff should be made deliberately, only when the dynamism is actually needed, not casually.

• Ordinary message sends let the compiler check, ahead of time, whether the selector is plausible for the receiver's declared type. • performSelector: gives up nearly all of that checking, because the selector is only known as a runtime SEL value. • A typo'd or mismatched selector surfaces as a runtime crash ('unrecognized selector'), not a compile-time error. • This tradeoff is inherent to dynamic invocation, not a bug — but it means performSelector: should be used deliberately, not as a casual default.

Limitation Two: Restricted Argument Support

The performSelector: family's arguments must be object pointers — id-compatible values — because the underlying mechanism is built around passing ordinary object arguments through a small, fixed set of method signatures. This immediately rules out directly invoking, through performSelector:, any method that expects a primitive argument like NSInteger, CGFloat, or BOOL, or a C struct like CGRect, without some form of manual wrapping first (for instance, boxing a primitive into an NSNumber and having the target method unwrap it itself — which only works if that method was specifically written to expect a wrapped value, not a plain primitive).

The two-argument ceiling compounds this: a method like a hypothetical setName:age:height: selector, taking three arguments, simply cannot be invoked through any method in the built-in performSelector: family at all — there is no version that accepts three separate arguments. Working around this typically requires falling back to NSInvocation, a considerably more elaborate mechanism (covered elsewhere) purpose-built for constructing and invoking calls with arbitrary argument counts and types, precisely because performSelector: was never designed to handle that generality.

The practical upshot is that performSelector: works cleanly only for a fairly narrow slice of real method signatures: zero, one, or two arguments, every one of them already an object pointer. Any method outside that slice needs either restructuring (redesigning it to accept object-wrapped arguments so it fits the family's constraints) or a different, heavier mechanism entirely.

• Arguments to performSelector: must be object pointers (id-compatible) — primitives and structs cannot be passed directly. • A primitive argument like NSInteger or a struct like CGRect needs manual wrapping (e.g. into NSNumber) and cooperation from the target method to unwrap it. • The family has a hard ceiling of two arguments; a three-argument selector cannot be invoked through it at all. • Methods outside performSelector:'s narrow slice (object-only, ≤2 args) typically require NSInvocation instead, a heavier and more general mechanism.

Limitation Three: ARC Warnings and Memory-Management Hazards

Under Automatic Reference Counting (ARC), the compiler normally inspects a method's name and declared return type to decide how to manage the memory of whatever it returns — certain naming conventions (like a method starting with alloc, new, copy, or mutableCopy) signal that the caller owns the returned object and must eventually release it, while most other methods return something the caller does not own outright. ARC performs this analysis by actually looking at the method being called, at compile time, using its name and signature.

performSelector: breaks this analysis, because the compiler, at the call site, does not know which method is actually going to run — it only sees a generic performSelector: call returning a generic id, with no visibility into what specific method the runtime will eventually dispatch to. This means ARC cannot apply its usual naming-convention-based memory-management rules at all for the return value of a performSelector: call, and the compiler flags this with a specific warning, -Wperformselector-leaks, precisely because it genuinely cannot verify whether the returned object should be retained, released, or left alone — the safety net ARC normally provides silently does not apply here.

The practical hazard is real: code that calls performSelector: on a method that happens to follow the alloc/new/copy naming convention, expecting ARC to manage the returned object correctly the way it would for a directly-written call, can leak memory or, less commonly, over-release, precisely because ARC's usual analysis never got to run for that particular return value. This is not a hypothetical footnote — it is a specifically named, real compiler warning that exists because this exact hazard is common enough in real-world code to warrant its own diagnostic.

• ARC normally infers memory-management rules for a returned object by analyzing the called method's name/signature at compile time. • performSelector: hides which actual method will run from the compiler, so ARC cannot apply that analysis to its return value. • The compiler warns about this specifically via -Wperformselector-leaks, because it cannot verify correct retain/release behavior for the result. • Real hazard: code relying on ARC to correctly manage a performSelector:-returned object (especially one following alloc/new/copy naming) can leak or, less commonly, over-release.

Why Typed Calls or Blocks Are Usually the Better Tool

Given these three limitations together — no compile-time checking, a narrow object-only, two-argument ceiling, and ARC's inability to manage the return value correctly — performSelector: is best understood as a narrow, special-purpose tool rather than a general-purpose alternative to ordinary message sends. It earns its keep specifically in situations where the selector genuinely cannot be known until runtime: configurable target-action patterns, plugin systems, or infrastructure that must invoke caller-supplied behavior it has no advance knowledge of.

For the much more common situation — needing to defer, pass around, or configure 'what to do next' as a value — Objective-C's blocks (covered in their own dedicated module) are usually the better tool today. A block captures actual executable code, with normal compile-time type checking of both its parameters and its return value, and without any of performSelector:'s ARC memory-management blind spot, since the compiler can see exactly what a block does and returns, the same way it can for any ordinary function or method. Where older Objective-C code reaches for performSelector: to achieve dynamic, configurable behavior, a great deal of that same flexibility is now achievable with a block instead, with real compile-time safety intact.

The practical guidance, then, is straightforward: when you already know, at the point you're writing the code, exactly which method you want to call, write an ordinary typed message send — [person sayHello], not [person performSelector:@selector(sayHello)] — because the ordinary form gives you compiler checking for free and carries none of performSelector:'s hazards. Reach for performSelector: only when the selector is genuinely unknown until runtime and a block-based design doesn't fit the situation, and when you do reach for it, be deliberate about the argument types and return-value memory management you're bypassing the compiler's usual protection for.

• performSelector: is a narrow tool for genuinely dynamic invocation, not a general substitute for ordinary message sends. • Blocks (covered in a dedicated module) usually replace the flexibility performSelector: historically provided, with full compile-time type checking and no ARC blind spot. • Default to an ordinary typed message send whenever the selector is already known when you're writing the code. • Reach for performSelector: deliberately, only when the selector is truly unknown until runtime and a block doesn't fit — and be aware of the tradeoffs each time you do.

Code Examples

The three built-in performSelector: methods cover zero, one, and two arguments, and every argument must be an object pointer -- note @30 would itself need to be an NSNumber (boxed via the @30 literal), not a raw NSInteger, to satisfy that requirement even for the two-argument case. The commented-out three-argument call illustrates the hard ceiling: no version of performSelector: accepts three separate arguments, so a method with that many parameters cannot be invoked this way at all.

[person performSelector:@selector(sayHelloTo:) withObject:@"World"];

commandName is only known once the program is running, which is exactly the situation performSelector: exists for -- respondsToSelector: is checked first as a defensive guard against sending a selector the receiver doesn't implement, avoiding a crash. The second call shows the ARC hazard directly: newWidget follows the 'new' naming convention that would normally tell ARC the caller owns the returned object, but performSelector: hides the actual method being called from the compiler at the call site, so ARC cannot apply that rule -- hence the -Wperformselector-leaks warning.

id result = [factory performSelector:@selector(newWidget)]; // compiler warns

// Using performSelector: as a routine substitute for an ordinary message send, even when the selector is already known [person performSelector:@selector(sayHello)];

[person sayHello];

When the selector is known while you're writing the code, an ordinary typed message send gives you compile-time checking for free and avoids ARC's return-value blind spot entirely. Reach for performSelector: only when the selector is genuinely unknown until runtime.

id result = [factory performSelector:@selector(newWidget)]; // assuming ARC manages result's memory exactly as it would for [factory newWidget]

// Recognizing that ARC cannot apply its alloc/new/copy naming-convention analysis through performSelector:, and either avoiding the pattern or managing memory manually with awareness of -Wperformselector-leaks

ARC infers ownership rules by analyzing the actual method being called, at compile time, using its name. performSelector: hides which method will run from the compiler at the call site, so ARC cannot apply that analysis to the return value -- this is exactly why -Wperformselector-leaks exists, and ignoring it can cause real leaks or over-releases.

// Trying to pass a primitive directly through performSelector:withObject: [person performSelector:@selector(setAge:) withObject:30]; // does not compile -- 30 is not an object pointer

[person performSelector:@selector(setAge:) withObject:@30]; // @30 boxes it into an NSNumber object

Every argument passed through the performSelector: family must be an object pointer. A raw primitive like 30 must be boxed (here, into an NSNumber via the @30 literal) before it can be passed -- and the receiving method must itself be written to expect and unwrap a boxed value, not a plain primitive.

Why does performSelector: exist, given that ordinary bracket message syntax already exists? A. It exists purely for performance -- performSelector: is faster than an ordinary message send B. Bracket syntax requires the selector to be written literally in source code; performSelector: allows sending a message using a SEL value determined only at runtime C. It exists to bypass the Objective-C runtime's method lookup entirely D. It is required for any message send involving more than one argument

B. Bracket syntax requires the selector to be written literally in source code; performSelector: allows sending a message using a SEL value determined only at runtime — Ordinary bracket syntax needs the selector written as literal text the compiler can see. performSelector: accepts a SEL as an ordinary argument, which can be a variable holding a value determined only while the program runs -- exactly the situation it exists to handle.

What is the maximum number of arguments the built-in NSObject performSelector: family can pass directly, and what type must they be? A. Unlimited arguments, of any type B. Up to three arguments, of any primitive or object type C. Up to two arguments, and each one must be an object pointer D. Exactly one argument, which must be a primitive type

C. Up to two arguments, and each one must be an object pointer — The built-in family stops at performSelector:withObject:withObject:, covering zero, one, or two arguments, and every argument must be an object pointer (or nil) -- primitives and structs cannot be passed directly, and there is no three-argument version at all.

Why does the compiler warn with -Wperformselector-leaks about certain performSelector: calls? A. Because performSelector: always causes a memory leak, with no exceptions B. Because ARC normally infers a method's return-value ownership from its name/signature at compile time, and performSelector: hides which actual method will run, so ARC cannot verify how to manage the returned value C. Because performSelector: cannot return any value at all D. Because the warning is unrelated to memory management and instead flags a performance issue

B. Because ARC normally infers a method's return-value ownership from its name/signature at compile time, and performSelector: hides which actual method will run, so ARC cannot verify how to manage the returned value — ARC's memory-management inference relies on seeing the actual method being called (its name and naming convention, like alloc/new/copy) at compile time. performSelector: obscures that from the compiler at the call site, so ARC cannot apply its usual rules to the return value -- which is exactly what -Wperformselector-leaks flags.

Why are blocks usually preferred over performSelector: for passing around 'what to do next' in modern Objective-C code? A. Blocks and performSelector: are functionally identical, so the choice is purely stylistic B. Blocks capture actual executable code with full compile-time type checking of parameters and return value, and have none of performSelector:'s ARC return-value blind spot C. performSelector: cannot be used inside any method that also uses blocks D. Blocks are only usable for methods with zero arguments, making them simpler than performSelector:

B. Blocks capture actual executable code with full compile-time type checking of parameters and return value, and have none of performSelector:'s ARC return-value blind spot — A block is checked by the compiler like ordinary code -- its parameters and return type are known statically -- so it avoids both performSelector:'s lack of compile-time checking and its ARC memory-management hazard, which is why blocks have largely replaced performSelector: for dynamic, configurable behavior in modern code.

// No arguments
[person performSelector:@selector(sayHello)];

// One argument
[person performSelector:@selector(sayHelloTo:) withObject:@"World"];

// Two arguments (both must be objects)
[person performSelector:@selector(setName:andNickname:)
                withObject:@"Ana"
                withObject:@"Ani"];

// This will NOT compile as written -- there is no three-argument version:
// [person performSelector:@selector(setName:age:height:)
//                 withObject:@"Ana" withObject:@30 withObject:@170];
NSString *commandName = configuredCommandFromFile();  // not known at compile time
SEL dynamicSelector = NSSelectorFromString(commandName);

if ([person respondsToSelector:dynamicSelector]) {
    [person performSelector:dynamicSelector];
} else {
    NSLog(@"person does not implement %@", commandName);
}

// A hazard: performSelector: on a method that returns a new, owned object
// triggers -Wperformselector-leaks, because ARC cannot verify how to
// manage the returned value's memory here.
id result = [factory performSelector:@selector(newWidget)];  // compiler warns