The Objective-C Syntax Mental Model: From Calling Functions to Sending Messages

Compare C's function-call syntax with Objective-C's message-send syntax, learn the core vocabulary of message sending (receiver, selector, message, arguments, return value), and understand the fundamental mental shift from 'calling a function' to 'sending a message to an object' that underlies everything else in this course.

Every C construct you've learned so far — variables, functions, control flow — carries over into Objective-C completely unchanged, because Objective-C is a strict superset of C. But Objective-C also adds something C does not have at all: a way to send messages to objects. The syntax for this looks unusual the first time you see it — square brackets, colons buried inside words — and it is tempting to treat it as just a different way of writing a function call. That temptation is worth resisting from the very first lesson, because the difference is not cosmetic. It reflects a genuinely different execution model, one that this lesson introduces at the surface level and that the Runtime module, much later in this course, will explain all the way down to the mechanism. For now, the goal is simpler: learn to read Objective-C message syntax fluently, and understand precisely what each piece of it means.

C Function Calls: A Quick Reminder

In C, calling a function looks like this: result = add(a, b);. Reading this left to right, add is the name of a function, a and b are the arguments being passed to it, and result receives whatever value add returns. This is a direct call: at compile time, the compiler resolves add to a specific block of machine code (found via the linker, as covered in Module 1), and at runtime, the CPU jumps straight to that code, runs it, and comes back with a return value. There is no ambiguity about which code runs — the function add is add, fixed and singular, determined the moment the program is built.

This is the model you've been using for every C function call so far: printf("..."), sizeof(x), a function you wrote yourself like callCounter(void). The function name IS the thing that gets executed. Nothing decides, at runtime, which code to run in response to that call — the call and the code it runs are the same fixed pairing from the moment the program is compiled and linked.

Keep this model firmly in mind, because Objective-C's message syntax is going to look like it's just a stylistic variant of a function call — brackets instead of parentheses, colons instead of commas — but it is doing something categorically different underneath, and understanding that difference is the single most important conceptual shift in moving from C to Objective-C.

• C function call: result = add(a, b); — a, b are arguments, add is the function, result receives the return value. • The compiler/linker resolves add to one specific, fixed block of machine code at build time. • At runtime, calling add(a, b) always executes that same fixed code — there is no runtime decision about which code runs. • This 'direct call' model applies to every C function you've written or used so far.

Objective-C Message Syntax: The Same Idea, a Different Mechanism

Now compare that to Objective-C: [result addNumber:b];. The square brackets are the giveaway that this is not a function call at all — it's a message send. Read it as: 'send the message addNumber: to the object result, passing b as the argument.' The object named result is being asked to do something (add a number to itself, in this made-up example); it is not a function named addNumber being invoked directly the way add was.

This distinction sounds subtle, but it has a real consequence: unlike a C function call, where add is a fixed, singular piece of code determined at compile/link time, an Objective-C message send does not commit, at compile time, to one single fixed piece of code to run. Instead, at runtime, the Objective-C runtime looks at the actual object that result refers to, checks what that specific object's class knows how to do in response to the message addNumber:, and only then dispatches to the matching method implementation. Two different objects can both receive the exact same message addNumber: and run completely different code in response, depending on what each object's class actually implements — something a plain C function call can never do, because add(a, b) always means the exact same fixed function.

This is the seed of an idea called dynamic dispatch, which the Runtime module of this course covers in full depth later — including exactly how the runtime finds the right method at runtime, what happens when no matching method exists, and how this enables patterns (like dynamically swapping behavior at runtime) that are simply impossible with C's direct function calls. For now, the important takeaway is just this: brackets mean 'a message is being sent to an object,' and which code actually runs in response is a runtime decision, not a compile-time one.

• Objective-C message send: [result addNumber:b]; — read as 'send the message addNumber: to result, with argument b'. • Square brackets signal a message send, not a direct function call. • Unlike a C function call, the code that runs is decided at runtime by looking at the actual object receiving the message. • Two different objects can respond to the identical message with entirely different code — impossible with a fixed C function call. • This is dynamic dispatch — introduced here at the surface level, covered in full mechanical depth in the Runtime module later.

The Core Vocabulary: Receiver, Selector, Message, Arguments

Every Objective-C message send is built from a small, precise set of vocabulary that you'll use constantly for the rest of this course, so it's worth learning the exact terms now rather than picking them up loosely later. The receiver is the object the message is being sent to — in [result addNumber:b], result is the receiver. It appears first, immediately after the opening bracket.

