【问题标题】:Swift Closures: Must Capture Lists Be Exhaustive?Swift 闭包:捕获列表必须详尽吗?
【发布时间】:2019-02-20 18:43:37
【问题描述】:

假设我有一个这样的 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(就像我在这里所做的那样),闭包是否仍会自动捕获对它的强引用,还是我的处境很糟糕?

【问题讨论】:

  • 虽然 Apple 没有在他们的文档 (docs.swift.org/swift-book/LanguageGuide/…) 中讨论这种“定义应该在捕获列表中强烈捕获哪些项目”的方法,但我已经在不同的地方看到了它,我想知道语义。因此这个问题。我是 RTFM。
  • 与您的问题无关,但 Swift 命名约定以大写字母开头的所有类命名。

标签: swift memory memory-management closures


【解决方案1】:

您的问题有几个小问题,很难清楚地回答,但我想我理解潜在的担忧,简短的回答是“不”。但是你的例子是不可能的,所以答案是“不可能的”。如果可能的话,就没有强有力的参考(也不需要一个),所以问题仍然是“不可能的”。尽管如此,让我们来看看这里发生了什么。

首先,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 Programming Language Reference 中的 Capture Lists,了解一些出色的示例,了解它在不同情况下是如何工作的。

在任何情况下,(*) 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
    }
}

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

【讨论】:

  • Ummm... 问题:在第一个示例中,在捕获列表中指定 [classPropertyString] 实际上会复制值,因为它是实例化闭包的时间点。但是,据我了解,localString 不会被复制,而是通过引用语义绑定到闭包,因为它没有被指定为显式捕获。这与它是一个值类型(参见第一个 n 示例)以及它是一个 let 常量这一事实无关。
  • 感谢真正彻底的演练;那太棒了。尽管我理解为什么使用 GCD 不会创建保留循环,但我已经回滚到原始示例。正如你所知道的,我是一名长期的 C/ObjC 开发人员,正在转向 Swift,所以我仍然对更好的观点有所了解。
  • @Hamish 感谢 cmets。我已经编辑过(大部分只是删除了不准确的句子。)
  • 感谢@MichaelLong。已编辑。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-03-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-07-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多