How to Capture Local Variable Inside an Async Closure in Swift

Unowned variables and async functions

Seems like an x-y problem. If a Student's school can go out of existence while this Student still exists, that is an incorrect use of unowned. The precondition for saying unowned is that that must be absolutely impossible.

Pass variable to dataTask async function

For this purpose DispatchGroup is the recommended API for example

var neededVariable = 1
let group = DispatchGroup()
let session = URLSession.shared
for _ in 0...10 {
group.enter()
let urlS = "https://sub2pewdiepie.com/subscribe.php?param1=123"
let url = URL(string: urlS.addingPercentEncoding(withAllowedCharacters: CharacterSet.urlQueryAllowed)!)!

let task = session.dataTask(with: url, completionHandler: { data, response, error in
defer(group.leave())
if let error = error { print(error); return }
do {
neededVariable += 1
print(neededVariable)
} catch let error { print(error.localizedDescription) }
})
task.resume()

}

group.notify(queue: DispatchQueue.main) {
print("notify", neededVariable)
}

How closure captures values in Swift?

From Swift Programming Guide - Closures

A closure can capture constants and variables from the surrounding context in which it’s defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.

A closure captures variables, not the contents of variables. When we are talking about local variables in a function (which are normally allocated on stack), it makes sure they are accessible even when the function exits and other local variables are deallocated, therefore we can do things like this:

func myFunc() {
var array: [Int] = []

DispatchQueue.main.async {
// executed when myFunc has already returned!
array.append(10)
}
}

Your example is similar. The closure captures the variable. This is a variable on module level, therefore its scope always exists. When you reassign its value, it will affect the value read inside the closure.

Or, in other words, the closure will be equivalent to:

var closure = {
print(CurrentModule.element?.name ?? "default value")
}

where CurrentModule is the name of your main module (which is usually the name of your project).

To prevent this behavior and capture the value of the variable instead, we can use closure capture list. Unfortunately, the official documentation does not properly explain what exactly is a capture list. Basically, using a capture list you declare variables local to the closure using values that are available when the closure is created.

For example:

var closure = { [capturedElement = element] in
print(capturedElement?.name ?? "default value")
}

This will create a new variable capturedElement inside the closure, with the current value of variable element.

Of course, usually we just write:

var closure = { [element] in
print(element?.name ?? "default value")
}

which is a shorthand for [element = element].

How do I access variables that are inside closures in Swift?

var dict: NSDictionary! // Declared in the main thread

The closure is then completed asynchronously so the main thread doesn't wait for it, so

println(dict)

is called before the closure has actually finished. If you want to complete another function using dict then you will need to call that function from within the closure, you can move it into the main thread if you like though, you would do this if you are going to be affecting UI.

var dict: NSDictionary!
NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.mainQueue(), completionHandler: {(response, data, error) in
var jsonError: NSError?
let json = NSJSONSerialization.JSONObjectWithData(data!, options: NSJSONReadingOptions.MutableContainers, error: &jsonError) as? NSDictionary
dict = json
//dispatch_async(dispatch_get_main_queue()) { //uncomment for main thread
self.myFunction(dict!)
//} //uncomment for main thread
})

func myFunction(dictionary: NSDictionary) {
println(dictionary)
}

Passing a local mutable struct into an async let binding

As SE-0317 - async let bindings says:

... async let is similar to a let, in that it defines a local constant that is initialized by the expression on the right-hand side of the =. However, it differs in that the initializer expression is evaluated in a separate, concurrently-executing child task.

The child task begins running as soon as the async let is encountered.

...

A async let creates a child-task, which inherits its parent task's priority as well as task-local values. Semantically, this is equivalent to creating a one-off TaskGroup which spawns a single task and returns its result ...

Similarly to the [group.addTask] function, the closure is @Sendable and nonisolated, meaning that it cannot access non-sendable state of the enclosing context. For example, it will result in a compile-time error, preventing a potential race condition, for a async let initializer to attempt mutating a closed-over variable:

var localText: [String] = ...
async let w = localText.removeLast() // error: mutation of captured var 'localText' in concurrently-executing code

