Objective-C (2.0)
This assessment applies the coupling account — coupling as access to a shared resource, and the coupling floor [1], [2] — and the knowledge-embodiment theorem [3]; the language is assessed against its official reference [4].
1 Assessment
This page assesses Objective-C in five parts: core coupling — what the language’s own constructs force; ecosystem coupling — what the surrounding platform, tooling, and community cost; engineering readiness — whether the artifact can be reproduced, verified, observed, worked in quickly, connected to other languages, and governed openly; the profile — the unscored facts a score cannot express; and the scoring detail — the mechanism marks and the arithmetic behind every score. A ✅ bullet marks a construct that helps. A ⚠️ or ❌ bullet marks a cost; the scoring model does not price every cost named here.
Core coupling 42/100 · Ecosystem coupling 65/100 · Engineering readiness 53/100
1.1 Core coupling
1.1.1 Type
- ❌ Dispatch is resolved at run time (T3): A message send such as
[obj doThing]compiles toobjc_msgSend, and the selector is looked up as the message is delivered, so an object can receive a message its declared interface does not list and the fault appears asunrecognized selector sent to instance - ❌ There is no namespace (T6): Every class and protocol shares one global name table, so code relies on prefixes such as
NS,UI, andCGto keep names apart - ❌
iderases the static type (T7): The generic object pointer carries no compile-time type, so a value held inidis checked only by-isKindOfClass:or-respondsToSelector:at run time - ✅ Protocols separate specification from implementation: A
@protocoldeclares a method set that any class can adopt with<NSCopying>, so conformance does not require inheritance and one class can adopt several protocols - ✅ Categories extend a class without its source:
@interface NSString (MyExtras)adds methods to an existing class, so a consumer extends behaviour without subclassing and without access to the original - ⚠️ Static type information is optional and partly erased: Declared parameter and return types (
NSString *,instancetype) are checked at compile time, but lightweight generics such asNSArray<NSString *> *and the nullability annotationsnonnullandnullableare erased at run time
1.1.2 Construction
- ❌ There is no immutability by default (C2):
NSString,NSArray, andNSDictionaryeach have a mutable counterpart, and a property declaredstrongwithoutcopyshares a mutable object with its setter;@property (copy)is the opt-in that protects it - ✅
allocandinitseparate allocation from initialisation:[[NSObject alloc] init]and a designated initialiser such as-initWithFrame:let one class route every construction path through one setup method - ✅
@propertysynthesises the accessors:@property (nonatomic, copy) NSString *name;generates the getter, the setter, and the instance variable, so a consumer binds to the property name rather than to the ivar layout - ✅ Blocks carry behaviour as a value: A
^-literal closure captures its environment and is passed as an argument, as in-[NSArray enumerateObjectsUsingBlock:], so a callback needs no delegate class - ✅ Class clusters hide the concrete class:
NSNumberandNSStringreturn a private subclass from a factory method such as+numberWithInt:, so construction does not couple a caller to one implementation class - ⚠️
+newand-initare conventions, not enforced rules: The runtime does not require an object to be initialised, so an uninitialised instance is possible and the class author must insist on the designated initialiser
1.1.3 Memory
- ✅ ARC removes manual retain and release: The compiler inserts
objc_retain,objc_release, and the autorelease-pool calls at the right points, so the common case needs no memory-management call - ✅
strongandweakstate the ownership intent:@property (weak) id delegate;and__weakmake the reference weak, and a weak reference zeroes itself when its referent is deallocated - ⚠️ An autorelease pool defers the release: An autoreleased object lives until its enclosing
@autoreleasepooldrains, so a loop that builds many objects adds its own pool - ❌ There is no RAII (M3): ARC is not block-scoped, so a resource that is not an object, such as a
FILE *, amallocblock, or a Core Foundation handle, is released by an explicit call or a@tryand@finallyblock - ❌ There is no ownership model (M4): No static lifetime or aliasing rule bounds an object, so a
strongreference cycle leaks until it is broken withweak - ❌ Reclamation timing is not a language rule (M2): The language never states when an object is freed; the tracing collector that once shipped with the Apple runtime was deprecated and then removed, and ARC’s reference counting is a compiler convention rather than a language guarantee
1.1.4 Error
- ❌ Exceptions are unchecked (Er2):
@throw,@try,@catch, and@finallycarry anNSExceptionthat no signature declares, so a caller cannot see the failure set, and the convention reserves exceptions for programmer errors - ✅
NSError **reports a recoverable failure: A method such as-[NSString writeToFile:atomically:encoding:error:]writes the failure into an out-parameter, so a caller recovers without an exception - ✅
@try,@catch, and@finallyare structured: A@catchclause names the exception class, and@finallyruns on both the normal and the exceptional path - ⚠️ Two error conventions coexist:
NSExceptionmarks a programming error andNSErrormarks a domain failure, so a caller must know which convention a given API follows - ⚠️ A message to
nilfails silently:[nil doThing]returnsnilor0and raises nothing, so a missed wiring point produces no error - ⚠️
NSAssertandNSParameterAssertare run-time checks: They abort in a debug build and are compiled out underNS_BLOCK_ASSERTIONS, so they do not enforce a contract in a release build
1.1.5 Concurrency
- ❌ Synchronisation is manual with no race detection (K3):
@synchronized,NSLock,NSRecursiveLock, anddispatch_semaphoreleave the reasoning to the programmer, and the compiler reports no data race - ✅ Grand Central Dispatch separates work from threads:
dispatch_async(dispatch_get_global_queue(0, 0), ^{ ... })submits a block to a queue, so the code names a queue rather than a thread - ✅ A completion block returns on a named queue: An asynchronous API takes a block and a queue, so a caller need not share mutable state with the worker
- ✅
NSOperationQueuedeclares dependencies:-[NSOperation addDependency:]orders operations in the queue, so sequencing is stated rather than hand-scheduled - ✅
@synchronizedgives a per-object mutex:@synchronized(self) { ... }serialises a critical section on one object without a separate lock object - ⚠️
@property (atomic)does not make a compound operation atomic: The default attribute serialises each accessor call, so a check-then-set sequence over the same property can still interleave with another thread
1.1.6 Platform
- ❌ One vendor owns the mainstream platform (P2): The complete language, its reference compiler (Clang), and its flagship frameworks (Cocoa and Cocoa Touch) are Apple’s
- ✅ GNUstep provides a second implementation: The GNUstep runtime, Foundation, and AppKit reimplementations let much Objective-C code build off Apple’s platform
- ✅ The language is a strict superset of C: A
.mfile mixes C functions and structs with message sends, so any C library is callable directly - ✅ Modules replace recursive header inclusion:
@import Foundation;imports a clang module, so a build does not depend on#importorder - ⚠️ The frameworks differ by platform:
AppKiton macOS andUIKiton iOS share Foundation but not the user-interface layer, so UI code does not move between them
1.1.7 Trade-offs (not coupling)
- Message-passing syntax is a familiarity cost:
[receiver selector:argument]differs from dot-syntax object languages, and the adjustment is a learning property rather than a forced dependency - Long method names are a deliberate style:
-[NSString stringByReplacingOccurrencesOfString:withString:]is verbose but self-describing, which is a readability choice - Manual retain and release remains available: A project can leave ARC off and manage lifetimes itself, so the older memory model is a choice rather than a requirement
NSExceptionfor programmer errors is a policy: Treating an exception as a bug signal andNSErroras a recovery signal is a convention the language permits but does not require- Prefixes mitigate the flat namespace: A class prefix is a convention that reduces collisions; it is a workaround for the missing namespace, not an additional coupling
- A mixed Objective-C and Swift target is a migration decision: Both languages share the runtime and call each other, so how far to migrate is a project choice
1.2 Ecosystem coupling
1.2.1 Performance
- ✅ The language compiles to native code: Clang lowers Objective-C to machine code through LLVM, so a message send is a real call and a program needs no warm-up
- ✅ The runtime is thin and written in C:
libobjcsupplies the selector table, the class metadata, and the reference-counting helpers with little work beyond the message send - ⚠️
objc_msgSendcosts more than a static call: Every message send performs a selector lookup that is cached after the first use, so a tight loop of message sends is slower than the equivalent direct call - ⚠️ Dynamic dispatch limits inlining and devirtualisation: Because the receiver’s class is known only at run time, the compiler cannot always inline a method body
- ⚠️ Key-value observing and method swizzling add indirection: Both change how a method runs after the fact, and the cost appears wherever they are installed
1.2.2 Ecosystem
- ❌ The primary ecosystem is proprietary (Ec4): The reference documentation, the frameworks, and the IDE (Xcode) come from one vendor under its licence terms
- ✅ The toolchain is mature inside that ecosystem: Xcode supplies one editor, compiler, debugger (
lldb), Interface Builder, and the Instruments profiler in one install - ✅ CocoaPods, Carthage, and Swift Package Manager resolve dependencies: Several long-established package managers exist for Objective-C libraries
- ✅ GNUstep supplies a free Foundation: A portable implementation covers
NSString,NSArray, and the rest of the Foundation API off Apple’s platform - ⚠️ Current material is written for Swift first: The platform APIs and sample code are presented for Swift, so an Objective-C reader works from older or generated headers
1.2.3 Evolution
- ❌ The language is frozen (V2): Objective-C 2.0 (2006) is effectively the last language revision, and platform-facing work has moved to Swift
- ✅ The compiler keeps supplying new features: ARC, blocks, object literals such as
@[]and@{}, lightweight generics, and nullability annotations arrived as Clang features rather than as language revisions - ✅ A mixed Objective-C and Swift target is supported: Both languages link against the same runtime and can call each other, so a project can migrate one file at a time
- ✅ Old source still compiles: OpenStep-era Foundation API and the 1990s NeXT runtime conventions are still supported by the current toolchain
- ⚠️ Documentation for new APIs follows the vendor’s current language: A header is the reliable reference for the Objective-C form of a new API
1.2.4 Domain
- ❌ The domain and the vendor are one decision (D1): The language is built for Apple’s platforms, so adopting it means shipping on macOS, iOS, iPadOS, watchOS, and tvOS
- ✅ Excellent fit for the Apple application model: The Cocoa delegate, target-action, and notification patterns and the main run loop are expressed directly in the language
- ✅ Foundation runs off Apple’s platform: Portable Foundation code from GNUstep works on Linux and other systems for command-line and server work
- ⚠️ Cross-platform work needs a second toolkit: GNUstep supplies Foundation but has no equivalent of a modern Apple user-interface layer
- ⚠️ Server and command-line use is marginal: The language is used for it, but the design centre is the application platform
1.2.5 Learning
- ❌ Message passing is a barrier for a newcomer (L1):
[receiver selector:argument], the colon-bearing selector notation, and the@-keyword set differ from dot-syntax object languages - ❌ The C substrate must be learned as well: Pointers,
struct, C strings, manual C memory, and@encodesit underneath the object model, so the language is C plus message passing - ⚠️ The runtime is a second topic:
objc_msgSend, metaclasses, key-value coding and observing, and ARC semantics are separate from the source syntax - ⚠️ Documentation quality follows the vendor: Apple’s guides and headers are thorough on the platform, and off-platform material is thinner
- ✅ The object model is conceptually small: Once the message send is understood, most of the rest follows from it, including classes, selectors, and protocols
1.2.6 Syntax
- ✅ A message send names every argument:
[view addSubview:subview positioned:NSWindowBelow relativeTo:nil]labels each argument at the call site, so the call reads as a sentence - ✅ Object literals are compact:
@42,@[@"a", @"b"], and@{@"k": @1}build anNSNumber, anNSArray, and anNSDictionaryin one form - ✅
@propertyshortens an accessor pair: One declaration replaces a hand-written getter, setter, and instance variable - ⚠️ The selector includes its colons:
addSubview:positioned:relativeTo:is one name, so a missing or an extra colon changes the selector - ⚠️ Dot syntax and brackets both reach an accessor:
obj.propand[obj prop]are interchangeable, so a reader must accept two spellings for the same call - ⚠️ The preprocessor is part of the surface:
#import,#define, and macro-based API patterns are load-bearing in existing code
1.2.7 Testing
- ✅ XCTest ships with the platform toolchain:
XCTestCase, theXCTAssertfamily, and performance tests are part of the developer tools - ✅ Instruments profiles a real run: The Time Profiler, Allocations, and Leaks instruments show where time and memory go, so a test can measure a change
- ✅ The debugger understands messages:
lldbcan break on a selector, inspect the arguments of a message send, and evaluate an expression in the running process - ⚠️ The framework is tied to the toolchain: XCTest and the simulator come from one vendor, so an off-platform run needs GNUstep and a different harness
- ⚠️ A wrong selector passes the build: With dynamic dispatch a missing method or a wrong type is a run-time failure, so the tested paths matter more than the compiled ones
1.2.8 Security
- ✅ ARC removes a class of memory faults: The compiler owns retain and release, so a use-after-free from a missing release is rare in ARC code
- ✅ A weak reference does not dangle:
weakand__weakzero the reference when its referent is deallocated, so a stale link becomesnil - ✅ A message to
nilis safe:[nil doThing]returnsnilor0instead of trapping, which removes a class of null-dereference crash - ⚠️
idpermits type confusion: A value typedidaccepts any object, and a wrong cast is caught only where the code checks the class - ⚠️ C is still underneath:
malloc, pointer arithmetic, and a file compiled with-fno-objc-arcbypass every object-model guarantee - ⚠️ Method swizzling changes behaviour after the build:
method_exchangeImplementationsreplaces an implementation at run time, which is powerful and easy to misuse
1.3 Engineering readiness
1.3.1 Reproducibility
- ❌ Builds are not byte-reproducible by default.
1.3.2 Supply chain
- ⚠️ Published packages carry no signed provenance by default.
- ⚠️ The package registry is a flat name space.
1.3.3 Observability
- ⚠️ No stable tracing and metrics standard.
1.3.4 Feedback
- ✅ No charged mechanism in this dimension.
1.3.5 Interop
- ⚠️ No maintained portable target.
1.3.6 Governance
- ⚠️ One vendor decides the language’s direction.
- ⚠️ The specification is not public.
1.4 Profile
Unscored, and kept separate from every score on purpose. The profile records what a mechanism price cannot express: what a running program costs, how well a coding model and an agent work in the language, and whether a safety-critical path exists.
1.4.1 Energy and cost
What this axis asks: what does a running program cost in energy and time?
1.4.2 AI and agent readiness
What this axis asks: how well does a coding model and an agent work in this language?
- ⚠️ Moderate training data: models write it competently, with measurable language-specific weakness [8].
1.4.3 Safety certification
What this axis asks: is there a certified or standards-qualified path to a safety-critical system?
- ⚠️ No certified or standards-qualified path for safety-critical systems.
1.5 Scoring detail
1.5.1 Core coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Type | T1 |
dynamic no static check | — | 0 |
T2 |
weak static | — | 0 | |
T3 |
dynamic/duck dispatch | ✅ | −8 | |
T4 |
no sum types | — | 0 | |
T5 |
no generics | — | 0 | |
T6 |
no namespaces | ✅ | −8 | |
T7 |
type erasure/reflection | ✅ | −4 | |
T8 |
operator overloading | — | 0 | |
T9 |
inheritance required for polymorphism | — | 0 | |
T10 |
no null-safety | — | 0 | |
T11 |
no module boundary | — | 0 | |
T12 |
no effect tracking | ✅ | −8 | |
T13 |
async/sync colouring | — | 0 | |
T14 |
unchecked foreign boundary | ✅ | −4 | |
| Type subtotal | −15 (−32) | |||
| Construction | C1 |
global mutable state by default | — | 0 |
C2 |
no immutability by default | ✅ | −4 | |
C3 |
constructors coupled to allocation | — | 0 | |
C4 |
no constructor/validation | — | 0 | |
| Construction subtotal | −4 | |||
| Memory | M1 |
manual deallocation | — | 0 |
M2 |
GC non-deterministic pauses | ✅ | −8 | |
M3 |
no RAII | ✅ | −4 | |
M4 |
no ownership model | ✅ | −8 | |
| Memory subtotal | −15 (−20) | |||
| Error | Er1 |
no structured path | — | 0 |
Er2 |
unchecked exceptions only | ✅ | −8 | |
Er3 |
errors ignored/non-exhaustive | — | 0 | |
Er4 |
no composable result type | — | 0 | |
| Error subtotal | −8 | |||
| Concurrency | K1 |
GIL/no parallel | — | 0 |
K2 |
shared mutable state default | — | 0 | |
K3 |
manual sync no race detection | ✅ | −8 | |
K4 |
no structured concurrency | — | 0 | |
K5 |
no primitives | — | 0 | |
| Concurrency subtotal | −8 | |||
| Platform | P1 |
cannot function outside one vendor/domain | — | 0 |
P2 |
single-vendor but other uses | ✅ | −8 | |
P3 |
legacy/discontinued platform | — | 0 | |
| Platform subtotal | −8 |
Core Dimensions score: 42/100 — total cost = −58 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.2 Ecosystem coupling Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Performance | R1 |
interpreted no AOT/JIT | — | 0 |
R2 |
GC pauses | — | 0 | |
R3 |
JIT warmup | — | 0 | |
| Performance subtotal | 0 | |||
| Ecosystem | Ec1 |
no package manager | — | 0 |
Ec2 |
fragmented tooling | — | 0 | |
Ec3 |
small/niche/discontinued | — | 0 | |
Ec4 |
proprietary | ✅ | −8 | |
Ec5 |
platform-locked tooling | — | 0 | |
Ec6 |
monoculture | — | 0 | |
Ec7 |
legacy burden | — | 0 | |
| Ecosystem subtotal | −8 | |||
| Evolution | V1 |
breaking changes | — | 0 |
V2 |
frozen | ✅ | −4 | |
V3 |
discontinued | — | 0 | |
V4 |
glacial standards | — | 0 | |
| Evolution subtotal | −4 | |||
| Domain | D1 |
domain/vendor lock-in | ✅ | −15 |
D2 |
poor domain fit | — | 0 | |
D3 |
framework forces architecture | — | 0 | |
| Domain subtotal | −15 | |||
| Learning | L1 |
steep | ✅ | −8 |
L2 |
large surface/multi-paradigm | — | 0 | |
L3 |
sparse docs | — | 0 | |
| Learning subtotal | −8 | |||
| Syntax | S1 |
verbose | — | 0 |
S2 |
inconsistent | — | 0 | |
S3 |
punctuation-heavy | — | 0 | |
| Syntax subtotal | 0 | |||
| Testing | G1 |
weak tooling | — | 0 |
G2 |
poor debugging | — | 0 | |
G3 |
runtime-only errors | — | 0 | |
| Testing subtotal | 0 | |||
| Security | Q1 |
memory-unsafe | — | 0 |
Q2 |
dynamic/weak typing risk | — | 0 | |
Q3 |
known vulnerability/injection | — | 0 | |
| Security subtotal | 0 |
Meta Dimensions score: 65/100 — total cost = −35 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
1.5.3 Engineering readiness Dimensions
| Dimension | Mechanism | Description | Present | Cost |
|---|---|---|---|---|
| Reproducibility | Y1 |
build not reproducible | ✅ | −8 |
Y2 |
dependencies not pinned | — | 0 | |
| Reproducibility subtotal | −8 | |||
| Supply chain | Y3 |
no signed provenance | ✅ | −8 |
Y4 |
registry without namespacing | ✅ | −4 | |
| Supply chain subtotal | −12 | |||
| Observability | Y5 |
no stable tracing standard | ✅ | −8 |
Y6 |
no structured logging | — | 0 | |
| Observability subtotal | −8 | |||
| Feedback | Y7 |
slow build | — | 0 |
Y8 |
no fast in-editor analysis | — | 0 | |
| Feedback subtotal | 0 | |||
| Interop | Y9 |
no foreign-function interface | — | 0 |
Y10 |
no portable target | ✅ | −4 | |
| Interop subtotal | −4 | |||
| Governance | Y11 |
single-vendor governance | ✅ | −8 |
Y12 |
closed specification | ✅ | −8 | |
| Governance subtotal | −15 (−16) |
Readiness Dimensions score: 53/100 — total cost = −47 (per-dimension subtotals are capped at −15; an uncapped cost is shown in parentheses where the cap bites).
Verdict: Objective-C’s design yields moderate language-imposed coupling, and its weakest core dimensions are the ones the language was built around. Dynamic dispatch, the flat global namespace, and the id pointer defer type and dispatch checks to run time (T3, T6, T7). Against that, protocols give multiple inheritance of specification without implementation, categories extend a class without its source, blocks carry a callback as a value, and ARC removes manual retain and release while weak makes a stale reference zero itself. Its main weaknesses lie underneath and around the object model: there is no ownership rule and no block-scoped release, exceptions are unchecked, and the entire platform is one vendor’s, so the domain, the evolution, and the ecosystem are all locked to the same decision.
The formal development. The definitions, theorems, and proofs behind this page are published in:
- On the Nature of Coupling: All Coupling as Shared Resource Access — Zenodo, 2026 — doi:10.5281/zenodo.21925122
- Coupling as a Consequence of Cohesion: Decomposition, the Coupling Floor, and Domain-Imposed Coupling — Zenodo, 2026 — doi:10.5281/zenodo.21925201
- On the Nature of Cohesion: Cohesion as Correctness Relative to a Partitioning Rule — Zenodo, 2026 — doi:10.5281/zenodo.21703193