Operators, Control Flow and Functions

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.

Operators, control flow and functions are C’s, inherited wholesale. What is worth writing down is the handful of places where Objective-C’s object model changes how you use them: BOOL truthiness, messages appearing inside conditions, switch over an NS_ENUM, and the division of labour between C functions and Objective-C methods.

Operators

The full C operator set applies — arithmetic (+ - * / %), comparison (== != < > ⇐ >=), logical (&& || !), bitwise (& | ^ ~ << >>), assignment and its compound forms, increment/decrement, the comma operator, sizeof, the address-of and dereference operators, and the conditional operator.

Precedence, associativity and short-circuit evaluation are unchanged from C — for the precedence table, value categories and the sequencing rules (including where unsequenced side effects are undefined behavior), see C Reference — Operators and Expressions. Two operators deserve an Objective-C note:

  • == on objects compares pointer identity, not value. [a isEqual:b] (or isEqualToString:, isEqualToArray: …) compares contents. This is the most frequent source of surprising bugs in the language.

    NSString *a = @"hello";
    NSString *b = [NSString stringWithFormat:@"%@", @"hello"];
    
    BOOL samePointer = (a == b);            // NO -- two distinct objects
    BOOL sameValue   = [a isEqualToString:b];  // YES -- same characters

    (Constant strings written @"hello" in the same binary are often interned to one object, so == may accidentally appear to work. Do not rely on it.)

  • Dot syntax is not the C struct-member operator when the receiver is an object. view.frame on an object is sugar for [view frame]; on a struct it is ordinary C member access. See Properties and Encapsulation.

BOOL Truthiness

Objective-C tests truth the way C does: zero is false, anything else is true. nil is zero, so a nil object pointer is false, which makes the idiomatic guard very short:

NSString *name = [self lookupName];
if (name) {                             // non-nil
    NSLog(@"%@", name);
}

if (![array count]) {                   // count == 0, or array is nil (nil returns 0)
    return;
}

That second example shows a genuine subtlety: array being nil and array being empty take the same branch, because a message to nil returns 0. Usually that is exactly what you want; when it is not, test for nil separately.

Never compare a BOOL against YES (see Basic Syntax and Types).

Control Flow

Every statement form is C’s, with C’s semantics; see C Reference — Control Flow for the full treatment and Advanced Control Flow for goto, setjmp/longjmp and signal handlers. This section covers what changes once message sends are in the picture.

if / else

Standard C. What is distinctive is how often a message send appears inside the condition — legal, common, and safe even on nil:

if ([delegate respondsToSelector:@selector(didFinish:)]) {
    [delegate didFinish:self];
} else if ([self.items count] > 0) {
    [self processItems];
} else {
    [self reportEmpty];
}

switch

C’s switch — integral values only, with fall-through unless you break. Objective-C’s characteristic use is over an NS_ENUM, and the characteristic advice is to omit default: so the compiler warns about unhandled cases:

typedef NS_ENUM(NSInteger, MyPlayerState) {
    MyPlayerStateStopped,
    MyPlayerStatePlaying,
    MyPlayerStatePaused
};

switch (self.state) {
    case MyPlayerStateStopped:
        [self start];
        break;
    case MyPlayerStatePlaying:
        [self pause];
        break;
    case MyPlayerStatePaused:
        [self resume];
        break;
    // no default: -- adding a fourth state now produces a -Wswitch warning here
}

You cannot switch on an object. To branch on a string, either compare with isEqualToString: in an if/else chain or — better for more than a few cases — look the behaviour up in an NSDictionary of blocks.

Declaring a variable inside a case without braces is a C error (declaration not allowed after label); wrap the case body in { … }.

Loops

All four C loop forms work, plus Objective-C’s own for…in:

for (NSUInteger i = 0; i < count; i++) { /* C for */ }

while ([self hasMoreData]) { /* … */ }

do { /* at least once */ } while (retries-- > 0);

for (NSString *name in names) {          // fast enumeration -- Objective-C's addition
    NSLog(@"%@", name);
}

Fast enumeration is the idiomatic way to walk a Foundation collection: shorter than an index loop, and substantially faster than sending objectAtIndex: per element. It is covered in Collections and Fast Enumeration, along with the rule that you must not mutate a collection while enumerating it.

break and continue behave as in C. Objective-C has no labelled break; to leave nested loops, use a flag, a goto, or — most idiomatically — factor the inner loop into its own method and return.

The Conditional Operator

cond ? a : b as in C, including GCC’s widely used ?: elision, which Clang supports:

NSString *display = name ? name : @"(unknown)";
NSString *shorter = name ?: @"(unknown)";      // evaluates `name` once

?: is Objective-C’s closest equivalent to a null-coalescing operator and reads well for defaulting.

Functions and Methods

