【问题标题】:Swift: Does closure have references to constants or variables?Swift:闭包是否引用了常量或变量?
【发布时间】:2017-06-20 00:50:37
【问题描述】:

我知道有几个相关的问题,而且我可以在互联网上找到很多帖子。 但是,我无法理解闭包可以保存引用的事实。在引用类型的情况下,这是完全正常且非常合理的,但是值类型呢,包括structenum? 请参阅此代码。

let counter: () -> Int
var count = 0
do  {
    counter = {
        count += 1
        return count
    }
}
count += 1 // 1
counter() // 2
counter() // 3

我们可以通过两种方式访问​​值类型count。一种是直接使用count,另一种是通过闭包counter。 但是,如果我们写

let a = 0
let b = a

,在内存中b 当然与a 有不同的区域,因为它们是值类型。这种行为是值类型的一个显着特征,与引用类型不同。 然后回到闭包主题,闭包引用了值类型的变量或常量。

那么,我可以说在闭包捕获值的情况下,我们不能对值类型有任何引用的值类型的特性被改变了吗? 对我来说,捕获对值类型的引用非常令人惊讶,同时我上面展示的经验也表明了这一点。

你能解释一下这件事吗?

【问题讨论】:

  • 我不确定您想要了解多少技术细节,但stackoverflow.com/a/40979548/2976878stackoverflow.com/q/43171341/2976878 可能有用。基本上,捕获的值被放入堆上的引用计数框,然后存储在函数值的上下文对象中。在捕获持有引用类型的变量时,引用本身被放入堆分配的盒子中(考虑到引用 它们自己 是值类型),所以你实际上是对引用的引用.
  • 我阅读了您的帖子,非常令人兴奋。我理解了内容。但是,什么是引用计数框?我读了你多次显示的答案,但我没有遇到这个词。你能解释一下吗?
  • 很高兴您发现它很有用!盒子只是给定值的包装器,因此通过闭包捕获,捕获的变量的值被“装箱”在堆中的结构中,在我的回答中具有专门的Box<T> 结构的布局。然后(大部分)将来对该变量的任何访问(在它定义的范围内以及闭包中)都被编译为访问堆上的装箱值。
  • 虽然这么说,但严格来说,在 your 示例中,count 是一个 global 变量,因此具有静态生命周期,这意味着闭包技术上不会捕获它——它只是正常访问它(如果你有兴趣,我会稍微介绍一下全局和静态访问器here的实现)。
  • 但我离题了。在这种情况下,引用计数只是跟踪堆上内存块的生命周期的一种方式,顾名思义,它是通过对对该内存的引用数进行计数来完成的,当引用 == 0。Swift 使用 Automatic Reference Counting 来执行此操作。

标签: swift closures


【解决方案1】:

我认为混淆是因为对值类型和引用类型考虑得太多了。这与此关系不大。让我们将数字设为引用类型:

class RefInt: CustomStringConvertible {
    let value: Int
    init(value: Int) { self.value = value }
    var description: String { return "\(value)" }
}

let counter: () -> RefInt
var count = RefInt(value: 0)
do  {
    counter = {
        count = RefInt(value: count.value + 1)
        return count
    }
}
count = RefInt(value: count.value + 1) // 1
counter() // 2
counter() // 3

这感觉有什么不同吗?我希望不是。都是一样的,只是参考。这不是价值/参考。

关键是,正如您所注意到的,闭包捕获了变量。不是变量的值,也不是变量指向的引用的值,而是变量本身)。因此,在捕获该变量的所有其他地方(包括调用者)都可以看到对闭包内变量的更改。 Capturing Values 对此进行了更全面的讨论。

如果你有兴趣,可以再深入一点(现在我正在讨论一些可能超出你现在关心的技术细节):

闭包实际上有一个对变量的引用,并且它们所做的更改会立即发生,包括调用didSet等。这与inout参数不同,后者仅在返回时才将值分配给其原始上下文.你可以这样看:

let counter: () -> Int
var count = 0 {
    didSet { print("set count") }
}

do  {
    counter = {
        count += 1
        print("incremented count")
        return count
    }
}

func increaseCount(count: inout Int) {
    count += 1
    print("increased Count")
}

print("1")
count += 1 // 1
print("2")
counter() // 2
print("3")
counter() // 3

increaseCount(count: &count)

打印出来:

1
set count
2
set count
incremented count
3
set count
incremented count
increased Count
set count

请注意“设置计数”总是在“增加计数”之前,但在“增加计数”之后。这让我们明白,闭包实际上指的是它们捕获的同一个变量(不是值或引用;变量),以及为什么我们称它为闭包的“捕获”,而不是“传递”给函数。 (当然,您也可以“传递”给闭包,在这种情况下,它们的行为与这些参数上的函数完全一样。)

【讨论】:

  • 感谢您的出色回答!但是,我仍然不清楚。 “闭包捕获变量”是什么意思。我无法想象这很好。如您所知,class 实例的变量包含实例的内存地址,并且位于堆栈区域中。另一方面,值类型对应的变量就是值本身,也是在栈区。当闭包捕获某些东西时,闭包有什么?你解释了变量本身,但是如果你给我更详细的解释,闭包对它们有多大影响?
  • 查看 Hamish 在上面评论中的链接以讨论实现。 (捕获的变量放在堆上的一个引用框中。)但重要的区别是“对应于值类型的变量”不是“值本身”。它是一个 引用 值的名称,并且可以更改它所引用的值。这个非常重要。如果它是“价值本身”,那么var 将毫无意义。 (let 可以这样认为,只是一个“绑定”而不是一个引用,当一切都是 let 时,这些问题中的大多数都消失了。)
  • 变量 (var) 是对值的引用,而不是值本身。 “参考”这个词被用于许多事情,这可能会令人困惑。例如,仅仅因为某物是“值类型”并不意味着它具有“值语义”,反之亦然。例如,NSDate 是引用类型,但具有值语义。使用引用语义构建结构(值类型)非常容易。这些东西是在栈上还是堆上完全是一个实现细节(Swift 在栈和堆之间移动了一些东西)。某些类型不在堆栈堆(标记指针)上。
  • 一个非常有用的主题演讲:news.realm.io/news/swift-gallagher-value-semantics
猜你喜欢
  • 2019-03-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-17
  • 1970-01-01
  • 2017-05-04
  • 2021-05-06
相关资源
最近更新 更多