Swift: capture semantics when calling a nested function from closure. Why does the compiler not cause an error?

I need your help in understanding how the semantics of Swift capture work when a nested function is called from closure. So, I have two methods loadHappinessV1 and loadHappinessV2 .

In the loadHappinessV1 method:

  • The compiler causes an error if self not specified: error: a reference to the 'callbackQueue' property in the closure requires an explicit "I". to make capture semantics explicit
  • To prevent a compiler error, I provide a weak reference to self .

In the loadHappinessV2 method:

  • I decided to introduce two nested functions and simplify the "body" of the operation.
  • The compiler does not raise an error about capture semantics.

Why loadHappinessV2 compiler not cause an error in the capture semantics in the loadHappinessV2 method? Are nested functions nested (along with the callbackQueue variable)?

Thanks!

 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: (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: (LoadResult) -> Void) { operationQueue.cancelAllOperations() func completeWithFailure() { callbackQueue.async { completion(.failure) } } func completeWithSuccess() { 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 .(") } } 
+5
source share
2 answers

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>, #Optional.some!enumelt.1, %0 : $Test, loc "Test.swift":13:38, scope 8 // users: %7, %6 store_weak %5 to [initialization] %3 : $*@sil_weak Optional<Test>, loc "Test.swift":13:38, scope 8 // id: %6 release_value %5 : $Optional<Test>, loc "Test.swift":13:38, scope 8 // id: %7 // function_ref Test.(closureA() -> () -> ()).(closure #1) %8 = function_ref @Test.Test.(closureA () -> () -> ()).(closure #1) : $@convention (thin) (@owned @box @sil_weak Optional<Test>) -> (), loc "Test.swift":13:30, scope 8 // user: %11 strong_retain %2 : $@box @sil_weak Optional<Test>, loc "Test.swift":13:30, scope 8 // id: %9 mark_function_escape %3 : $*@sil_weak Optional<Test>, loc "Test.swift":13:30, scope 8 // id: %10 %11 = partial_apply %8(%2) : $@convention (thin) (@owned @box @sil_weak Optional<Test>) -> (), loc "Test.swift":13:30, scope 8 // users: %14, %12 debug_value %11 : $@callee _owned () -> (), let, name "nestedClosureA", loc "Test.swift":13:13, scope 7 // id: %12 strong_release %2 : $@box @sil_weak Optional<Test>, loc "Test.swift":15:9, scope 7 // id: %13 return %11 : $@callee _owned () -> (), loc "Test.swift":16:9, scope 7 // id: %14 } 

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 .(") } } 
+2
source

My first guess: Swift implicitly defines nested functions of both @noescape and Autoclosure functions. ( some info here ). With any of these types you do not need to use "self", and the hapynessOp block will capture links to nested functions, so there will be no problems there.

Otherwise, it may be that the nested functions are actually added to the class signature. I think that you can conduct some testing and find out (can do it).

-1
source

All Articles