Need your help in getting understanding how Swift capture semantics working when nested function called from closure. So, I have two methods loadHappinessV1
and loadHappinessV2
.
In method loadHappinessV1
:
self
is not specified: error: reference to property 'callbackQueue' in closure requires explicit 'self.' to make capture semantics explicit
self
.In method loadHappinessV2
:
Why in method loadHappinessV2
compiler does not raise error about capture semantics? Are the nested functions (together with variable callbackQueue
) not captured?
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 .(")
}
}
I found some explanation how capture semantics working with nested functions. Source: Nested functions and reference capturing.
Consider 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
}
}
Compiler reminds us to maintain ownership in function closureA
. But does not tell anything about capturing self
in function functionA
.
Lets look on 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
}
The line strong_retain %0 : $Test, loc "Test.swift":9:16, scope 3 // id: %3
tells us that compiler making strong reference for $Test
(which is defined as self
), this reference lives in scope 3
(which is functionA
) and not released at a time of leaving scope 3
.
Second function closureA
deals with optional reference to self
. It is represented in 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 nested function accesses some properties defined in self
, then nested function keeps strong reference to self
. Compiler does not notify about it (Swift 3.0.1).
To avoid this behaviour we just need to use closures instead nested functions. Then compiler will notify about self
usage.
Original example could be rewtitten as following:
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 .(")
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With