我注意到当Swift中的闭包捕获变量时,闭包实际上可以修改该值。这对我来说似乎很疯狂,并且是获取可怕错误的绝佳方法,特别是当几个闭包捕获相同的var时。
var capture = "Hello captured"
func g(){
// this shouldn't be possible!
capture = capture + "!"
}
g()
capture
另一方面,有inout参数,允许函数或闭包修改其参数。
是什么需要inout,甚至捕获的变量已经可以修改而不受惩罚?? !!
只是想了解这背后的设计决策......
答案 0 :(得分:8)
捕获的外部作用域的变量不是例程的参数,因此它们的可变性是从上下文继承的。默认情况下,例程的实际参数是常量(let),因此不能在本地修改(并且不返回它们的值)
另请注意,您的示例并未真正捕获capture
,因为它是一个全局变量。
var global = "Global"
func function(nonmutable:Int, var mutable:Int, inout returnable:Int) -> Void {
// global can be modified here because it's a global (not captured!)
global = "Global 2"
// nomutable can't be modified
// nonmutable = 3
// mutable can be modified, but it's caller won't see the change
mutable = 4
// returnable can be modified, and it's caller sees the change
returnable = 5
}
var nonmutable = 1
var mutable = 2
var output = 3
function(nonmutable, mutable, &output)
println("nonmutable = \(nonmutable)")
println("mutable = \(mutable)")
println("output = \(output)")
另外,正如您所看到的,inout参数的传递方式不同,因此很明显返回时,值可能会有所不同。
答案 1 :(得分:4)
大卫的回答是完全正确的,但我想我会举例说明捕获实际上是如何运作的:
func captureMe() -> (String) -> () {
// v~~~ This will get 'captured' by the closure that is returned:
var capturedString = "captured"
return {
// The closure that is returned will print the old value,
// assign a new value to 'capturedString', and then
// print the new value as well:
println("Old value: \(capturedString)")
capturedString = $0
println("New value: \(capturedString)")
}
}
let test1 = captureMe() // Output: Old value: captured
println(test1("altered")) // New value: altered
// But each new time that 'captureMe()' is called, a new instance
// of 'capturedString' is created with the same initial value:
let test2 = captureMe() // Output: Old value: captured
println(test2("altered again...")) // New value: altered again...
// Old value will always start out as "captured" for every
// new function that captureMe() returns.
结果就是你不必担心关闭会改变捕获的值 - 是的,它可以改变它,但只针对返回的闭包的特定实例。全部返回闭包的其他实例将获得他们自己的,独立的捕获值副本,只有他们可以改变。
答案 2 :(得分:1)
以下是一些用于捕获变量超出其本地上下文的闭包的用例,这可能有助于了解此功能为何有用:
假设您要从数组中过滤重复项。有一个filter
函数,它接受一个过滤谓词并返回一个只有与该谓词匹配的条目的新数组。但是如何通过已经看到条目的状态并因此重复?你需要谓词来保持调用之间的状态 - 你可以通过让谓词捕获一个保存该状态的变量来实现这一点:
func removeDupes<T: Hashable>(source: [T]) -> [T] {
// “seen” is a dictionary used to track duplicates
var seen: [T:Bool] = [:]
return source.filter { // brace marks the start of a closure expression
// the closure captures the dictionary and updates it
seen.updateValue(true, forKey: $0) == nil
}
}
// prints [1,2,3,4]
removeDupes([1,2,3,1,1,2,4])
确实,您可以使用也带有inout参数的过滤器函数来复制此功能 - 但是很难编写一些如此通用但灵活的闭包可能性。 (你可以使用reduce
代替filter
进行此类过滤,因为reduce会将调用状态传递给调用 - 但filter
版本可能更清晰了)
标准库中有一个GeneratorOf
结构,可以很容易地编写各种序列生成器。用闭包初始化它,该闭包可以捕获用于生成器状态的变量。
假设您想要一个生成器,该生成器提供从0到n范围内的m个数字的随机递增序列。以下是使用GeneratorOf
:
import Darwin
func randomGeneratorOf(#n: Int, #from: Int) -> GeneratorOf<Int> {
// state variable to capture in the closure
var select = UInt32(n)
var remaining = UInt32(from)
var i = 0
return GeneratorOf {
while i < from {
if arc4random_uniform(remaining) < select {
--select
--remaining
return i++
}
else {
--remaining
++i
}
}
// returning nil marks the end of the sequence
return nil
}
}
var g = randomGeneratorOf(n: 5, from: 20)
// prints 5 random numbers in 0..<20
println(",".join(map(g,toString)))
同样,没有闭包可以做这种事情 - 在没有闭包的语言中,你可能有一个生成器协议/接口,并创建一个持有状态的对象,并有一个提供值的方法。但是闭合表达式允许以最小的锅炉板灵活地实现这一点。
答案 3 :(得分:1)
能够修改外部作用域中捕获的变量的闭包在各种语言中非常常见。这是C#,JavaScript,Perl,PHP,Ruby,Common Lisp,Scheme,Smalltalk等许多默认行为。这也是Objective-C中的行为,如果外部变量是__block
,则在Python 3中,如果外部变量是nonlocal
,则在C ++中如果外部变量是用&
捕获的} p>