swift 函数可以在异步闭包中调用自己吗?

aiqt4smr  于 2023-06-21  发布在  Swift
关注(0)|答案(4)|浏览(157)

以下是有关守则的要点:我们有一些Int @State,我们希望以秒为间隔倒计时到零,但从函数到自身添加闭包到分派队列似乎不起作用:

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)
    }
  ...

这种模式在原则上是错误的吗?如果是,为什么?最简单的正确模式是什么?

qc6wkl3g

qc6wkl3g1#

这里有一个非常优雅的答案来回答类似的问题: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())       // Recursive call
    }
}

在没有State var的情况下调用:

var countdown: Int = 10

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

如果你需要一个状态变量来进行倒计时:

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

在视图中:

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) 
}

希望能帮上忙。
原因如下:Swift 3.0 Error: Escaping closures can only capture inout parameters explicitly by value

icnyk63a

icnyk63a2#

这种模式在原则上是错误的吗?
是的。不管看起来如何,inout不是通过引用传递。相反,它创建一个副本,然后在函数退出时将其写回原始值。所以DispatchQueue中发生的任何事情都不会影响原始值。
你可以在这里看到更多细节:Swift 3.0 Error: Escaping closures can only capture inout parameters explicitly by value
您尝试实现的一个非常简单的版本是使用每秒触发一次的TimerPublisher。在body中,您可以使用.onReceive修饰符订阅发布者:

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
        }
    }
}
8yoxcaq7

8yoxcaq73#

如上所述,您无法捕获inout参数。这说不通啊inout参数的工作方式是将其复制到函数中,当函数返回时,将其复制回来。这个函数试图在函数返回后继续操纵它;那是无效的。
由于您在这里使用SwiftUI,通常的方法是.task

.task {
        do {
            while countdown > 0 {
                countdown -= 1
                try await Task.sleep(for: .seconds(1))
            }
        } catch {
            // This might be cancelled
            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 {
            // might cancel
            return
        }
    }
}

你应该这样开场:

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

c86crjj04#

非常好的答案,我从阅读中学到了很多,并试图了解它们是如何工作的。关键是闭包参数是按值传递的,因此只要状态可以从一个调用流到另一个调用(通过参数),claude 31的解决方案就是一种直接的方法。
序列参数似乎有点大材小用,除非调用的数量相对较少。如果我想倒计时10,000,000次;这意味着具有10 M个元素的序列的第一调用。编译器可能对第一个闭包和后续闭包进行了一些写时复制优化,但我不想对此进行推理。对于我的用例,我修改了它,所以状态(参数大小)是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)
    }
  }
}

我认为这可能并不完美,因为闭包的执行次数没有得到保证,所以连续调用之间的间隔可能不是我想要的。我没有读过DispatQueue的保证,但是连续的闭包可能会以不同的顺序执行(不同于它们被添加的顺序)。如果这是一个问题,那么Timer发布者解决方案听起来更可取,也是一个更好的方法。

相关问题