一个函数在异步闭包中能否调用自身?

huangapple go评论59阅读模式
英文:

Can a function call itself in an async closure?

问题

以下是问题代码的要点:我们有一些Int @State,我们想要以秒为间隔倒计时到零,但是将闭包从函数中添加到自身的调度队列似乎不起作用:

func counting(value: inout Int) {
  value -= 1
  if value > 0 {
    // 错误:逃逸闭包捕获 'inout' 参数 'value'
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
      counting(value: &value)
    }
  }
}
...
@State private var countdown: Int
...
// 启动倒计时正常运行
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
  counting(value: &countdown)
}
...

这种模式在原则上是否有问题?如果是的话,为什么?使用的最简单正确模式是什么?

英文:

The following is the essence of the code in question: we have some Int @State that we want to countdown to zero with second intervals but adding closures to the dispatch queue from a function to itself does not seem to work:

func counting(value: inout Int) {
  value -= 1
  if value > 0 {
    // ERROR: Escaping closure captures 'inout' parameter 'value'
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
      counting(value: &value)
    }
  }
}
...
  @State private var countdown: Int
  ...
  // kickstarting the countdown works ok
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
      counting(value: &countdown)
    }
  ...

Is this pattern wrong in principle? If yes, why and what is the simplest correct pattern to use?

答案1

得分: 1

这里有一个类似的问题的非常优雅的答案:https://stackoverflow.com/questions/64202210/dispatchqueue-main-asyncafter-not-delaying
递归模式将如下使用:

func counting<T: Sequence>(in sequence: T) where T.Element == Int {
    guard let step = sequence.first(where: { _ in true }) else { return }   
    print("step", step)
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
        counting(in: sequence.dropFirst())       // 递归调用
    }
}

并且可以不使用 State var 进行调用:

var countdown: Int = 10

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
    counting(in: 0..<countdown)
}

如果你需要一个 State var 来进行倒计时:

@State var countdown: Int = 10
var initialCount = 10

在视图中:

