Closures and Functions
Closures and their capture semantics — escaping vs non-escaping, capture lists, `@autoclosure`, higher-order functions, `inout`, and result builders.
12 questions
JuniorTheoryVery commonWhat is a closure in Swift — trailing-closure syntax, shorthand arguments, and capturing the environment?
What is a closure in Swift — trailing-closure syntax, shorthand arguments, and capturing the environment?
A closure is a self-contained block of code passed like an unnamed function. Trailing syntax moves a final closure outside the parentheses, $0/$1 are shorthand argument names, and capturing means it holds references to enclosing-scope variables.
Common mistakes
- ✗Thinking a closure cannot access variables from its surrounding scope
- ✗Believing
$0/$1must be declared explicitly before use - ✗Confusing trailing-closure syntax with a language-required keyword
Follow-up questions
- →When does capturing a variable extend that variable's lifetime?
- →How does trailing-closure syntax change a call with multiple closures?
MiddleTheoryVery commonWhy does a strong self capture in an escaping closure leak, and what exactly does [weak self] change?
Why does a strong self capture in an escaping closure leak, and what exactly does [weak self] change?
If an object stores an escaping closure that strongly captures self, each side retains the other — a cycle neither releases, so deinit never runs. [weak self] captures self as a weak optional that adds no retain count, so the cycle breaks and self becomes nil.
Common mistakes
- ✗Thinking
[weak self]keepsselfalive rather than avoiding a strong hold - ✗Blaming the thread rather than the mutual strong references
- ✗Believing only one side of the cycle holds a reference
Follow-up questions
- →When would you choose
[unowned self]over[weak self]? - →How do you guard against
selfbeingnilinside the closure body?
JuniorTheoryCommonFunctions vs methods — what do static func and class func mean on a type, and which can a subclass override?
Functions vs methods — what do static func and class func mean on a type, and which can a subclass override?
A method belongs to a type and gets an implicit self; a free function has none. Both static func and class func are type-level, but only class func can be overridden by a subclass; static func is effectively final.
Common mistakes
- ✗Assuming
static funccan be overridden likeclass func - ✗Thinking a method and a free function are the same thing (no
self) - ✗Believing
class funcandstatic funcare interchangeable
Follow-up questions
- →Why can a subclass override
class funcbut notstatic func? - →When would you prefer
static funcoverclass funcon a class?
JuniorCodeCommonMutate a caller's array in place via an inout parameter
Mutate a caller's array in place via an inout parameter
inout uses copy-in/copy-out — the callee mutates a local copy that is written back to the caller's variable at return, called with &list. You cannot capture it in an escaping closure because the parameter's lifetime ends at return, so a deferred write would hit a dead variable.
Common mistakes
- ✗Thinking
inoutpasses by reference like a C pointer rather than copy-in/copy-out - ✗Forgetting the
&prefix at the call site - ✗Believing you can stash an
inoutparameter in an escaping closure for later
Follow-up questions
- →Why does copy-out happen at return rather than at each write inside the callee?
- →How is an
inoutparameter different from passing aclassinstance?
MiddleDebuggingCommonA stored completion handler keeps its view controller alive — trace the ownership and fix the leak
A stored completion handler keeps its view controller alive — trace the ownership and fix the leak
The immortal singleton Downloader.shared stores onDone, which strongly captures the controller. Since start never calls or clears it, the closure pins the controller and deinit never runs. Fix it with [weak self] plus onDone = nil after use.
Common mistakes
- ✗Blaming a background thread rather than the strong capture kept by the singleton
- ✗Thinking the view controller retains the singleton instead of the reverse
- ✗Believing
@escapingitself, not the never-cleared stored closure, causes the leak
Follow-up questions
- →Why does clearing
onDoneafter invoking it matter even with[weak self]? - →When would
[unowned self]be wrong for a handler that may outlive the controller?
MiddleTheoryCommonEscaping vs non-escaping closures — what does @escaping change, and why is non-escaping the default?
Escaping vs non-escaping closures — what does @escaping change, and why is non-escaping the default?
@escaping marks a closure that may outlive the call — stored or run on another thread. Non-escaping is the default so the compiler can assume the closure runs before return, enabling optimizations and no explicit self. Escaping closures require explicit self.
Common mistakes
- ✗Thinking
@escapingis the default and non-escaping is opt-in - ✗Believing
@escapinghas no effect onselfcapture rules - ✗Assuming a non-escaping closure can be stored and called after return
Follow-up questions
- →Why does an escaping closure force you to write
selfexplicitly? - →Which optimizations does the compiler gain from a non-escaping closure?
MiddleCodeCommonRewrite an accumulating loop with reduce, then implement your own reduce
Rewrite an accumulating loop with reduce, then implement your own reduce
The loop becomes xs.reduce(0, +). reduce folds a sequence into a single value by threading an accumulator through a combining closure. Your myReduce starts from initial, iterates over self, reassigns result = next(result, element) per element, and returns the final accumulator.
Common mistakes
- ✗Confusing
reduce(fold to one value) withmap(element-wise transform) - ✗Forgetting
reduceneeds an initial accumulator value - ✗Thinking
reducemutates the source sequence
Follow-up questions
- →When is
reduce(into:)preferable toreduce(_:_:)? - →How does
reducediffer frommapfollowed by a summation?
JuniorTheoryOccasionalAre closures value types or reference types in Swift, and what follows from the answer?
Are closures value types or reference types in Swift, and what follows from the answer?
Closures are reference types. Assigning one closure to two variables shares a single instance, and captured variables are held by reference, so a mutation through one copy is seen through the other. That shared capture is also why closures can form retain cycles.
Common mistakes
- ✗Assuming closures are copied by value like structs
- ✗Thinking captured variables are snapshotted by value by default
- ✗Believing closures cannot participate in retain cycles
Follow-up questions
- →How does a capture list change the default reference capture?
- →Why does the reference nature of closures enable retain cycles?
MiddleCodeOccasionalPredict the output when a captured variable is mutated after capture
Predict the output when a captured variable is mutated after capture
byValue() prints 1; byReference() prints 99. A capture list [x] copies x by value when the closure is created, so a later mutation of x is invisible to it. Without a capture list the closure captures the variable by reference and reads its current value at call time, 99.
Common mistakes
- ✗Thinking a capture list is only a style hint with no runtime effect
- ✗Believing a bare capture snapshots the value at creation time
- ✗Assuming both closures always print the same value
Follow-up questions
- →How does
[weak self]differ from[x]in what it captures? - →Why does reference capture read the value at call time, not creation?
MiddleTheoryOccasionalWhat do @discardableResult, @inlinable, and @frozen do, and what does each one commit you to?
What do @discardableResult, @inlinable, and @frozen do, and what does each one commit you to?
@discardableResult silences the unused-result warning — no ABI impact. @inlinable exposes a function's body across modules so callers can inline it, committing that body to your ABI. @frozen promises a type's layout never changes, letting clients optimize but freezing evolution.
Common mistakes
- ✗Thinking
@inlinablehas no cost — it locks the body into your ABI - ✗Believing
@frozenlets you keep evolving the type's layout later - ✗Assuming
@discardableResultchanges when the result is computed
Follow-up questions
- →Why does
@frozenmatter only for library-evolution-enabled modules? - →What breaks a client if you change an
@inlinablefunction's body?
SeniorDesignOccasionalYou are designing the public API of a networking module. Pick one delivery style and defend it against the alternatives — an escaping-closure completion handler, a Result-returning completion handler, an async throws function, or an async stream AsyncSequence. The call sites are modern Swift 6 code that already uses structured concurrency; most requests return a single value, but one endpoint streams progress updates over time. You must support cancellation, clean error propagation, and testability while keeping the API small. Which style do you choose for the single-value requests and which for the streaming endpoint, and how do you justify each choice over the rejected options in terms of cancellation, error handling, and how they compose with await?
You are designing the public API of a networking module. Pick one delivery style and defend it against the alternatives — an escaping-closure completion handler, a Result-returning completion handler, an async throws function, or an async stream AsyncSequence. The call sites are modern Swift 6 code that already uses structured concurrency; most requests return a single value, but one endpoint streams progress updates over time. You must support cancellation, clean error propagation, and testability while keeping the API small. Which style do you choose for the single-value requests and which for the streaming endpoint, and how do you justify each choice over the rejected options in terms of cancellation, error handling, and how they compose with await?
Choose async throws for single-value requests and an AsyncSequence for the streaming endpoint. async throws composes with await, propagates errors via try, and gets cooperative cancellation, whereas completion handlers and Result need manual cancellation and nest callbacks. AsyncSequence fits the endpoint yielding values over time.
Common mistakes
- ✗Claiming completion handlers cancel more cleanly than structured
asynctasks - ✗Forcing every endpoint into one style regardless of single vs streaming shape
- ✗Believing
async throwsloses error information versusResult
Follow-up questions
- →How does task cancellation reach an in-flight
asyncnetwork call? - →When would you still expose a completion-handler overload for callers?
SeniorTheoryOccasionalWhat does @autoclosure do, and why do ?? and assert use it?
What does @autoclosure do, and why do ?? and assert use it?
@autoclosure wraps an argument expression in a closure, so it runs only when the callee invokes it. ?? uses it so the right-hand default is evaluated only when the left side is nil; assert uses it to skip its condition in release builds — plain call syntax with lazy evaluation.
Common mistakes
- ✗Thinking
@autoclosureevaluates the argument eagerly - ✗Believing it is about performance or inlining rather than deferred evaluation
- ✗Assuming the
??default is always computed regardless of the left side
Follow-up questions
- →What readability risk does overusing
@autoclosureintroduce? - →How does
@autoclosure @escapingchange when the wrapped expression may run?