The selector is the name that identifies which method is being requested — in that same example, addNumber: is the selector. A selector is not the method's implementation (the actual code that runs); it's just the name used to identify which behavior is being asked for. Selectors that take arguments include a trailing colon for each argument; a selector with no arguments, like sayHello, has no colon at all. You'll meet the SEL type, which represents a selector as an actual value you can store and pass around, in the very next lesson.

The message is the combination of receiver, selector, and any arguments together, considered as one complete unit — 'sending a message' means the whole act of specifying who should respond (receiver), what's being asked of them (selector), and with what data (arguments). The arguments are the values being passed along with the message, each one following a colon in the selector, and the return value is whatever the method that ends up handling the message hands back — assigned, just like a C function's return value, to a variable on the left of =, if the caller wants to keep it.

• Receiver: the object being sent the message (result in [result addNumber:b]). • Selector: the name identifying which method is being requested (addNumber:); colons mark where arguments belong. • Message: receiver + selector + arguments together, as one complete unit being sent. • Arguments: the values passed along with the message, each following a colon in the selector. • Return value: whatever the responding method hands back, usable exactly like a C function's return value.

Multi-Argument Messages: Colons Interleaved with Names

So far the examples have used a selector with a single argument. Objective-C selectors can take multiple arguments, and this is where the syntax looks most unfamiliar coming from C: instead of a single function name followed by a comma-separated argument list, an Objective-C selector with multiple arguments is written as several name-and-colon pieces, each immediately followed by its corresponding argument, all strung together inside one pair of brackets.

For example, a message that tells a person object to update both a name and an age at once might read [person setName:@"Ana" age:30];. Here the full selector is actually setName:age: — a single selector name, even though it's visually split across the message by its two arguments (@"Ana" and 30). This is a deliberate design choice in Objective-C: because argument values sit directly next to the name describing what they mean (setName: right before the name value, age: right before the age value), message sends tend to read almost like a sentence in English, which is often cited as one of Objective-C's more distinctive (and, once you're used to it, genuinely readable) qualities compared to C's plain comma-separated argument lists.

It's worth being precise here: setName:age: is one selector, not two. When you look this method up later (for instance while reading Apple's documentation, or when the Messaging module covers selectors and method signatures in depth), you'll see it written exactly that way, as a single colon-separated name, because that's genuinely what the runtime treats it as — one identifier, naming one specific method, that happens to take two arguments interleaved with descriptive labels.

• A selector can have multiple parts, each ending in a colon, each immediately followed by its argument value. • [person setName:@"Ana" age:30]; — the full selector is setName:age:, a single name, not two separate calls. • Interleaving argument values with descriptive labels is a deliberate readability choice — messages tend to read like English sentences. • Always think of a multi-part selector like setName:age: as one identifier naming one method, not multiple separate pieces.

Why This Mental Shift Matters Going Forward

It's tempting, especially in your first week with Objective-C, to mentally translate [result addNumber:b] into 'it's basically addNumber(result, b) with weird punctuation.' That translation is close enough to get simple code working, but it quietly hides the exact thing that makes Objective-C's object model powerful: the receiver, not just the selector, determines what code actually runs. A C function name resolves to one piece of code, full stop. An Objective-C selector sent to a receiver resolves to whatever code that receiver's class says should handle it — and that resolution happens while the program is running, not while it's being compiled.

This single distinction is what makes possible almost everything covered later in this course that C alone cannot do: subclasses that override behavior their parent class defined, objects that respond differently to the same message depending on their actual runtime type, and even objects that decide, dynamically, to forward a message somewhere else entirely rather than handling it themselves. None of that is visible yet in a simple example like [result addNumber:b] — but the seed of all of it is exactly the fact that this is a message being sent to a receiver, evaluated at runtime, rather than a function call resolved once at build time.

From here forward in the course, train yourself to read [receiver selector:argument] by naming each part out loud in your head: 'send selector:argument to receiver.' That habit, formed now while the examples are still simple, will make the far more elaborate message sends you'll write later — multi-argument, nested, chained — much easier to parse correctly on sight.

• Resist reading [result addNumber:b] as just 'a function call with odd punctuation' — the receiver genuinely determines what code runs. • C: function name resolves to one fixed piece of code at build time. Objective-C: selector resolves via the receiver, at runtime. • This runtime resolution is what later enables subclass overrides, differing runtime behavior, and message forwarding. • Practice reading [receiver selector:argument] as 'send selector:argument to receiver' until it's automatic.