Objective-C has both, and choosing between them is a real design decision. Plain C functions behave exactly as they do in C — prototypes, pass-by-value, static linkage, inline, recursion and variadic functions via <stdarg.h> are all covered in C Reference — Functions.

C function Objective-C method

Syntax

double area(double r);

- (double)areaWithRadius:(double)r;

Call

area(2.0)

[shape areaWithRadius:2.0]

Dispatch

Static — resolved at link time

Dynamic — resolved by selector at run time

Receiver

None

self, implicitly available (plus _cmd, the current selector)

Overridable

No

Yes, by any subclass

Cost

A direct call

A call through objc_msgSend (cached, but not free)

Use a function for stateless computation, for performance-critical inner loops, and for anything that must be callable from plain C. Use a method whenever behaviour belongs to an object, or must be overridable, or forms part of a public API. Foundation itself does exactly this: NSMakeRange, CGRectIntersectsRect and NSStringFromClass are functions; everything with a receiver is a method.

Method Syntax

A method declaration interleaves its name with its arguments, which is why Objective-C selectors read like sentences:

- (void)insertObject:(id)object atIndex:(NSUInteger)index;
//^     ^             ^                  ^
//|     |             |                  `-- second argument
//|     |             `-- first argument
//|     `-- return type
//`-- "-" = instance method, "+" = class method

The selector of that method is insertObject:atIndex: — the concatenated keywords with their colons. Two methods differing only in argument types share a selector and therefore collide; there is no overloading by type. See Classes and Objects.

Pointers, struct and union

Pointers are C’s, and every Objective-C object is accessed through one. NSString *s is a pointer; id is already a pointer type. There are no stack-allocated Objective-C objects.

struct and union are C’s and are used freely for small value types — Foundation and the graphics frameworks are full of them (NSRange, CGPoint, CGRect, CGSize). For the C semantics in depth see C Reference — Pointers, Structures, Unions and Type Aliases and The Memory Model and Alignment:

NSRange r = NSMakeRange(0, 5);
NSLog(@"%lu…%lu", (unsigned long)r.location, (unsigned long)(r.location + r.length));

CGRect frame = CGRectMake(0, 0, 320, 480);
CGFloat mid  = CGRectGetMidX(frame);

Two rules govern their interaction with the object system:

  • A struct is not an object. It cannot receive messages, is copied by value on assignment, and is invisible to the run time’s introspection.

  • Under ARC, a C struct field may be an object pointer — the compiler emits copy and destroy helpers for it — but those helpers never run for a malloc-allocated struct, so such a field must be __unsafe_unretained with a hand-managed lifetime. A small class or an NSValue is usually better. See Automatic Reference Counting.

To carry a struct inside a Foundation collection, box it in an NSValue — see Strings, Numbers and Values.

The Preprocessor

The C preprocessor is present in full — macro definition, conditional compilation, stringification and token pasting, variadic macros and VA_OPT are all covered in C Reference — The Preprocessor and Macros. Three points are specific to how Objective-C uses it.

#import versus #include

#import is #include that never includes the same file twice. It makes include guards unnecessary and is what Objective-C code uses for headers, without exception:

#import <Foundation/Foundation.h>       // framework header
#import "MyClass.h"                     // project header
#include <math.h>                       // plain C header: either works, #import is fine too

Prefer a @class forward declaration to an #import in a header when you only need the name of a type — it cuts compile times and breaks circular-import chains — and #import the real header in the .m:

// MyView.h
@class MyModel;                         // just the name is needed here
@interface MyView : NSObject
@property (nonatomic, strong) MyModel *model;
@end

// MyView.m
#import "MyModel.h"                     // the full declaration is needed here

Clang modules supersede both for framework imports: @import Foundation; parses a precompiled module instead of re-reading text. See Modules, Frameworks and Code Organization.

#pragma mark

#pragma mark inserts a labelled divider into Xcode’s jump bar. It is pure organisation, costs nothing, and is conventional in any implementation file long enough to scroll:

#pragma mark - Lifecycle
- (instancetype)init { /* … */ }
- (void)dealloc      { /* … */ }

#pragma mark - MyDelegateProtocol
- (void)didFinish:(id)sender { /* … */ }

The leading - draws a separator line above the label.

Conditional Compilation

#import <TargetConditionals.h>

#if TARGET_OS_IPHONE
    // iOS, iPadOS, tvOS, watchOS
#elif TARGET_OS_OSX
    // macOS
#endif

#if TARGET_OS_SIMULATOR
    NSLog(@"running in the simulator");
#endif

#ifdef DEBUG
    NSLog(@"debug build only");
#endif

Compile-time conditionals answer "which platform was this built for". They cannot answer "which OS version is this running on" — that is @available, a run-time check, covered in Lightweight Generics and Nullability.

Prefer static inline functions and const variables to function-like and object-like macros: macros are untyped, invisible to the debugger, and re-evaluate their arguments.

See Also