ProgrammingiOS Developer

Explain the work of closures in Swift: the difference from functions, features of variable capture, possible problems with incorrect usage.

Pass interviews with Hintsage AI assistant

Answer

Closures in Swift are self-contained blocks of code that can capture and store references to variables and constants from their surrounding context. They allow for organizing callbacks, asynchronous processing, and storing code execution as variable values.

Differences from Functions

  • Closures can capture variables from the outer scope.
  • They have a more compact syntax.
  • They can be passed as values.

Example of a Closure:

var counter = 0 let incrementer: () -> Void = { counter += 1 } incrementer() print(counter) // 1

Features of Capture

  • By default, variables are captured strongly (by strong reference).
  • Capture rules can be explicitly defined through a capture list ([weak self], [unowned self]).

Problems

  • The main problem is possible retain cycles when capturing self inside a closure.
  • In multithreaded code, stale variable values can be captured unknowingly.

Tricky Question

What is the difference between strong capture and using [weak self] or [unowned self] in the capture list of a closure? What are the risks?

Answer: If you do not specify [weak self] or [unowned self], the closure will by default capture self by strong reference, which will lead to a retain cycle if the closure and self reference each other. [weak self] captures self by weak reference (optional), and [unowned self] captures by unowned reference (non-optional). Improper choice or absence of a capture list leads to memory leaks or crashes.

class Test { var closure: (() -> Void)? func setup() { closure = { [weak self] in self?.doWork() } } func doWork() { ... } }

Examples of Real Errors Due to Ignorance of the Nuances of the Topic


Story

A programmer did not use a capture list in a closure within a UIViewController that initiated asynchronous data loading. As a result, the controller and its view were not released, leading to a memory leak upon dismissal. An analysis of the infrastructure revealed hundreds of "stuck" controllers in memory.


Story

A closure captured self using [unowned self], but the lifecycle of self ended before the closure. This resulted in a crash when accessing an already released object — the application crashed when attempting to work with self inside the closure.


Story

In a project, within a nested closure, two variables were captured: one strongly, the other weakly. It turned out that the weak reference was nilled too early, and the data needed for the closure's operation was lost — the bug only manifested during a stress test under load in a multithreaded environment.