Code Examples

add(3, 4) is a direct call: the compiler and linker fix add to one specific block of machine code before the program ever runs. [calculator addNumber:4 toNumber:3] is a message: calculator is the receiver, addNumber:toNumber: is the single two-part selector, 4 and 3 are the arguments, and which actual code runs is resolved by looking at calculator's class at runtime.

[calculator addNumber:4 toNumber:3];

person is the receiver. The full selector is setName:age: — a single identifier split visually into two labeled parts, each immediately followed by its argument (@"Ana" and 30). The entire bracketed expression, receiver plus selector plus arguments, is 'the message'.

setName:age: // one selector, two arguments // Thinking this is equivalent to a C call like: addNumber(result, b);

[result addNumber:b];

There is no free-floating function named addNumber that you could call C-style. addNumber: is a selector that only means something when sent to a receiver — the code that runs depends on what class result actually is, which a bare function call has no equivalent of.

// Treating setName:age: as two separate selectors/messages [person setName:@"Ana"]; [person age:30];

[person setName:@"Ana" age:30];

setName:age: is one selector naming one method that happens to take two arguments. Splitting it into two separate message sends calls two entirely different (and likely nonexistent) methods rather than the single intended one.

// Assuming the compiler alone determines what [obj doSomething] runs, the way it does for a C function

// Understanding that the receiver's actual runtime class determines what code runs in response to a message

A C function call is resolved once, at compile/link time, to one fixed block of code. A message send's actual handling code is looked up via the receiver at runtime — the same selector sent to different receivers can run entirely different code.

In the message send [result addNumber:b];, what is result? A. The selector B. The receiver — the object the message is being sent to C. The return value D. An argument

B. The receiver — the object the message is being sent to — result is the receiver: the object that the message addNumber:b is being sent to. It appears first, right after the opening bracket.

What is the single, full selector in [person setName:@"Ana" age:30];? A. setName B. age C. setName:age: D. setName:, age: (two separate selectors)

C. setName:age: — Even though it's visually split by its two arguments, setName:age: is one selector — one identifier naming one method that takes two arguments, each following its own colon-labeled part.

What is the fundamental difference between a C function call like add(a, b) and an Objective-C message send like [obj add:a with:b]? A. There is no real difference; it's purely a stylistic choice of punctuation B. A C function call always resolves to one fixed block of code determined at build time; a message send's handling code is resolved at runtime based on the receiver's actual class C. Message sends cannot return a value, while C function calls can D. C function calls are slower than Objective-C message sends in every case

B. A C function call always resolves to one fixed block of code determined at build time; a message send's handling code is resolved at runtime based on the receiver's actual class — This is the core mental shift of the lesson: C resolves a function call to one fixed piece of code at compile/link time, while Objective-C resolves a message send's handling code at runtime by consulting the receiver's actual class — the seed of dynamic dispatch.

Why can two different objects respond to the exact same selector with completely different code, something a plain C function call cannot do? A. Because Objective-C selectors are randomly assigned at runtime B. Because each object's actual class is consulted at runtime to find the matching method implementation for that selector, rather than the code being fixed once at compile time C. Because C functions are also resolved at runtime, just more slowly D. This isn't actually true — two objects always run identical code for the same selector

B. Because each object's actual class is consulted at runtime to find the matching method implementation for that selector, rather than the code being fixed once at compile time — Message dispatch looks at the receiver's actual runtime class to find what code should handle a given selector, so different receivers (different classes) can implement the same selector name with entirely different behavior — impossible with a fixed C function call.

Which part of [calculator addNumber:4 toNumber:3]; is the argument corresponding to the toNumber: part of the selector? A. calculator B. addNumber C. 4 D. 3

D. 3 — Each colon-labeled part of a selector is immediately followed by its own argument. addNumber: is followed by its argument 4, and toNumber: is followed by its argument 3.

// C: a direct function call
int sum = add(3, 4);

// Objective-C: a message send to an object
// (NSNumber and arithmetic messages are illustrative here;
//  NSNumber itself is covered fully in the Foundation module)
NSInteger total = [calculator addNumber:4 toNumber:3];
[person setName:@"Ana" age:30];
//  ^^^^^^  ^^^^^^^^ ^^^^^ ^^^ ^^
//  receiver  |        |    |  |
//         selector part 1  |  |
//              argument 1 -+  |
//                   selector part 2
//                        argument 2 -+
//
// Full selector: setName:age:  (one name, two colon-separated parts)