避免在闭包中捕获值副本的最佳方法

时间:2015-05-05 17:27:41

标签: swift

struct Foo {

    var i = 0 { didSet { println("Current i: \(i)") } }

    func delayedPrint() {
        dispatch_async(dispatch_get_main_queue(), { _ in
            println("Closure i: \(self.i)")
        })
    }

    mutating func foo() {
        delayedPrint()
        i++
    }
}

现在输出

var a = Foo()
a.foo()

Current i: 1
Closure i: 0 // I want current value here.

我想知道什么是避免捕获ivar副本的最佳方式。

编辑1

是的,上课是第一次也是我唯一想到的,但是......这个骗过我,认为它可以用结构以某种方式完成......为什么会这样?

mutating func foo() {
    delayedPrint()
    dispatch_async(dispatch_get_main_queue(), { _ in
        println("From foo: \(self.i)")
    })
    delayedPrint()
    i++
}

输出:

Current i: 1
Closure i: 0
From foo: 1
Closure i: 0

3 个答案:

答案 0 :(得分:4)

我认为你必须在这里使用类而不是结构,因为结构通过引用传递副本和类

答案 1 :(得分:2)

  

我想知道什么是避免捕获ivar副本的最佳方式。

这是一种误解。你不能抓住一个伊娃"通过这种方式。你捕获的是self!这正是Swift迫使你 self的原因,以便你理解这个事实。因此,self 是什么样的事情。这就是为什么self是结构还是类的重要性。类实例是可变的;结构实例不是,因此在捕获时会复制该副本,并且该副本会独立存在。

但是,你可以捕获一个简单的Int(即一个ivar),当你这样做时,你会得到你期望的结果:

var i = 0
struct Foo {
    func delayedPrint() {
        dispatch_async(dispatch_get_main_queue(), {
            println(i) // 1
        })
    }
    func foo() {
        delayedPrint()
        i++
    }
}

现在让我们谈谈你摆出的第二个谜题。这是一个重写,以澄清谜题是什么:

func delay(delay:Double, closure:()->()) {
    dispatch_after(
        dispatch_time(
            DISPATCH_TIME_NOW,
            Int64(delay * Double(NSEC_PER_SEC))
        ),
        dispatch_get_main_queue(), closure)
}
struct Foo {
    var i = 0
    mutating func foo() {
        delay(0.5) {
            println("From foo: \(self.i)") // 1
        }
        bar(2)
        i++
    }
    func bar(d:Double) {
        delay(d) {
            println("from bar: \(self.i)") // 0
        }
    }
}

我会像这样测试它:

var a = Foo()
a.foo()
a.bar(1)

控制台显示:

From foo: 1 [after half a second]
from bar: 1 [after 1 second]
from bar: 0 [after 2 seconds]

那么bar如何在第二次以后第二次调用,但显示之前的值self.i ?为什么foo表现不同?

答案与事务内部发生的一切事实有关 - 包括匿名函数的定义。代码必须在某些时候运行。在此之前,未定义匿名函数。

  1. 首先考虑a.bar(1)。这会导致bar运行并定义将捕获self的匿名函数。但这发生在之后我们调用foo并递增i。因此,此时捕获的self会增加i

  2. 接下来让我们考虑foo调用bar时会发生什么。它在递增i之前执行此操作。所以现在bar运行并且定义了匿名函数,并且self的{​​{1}}仍然设置为0.这个结果在两秒钟后到达控制台的事实无关紧要;重要的是捕获发生的时间。

  3. 最后,我们来到i内部匿名函数的令人惊讶的情况。显然,fooi++的存在会产生重大影响。为什么?好吧,当foo运行时,它定义了一个捕获foo的匿名函数。但 self 已在self内捕获<{1}},目的是说foo - 这真的是{{1} }。因此,这个匿名函数也可以看到i++执行self.i++的更改,因为它们正在查看相同的self

    换句话说,我建议您点击一个本身变异i++的函数中定义的匿名函数的神秘边缘案例。 (我不知道我是否认为这是一个错误;我将把它提交给开发论坛,看看他们的想法。)

答案 2 :(得分:0)

为了补充@nikolayn的完美答案,这里有一个可以在控制台中运行的示例,它演示了如何使用类而不是结构来完成此操作(并且还没有数据竞争): (变量是显式定义的,以便您可以轻松调试)

import Foundation
import Cocoa


let queue = dispatch_queue_create("sync_queue", nil)!

class Foo {

    var i = 0 { didSet { println("Current i: \(i)") } }
    let sem = dispatch_semaphore_create(0);

    func delayedPrint() {
        let i_copy = i
        let t = dispatch_time(DISPATCH_TIME_NOW, Int64(1 * Double(NSEC_PER_SEC)))
        dispatch_after(t, queue) { _ in
            let _i = self.i
            let _i_copy = i_copy
            println("Closure i: \(_i)")
            println("Closure i_copy: \(_i_copy)")
            dispatch_semaphore_signal(self.sem)
        }
    }

    func foo() {
        delayedPrint()
        dispatch_async(queue) {
            self.i++
        }
    }

    func wait() {
        dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER)
    }
}

var a = Foo()
a.foo()
a.wait()