The async let initializer may refer to any sendable state, same as any non-isolated sendable closure.

So, it is not the case that the parameter to data(for:delegate:) is copied and then the asynchronous task is created, but rather the other way around.

Usually, if you were using a closure, you would just add request to the closure’s capture list, but that’s not possible in this case. E.g., you could create a Task yourself with a capture list, achieving something akin to async let, but with greater control:

func test() async throws {
var request = URLRequest(url: URL(string:"https://httpbin.org/get")!)
request.httpMethod = "GET" // just for example

let task = Task { [request] in
try await URLSession.shared.data(for: request).0
}

// do some more stuff in parallel

print("Response body: \(String(data: try await task.value, encoding: .utf8) ?? "Not string")")
}

Obviously, you can simply await the data(for:delegate:), rather than async let, and the problem goes away:

func test() async throws {
var request = URLRequest(url: URL(string:"https://httpbin.org/get")!)
request.httpMethod = "GET" // just for example

let data = try await URLSession.shared.data(for: request).0

print("Response body: \(String(data: data, encoding: .utf8) ?? "Not string")")
}

How exactly are structs captured in escaping closures?

While it might make sense for a struct to be copied, as your code demonstrates, it is not. That's a powerful tool. For example:

func makeCounter() -> () -> Int {
var n = 0
return {
n += 1 // This `n` is the same `n` from the outer scope
return n
}

// At this point, the scope is gone, but the `n` lives on in the closure.
}

let counter1 = makeCounter()
let counter2 = makeCounter()

print("Counter1: ", counter1(), counter1()) // Counter1: 1 2
print("Counter2: ", counter2(), counter2()) // Counter2: 1 2
print("Counter1: ", counter1(), counter1()) // Counter1: 3 4

If n were copied into the closure, this couldn't work. The whole point is the closure captures and can modify state outside itself. This is what separates a closure (which "closes over" the scope where it was created) and an anonymous function (which does not).

(The history of the term "close over" is kind of obscure. It refers to the idea that the lambda expression's free variables have been "closed," but IMO "bound" would be a much more obvious term, and is how we describe this everywhere else. But the term "closure" has been used for decades, so here we are.)

Note that it is possible to get copy semantics. You just have to ask for it:

func foo(){

var wtf = Wtf()

DispatchQueue.global().async { [wtf] in // Make a local `let` copy
var wtf = wtf // To modify it, we need to make a `var` copy
wtf.x = 5
}

Thread.sleep(forTimeInterval: 2)
// Prints 1 as you expected
print("x = \(wtf.x)")
}

In C++, lambdas have to be explicit about how to capture values, by binding or by copying. But in Swift, they chose to make binding the default.

As to why you're allowed to access wtf after it's been captured by the closure, that's just a lack of move semantics in Swift. There's no way in Swift today to express "this variable has been passed to something else and may no longer be accessed in this scope." That's a known limitation of the language, and a lot of work is going into fix it. See The Ownership Manifesto for more.

Swift 3: capture strong self in @escaping closure without asynchronous work

In SecondClass, there is no need to create a strongSelf variable. Where would you put it? The point is that self is guaranteed not to be nil anyway because you are running within the scope of one of its methods.

The same is true of your additional question, but for a different reason. suggestions is now static, so prefixing with self is a matter of syntax, (I am presuming you meant to also prefix the suggest method with static).

However, in FirstClass, there is a subtle difference between capturing strongSelf and not capturing it.

Because you are using [weak self], self could be nil when you enter that block so you need to check against that anyway. One way is to repeatedly use optional chaining, i.e.:

self?.doSomething()
self?.doSomethingElse()

This is saying:

If I have a reference to self, do something. If I still have a
reference to self, do something else.

By adding a strongSelf variable:

guard let strongSelf = self else {
return
}
strongSelf.doSomething()
strongSelf.doSomethingElse()

...you are saying:

do something and do something else if you have a reference to self,
otherwise do nothing.

So, you guarantee that if the first thing happens, so does the second. The approach you take is going to depend on your application.



Related Topics



Leave a reply



Submit