快速关闭:捕获列表是否必须详尽无遗?

时间:2019-02-20 18:43:38

标签: swift memory memory-management closures

假设我有一个这样的Swift类:

@objc final MyClass : NSObject
{
    let classPropertyString = "A class property"


    func doStuff() 
    {
        let localString = "An object local to this function"

        DispatchQueue.global(qos: .userInitiated).async { [classPropertyString] in
             // Do things with 'classPropertyString' and 'localString'
        }
    }
}

我的问题是:当我写一个捕获列表时,我是否应该负责详尽地列出我希望闭包具有强大参考力的所有内容?

换句话说,如果我从捕获列表中省略了localString(如我在此处所做的那样),那么闭包是否仍会自动捕获对其的强引用,或者我在很糟糕的时间内进入了? >

1 个答案:

答案 0 :(得分:2)

您的问题有几个小问题,很难一目了然地回答,但我想我理解潜在的问题,简短的回答是“不”。但是您的示例是不可能的,因此答案是“不可能”。而且,如果有可能,就没有强有力的参考(也不需要参考),因此问题仍然是“不可能”。即便如此,让我们逐步了解一下这里发生的事情。

首先,closure不能引用localString,除非在doStuff()内的注释中以某种方式对其进行了重新分配。 closure的分配级别是localString不在范围内。闭包只能在分配变量时捕获范围内的变量,而不能在调用它们时捕获。但是让我们回到该问题的原始版本,然后再对其进行编辑。该版本确实具有您要描述的情况:

@objc final myClass : NSObject
{
    let classPropertyString = "A class property"


    func doStuff() 
    {
        let localString = "An object local to this function"

        DispatchQueue.global(qos: .userInitiated).async { [classPropertyString] in // (1)
             // Do things with 'classPropertyString' and 'localString'
        }
        // (2)
    }
}

这里没有问题。 classPropertyString被复制到闭包中,避免了任何保留循环。 localString被闭包引用,因此只要闭包存在,它就被保留。

因为您在捕获列表中列出了classPropertyString,所以它在点(1)处被求值并复制到闭包中。由于您隐式捕获了localString,因此将其视为参考。请参见《 Swift编程语言参考》中的Capture Lists,以获取一些在不同情况下如何正常工作的出色示例。<​​/ p>

在任何情况下(*)Swift都不会允许您在封闭中使用的内容的基础存储在背后消失。这就是为什么通常的关注点是过多的保留(内存泄漏)而不是晃动的引用(崩溃)。

(*)“在任何情况下”都是谎言。 Swift可以通过多种方式来允许它,但是几乎所有的方式都涉及“不安全”,这是您对此的警告。主要的例外是unowned,当然还有涉及!类型的任何事物。而且Swift通常不是线程安全的,因此您需要注意这一点...

关于线程安全的最后一句话是隐式捕获和显式捕获之间的微妙区别确实很重要的地方。考虑这种情况,您在两个队列上修改了一个隐式捕获的值:

func doStuff() -> String
{
    var localString = "An object local to this function"

    DispatchQueue.global(qos: .userInitiated).async {
         localString = "something else"
         callFunction(localString)
    }

    localString = "even more changes"
    return localString
}

在这种情况下会发生什么?真可悲,永远不要那样做。我相信这是未定义的行为,至少在大多数情况下,localString可能是任何东西,包括损坏的内存(它可能是调用.async的行为;我不确定)。但是不要这样做。

但是对于您的正常情况,没有理由显式捕获局部变量。 (我有时希望Swift用C ++的方式说必需的,但不是必需的。)

好吧,隐式和显式的另一种方式有所不同,这可能会促使人们理解其工作方式。考虑这样的有状态关闭(我经常建立这些关闭状态):

func incrementor() -> () -> Int {
    var n = 0
    return {
        n += 1
        return n
    }
}

let inc = incrementor()
inc()  // 1
inc()  // 2
inc()  // 3

let inc2 = incrementor()
inc2() // 1

查看闭包如何捕获局部变量n,并在超出范围后对其进行修改。并查看inc2如何拥有自己的本地变量版本。现在,尝试使用显式捕获。

func incrementor() -> () -> Int {
    var n = 0
    return { [n] in // <---- add [n]
        n += 1      // Left side of mutating operator isn't mutable: 'n' is an immutable capture
        return n
    }
}

显式捕获是副本,它们是不可变的。隐式捕获是引用,因此与引用的事物具有相同的可变性。