I found some explanation of how capture semantics work with nested functions. Source: Nested Functions and Link Capture .
Consider the following example:
class Test { var bar: Int = 0 func functionA() -> (() -> ()) { func nestedA() { bar += 1 } return nestedA } func closureA() -> (() -> ()) { let nestedClosureA = { [unowned self] () -> () in self.bar += 1 } return nestedClosureA } }
The compiler reminds us to retain ownership of the closureA function. But nothing is said about capturing self in functionA functionA .
Let's take a look at Swift Intermediate Language ( SIL ):
xcrun swiftc -emit-silgen Test.swift | xcrun swift-demangle > Test.silgen
sil_scope 2 { loc "Test.swift":5:10 parent @Test.Test.functionA () -> () -> () : $@convention (method) (@guaranteed Test) -> @owned @callee_owned () -> () } sil_scope 3 { loc "Test.swift":10:5 parent 2 } // Test.functionA() -> () -> () sil hidden @Test.Test.functionA () -> () -> () : $@convention (method) (@guaranteed Test) -> @owned @callee_owned () -> () { // %0 // users: %4, %3, %1 bb0(%0 : $Test): debug_value %0 : $Test, let, name "self", argno 1, loc "Test.swift":5:10, scope 2 // id: %1 // function_ref Test.(functionA() -> () -> ()).(nestedA #1)() -> () %2 = function_ref @Test.Test.(functionA () -> () -> ()).(nestedA #1) () -> () : $@convention (thin) (@owned Test) -> (), loc "Test.swift":9:16, scope 3 // user: %4 strong_retain %0 : $Test, loc "Test.swift":9:16, scope 3 // id: %3 %4 = partial_apply %2(%0) : $@convention (thin) (@owned Test) -> (), loc "Test.swift":9:16, scope 3 // user: %5 return %4 : $@callee _owned () -> (), loc "Test.swift":9:9, scope 3 // id: %5 }
Line strong_retain %0 : $Test, loc "Test.swift":9:16, scope 3 // id: %3 tells us that the compiler makes a strong link for $Test (which is defined as self ), this link is in the scope 3 (which is functionA ) and is not released at a time leaving area 3 .
The second closureA function is associated with an optional self reference. It is represented in the code as %2 = alloc_box $@sil _weak Optional<Test>, var, name "self", loc "Test.swift":13:38, scope 8 // users: %13, %11, %9, %3 .
sil [transparent] [fragile] @Swift.Int.init (_builtinIntegerLiteral : Builtin.Int2048) -> Swift.Int : $@convention (method) (Builtin.Int2048, @thin Int.Type) -> Int sil_scope 6 { loc "Test.swift":12:10 parent @Test.Test.closureA () -> () -> () : $@convention (method) (@guaranteed Test) -> @owned @callee_owned () -> () } sil_scope 7 { loc "Test.swift":17:5 parent 6 } sil_scope 8 { loc "Test.swift":15:9 parent 7 } // Test.closureA() -> () -> () sil hidden @Test.Test.closureA () -> () -> () : $@convention (method) (@guaranteed Test) -> @owned @callee_owned () -> () { // %0 // users: %5, %4, %1 bb0(%0 : $Test): debug_value %0 : $Test, let, name "self", argno 1, loc "Test.swift":12:10, scope 6 // id: %1 %2 = alloc_box $@sil _weak Optional<Test>, var, name "self", loc "Test.swift":13:38, scope 8 // users: %13, %11, %9, %3 %3 = project_box %2 : $@box @sil_weak Optional<Test>, loc "Test.swift":13:38, scope 8 // users: %10, %6 strong_retain %0 : $Test, loc "Test.swift":13:38, scope 8 // id: %4 %5 = enum $Optional<Test>,
So, if a nested function accesses some properties defined in self , then the nested function retains a strong reference to self . The compiler does not notify about this (Swift 3.0.1).
To avoid this behavior, we just need to use locks instead of nested functions. Then the compiler will notify you of using self .
The original example can be rewritten as follows:
import PlaygroundSupport import Cocoa PlaygroundPage.current.needsIndefiniteExecution = true struct Happiness { final class Net { enum LoadResult { case success case failure } private var callbackQueue: DispatchQueue private lazy var operationQueue = OperationQueue() init(callbackQueue: DispatchQueue) { self.callbackQueue = callbackQueue } func loadHappinessV1(completion: @escaping (LoadResult) -> Void) { operationQueue.cancelAllOperations() let hapynessOp = BlockOperation { [weak self] in let hapynessGeneratorValue = arc4random_uniform(10) if hapynessGeneratorValue % 2 == 0 { // callbackQueue.async { completion(.success) } // Compile error self?.callbackQueue.async { completion(.success) } } else { // callbackQueue.async { completion(.failure) } // Compile error self?.callbackQueue.async { completion(.failure) } } } operationQueue.addOperation(hapynessOp) } func loadHappinessV2(completion: @escaping (LoadResult) -> Void) { operationQueue.cancelAllOperations() // Closure used instead of nested function. let completeWithFailure = { [weak self] in self?.callbackQueue.async { completion(.failure) } } // Closure used instead of nested function. let completeWithSuccess = { [weak self] in self?.callbackQueue.async { completion(.success) } } let hapynessOp = BlockOperation { let hapynessGeneratorValue = arc4random_uniform(10) if hapynessGeneratorValue % 2 == 0 { completeWithSuccess() } else { completeWithFailure() } } operationQueue.addOperation(hapynessOp) } } } // Usage let happinessNetV1 = Happiness.Net(callbackQueue: DispatchQueue.main) happinessNetV1.loadHappinessV1 { switch $0 { case .success: print("Happiness V1 delivered .)") case .failure: print("Happiness V1 not available at the moment .(") } } let happinessNetV2 = Happiness.Net(callbackQueue: DispatchQueue.main) happinessNetV2.loadHappinessV2 { switch $0 { case .success: print("Happiness V2 delivered .)") case .failure: print("Happiness V2 not available at the moment .(") } }