```swift
func counting<T: Sequence>(in sequence: T) where T.Element == Int {
    guard let step = sequence.first(where: { _ in true }) else { return }      
    print("step", step)
    self.countdown -= 1
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
        counting(in: sequence.dropFirst())
    }
}

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
    counting(in: 0..<initialCount) 
}

希望对你有所帮助。

关于为什么要这样做的原因,请参考:https://stackoverflow.com/questions/39569114/swift-3-0-error-escaping-closures-can-only-capture-inout-parameters-explicitly

英文:

There was a very elegant answer to a similar question here: https://stackoverflow.com/questions/64202210/dispatchqueue-main-asyncafter-not-delaying
The recursive pattern would be used as follows:

func counting&lt;T: Sequence&gt;(in sequence: T) where T.Element == Int {
    guard let step = sequence.first(where: { _ in true }) else { return }   
    print(&quot;step&quot;, step)
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
        counting(in: sequence.dropFirst())       // Recursive call
    }
}

And called without State var:

var countdown: Int = 10

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
    counting(in: 0..&lt;countdown)
}

If you need a State var for the countdown:

@State var countdown: Int = 10
var initialCount = 10

In the View:

func counting&lt;T: Sequence&gt;(in sequence: T) where T.Element == Int {
    guard let step = sequence.first(where: { _ in true }) else { return }      
    print(&quot;step&quot;, step)
    self.countdown -= 1
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
        counting(in: sequence.dropFirst())
    }
}

DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.0) {
    counting(in: 0..&lt;initialCount) 
}

Hope that helps.

For the reason why: https://stackoverflow.com/questions/39569114/swift-3-0-error-escaping-closures-can-only-capture-inout-parameters-explicitly

答案2

得分: 0

这个模式从原则上讲是错误的吗?

是的。尽管看起来似乎是这样,但 inout 不是传递引用的方式。相反,它创建一个副本,然后在函数退出时将其写回原始值。因此,在 DispatchQueue 中发生的任何事情都不会影响原始值。

您可以在这里详细了解它:Swift 3.0错误:逃逸闭包只能以明确的方式按值捕获inout参数


您尝试实现的一个更简单的版本是使用 TimerPublisher,它每秒触发一次。在函数体内,您可以使用 .onReceive 修饰符订阅该发布者:

struct ContentView: View {  
    @State var value: Int = 10
    
    // 创建一个每秒触发一次的 `TimerPublisher`
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        Text(value.formatted())
            .onReceive(timer) {_ in
                // 订阅 `timer`,并在每次 `timer` 触发时执行 `counting`
                counting()
            }
    }
    
    func counting() {
        if value == 0 {
            // 一旦 `value` 达到 0,就取消原始计时器
            timer.upstream.connect().cancel()
        } else {
            value -= 1
        }
    }
}
英文:

> Is this pattern wrong in principle?

Yes. Despite what it seems like, inout is not a pass-by-reference. Instead it creates a copy, then write it back to the original value when the function exit. So anything that happens in the DispatchQueue won't affect the original value.

You can see more detail about it here: Swift 3.0 Error: Escaping closures can only capture inout parameters explicitly by value


A much simple version of what you were trying to achieve is to use a TimerPublisher, that fires every second. And within the body, you can use .onReceive modifier to subscribe to the publisher:

struct ContentView: View {  
    @State var value: Int = 10
    
    // creates a `TimerPublisher` that fires every second
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()

    var body: some View {
        Text(value.formatted())
            .onReceive(timer) {_ in
                // subscribe to `timer`, and perform `counting` every time the `timer` fires
                counting()
            }
    }
    
    func counting() {
        if value == 0 {
            // cancels the original timer once `value` reaches 0
            timer.upstream.connect().cancel()
        } else {
            value -= 1
        }
    }
}

答案3

得分: 0

正如注意到的,你不能捕获inout参数。这没有意义。inout参数的工作方式是它被复制到函数内部,在函数返回时再次被复制回来。这个函数在函数返回后尝试继续操作它,这是无效的。

由于你在这里使用SwiftUI,通常的方法是使用.task

.task {
    do {
        while countdown > 0 {
            countdown -= 1
            try await Task.sleep(for: .seconds(1))
        }
    } catch {
        // 这可能会被取消
        return
    }
}

如果你真的想要一个像你描述的修改一个值的递归函数(在这个示例中并不是非常明智,但是合法),你可以使用一个Binding:

@MainActor
func counting(value: Binding<Int>) async {
    value.wrappedValue -= 1
    if value.wrappedValue > 0 {
        do {
            try await Task.sleep(for: .seconds(1))
            await counting(value: value)
        } catch {
            // 可能会取消
            return
        }
    }
}

你可以这样启动它:

.task {
    await counting(value: $countdown)
}
英文:

As noted, you cannot capture inout parameters. That doesn't make sense. The way an inout parameter works is that it is copied into the function, and when the function returns, it is copied back. This function tries to keep manipulating it after the function returns; that's invalid.

Since you're using SwiftUI here, the usual approach would be .task:

    .task {
        do {
            while countdown &gt; 0 {
                countdown -= 1
                try await Task.sleep(for: .seconds(1))
            }
        } catch {
            // This might be cancelled
            return
        }
    }

If you really want a function like you've described, that modifies a value recursively (this isn't really very sensible for this example, but it is legal), you'd use a Binding:

@MainActor
func counting(value: Binding&lt;Int&gt;) async {
    value.wrappedValue -= 1
    if value.wrappedValue &gt; 0 {
        do {
            try await Task.sleep(for: .seconds(1))
            await counting(value: value)
        } catch {
            // might cancel
            return
        }
    }
}

You'd kick it off with:

    .task {
        await counting(value: $countdown)
    }

答案4

得分: 0

非常好的回答,我从阅读中学到了很多,试图理解它们的工作原理。关键是闭包参数按值传递,只要状态可以通过调用之间的参数传递(通过参数)流动,claude31的解决方案是一种直接的方法。

序列参数似乎有点过度,除非调用次数相对较少。如果我想要倒计时10,000,000次,那就意味着首次调用时序列中有10M个元素。编译器可能对第一个和连续的闭包进行一些写时复制优化,但我不想对此进行推理。对于我的用例,我进行了修改,以使状态(参数大小)为O(1):

struct MyView: View {
  @State private var countdown: Double = 0
  ...
  func counting(decrement: Double, remaining: Double, limit: Double = 0.0) {
    let newRemaining = Double.maximum(remaining - decrement, limit)
    self.countdown = newRemaining
    if newRemaining == limit { return }
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + decrement) {
      counting(decrement: decrement, remaining: newRemaining, limit: limit)
    }
  }
  ...
  var body: some View {
    ...
    // kickstart it 
    DispatchQueue.main.asyncAfter(...) {
      self.counting(decrement: decrement, remaining: countdown, limit: 0)
    }
  }
}

我认为可能不是完美的,因为不能保证闭包执行的次数,因此连续调用之间的间隔可能不是我想要的。我还没有阅读DispatchQueue的保证,但很可能连续的闭包可能按不同于添加它们的顺序的顺序执行。如果这是个问题,那么Timer发布者解决方案听起来更可取,也是更好的选择。

英文:

Very good answers and I learned a lot from reading them and trying to understand how they work. The key is that the closure parameters are pass by value so as long as the state can flow from call to call (through the params) the claude31's solution is a straightforward way to go.

The sequence argument seemed like a bit of an overkill except for when the number of calls is relatively small. What if I want to countdown 10,000,000 times; that implies a first call with a sequence with 10M elements. The compiler may have some copy-on-write optimization here for the first and successive closures but I don't want to have to reason about it. For my use case I modified it so the state (params size) is O(1):

struct MyView: View {
  @State private var countdown: Double = 0
  ...
  func counting(decrement: Double, remaining: Double, limit: Double = 0.0) {
    let newRemaining = Double.maximum(remaining - decrement, limit)
    self.countdown = newRemaining
    if newRemaining == limit { return }
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + decrement) {
      counting(decrement: decrement, remaining: newRemaining, limit: limit)
    }
  }
  ...
  var body: some View {
    ...
    // kickstart it 
    DispatchQueue.main.asyncAfter(...) {
      self.counting(decrement: decrement, remaining: countdown, limit: 0)
    }
  }
}

I think it's possible that this is not perfect, in that the times the closure executes are not guaranteed so the spacings between the successive calls may not be what I want them to be. I haven't read the DispatQueue's guarantees but it's probably not impossible that the successive closures may execute in a different order (different than the order in which they were added). If this is a problem, then the Timer publisher solution sounds bery much preferable and a better way to go.

huangapple
  • 本文由 发表于 2023年6月13日 03:06:49
  • 转载请务必保留本文链接:https://go.coder-hub.com/76459622.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定