Swift 和 inout 参数中的闭包捕获变量

Variable capture by closures in Swift and inout parameters

我注意到当一个变量被Swift中的闭包捕获时,闭包实际上可以修改值。这对我来说似乎很疯狂,而且是获得可怕错误的绝佳方法,特别是当同一个 var 被多个闭包捕获时。

var capture = "Hello captured"
func g(){
    // this shouldn't be possible!
    capture = capture + "!"
}

g()
capture

另一方面,有 inout 参数,它允许函数或闭包修改其参数。

inout有什么用,捕获的变量已经可以修改了??!!

只是想了解这背后的设计决策...

捕获的来自外部作用域的变量不是例程的参数,因此它们的可变性是从上下文继承的。默认情况下,例程的实际参数是常量 (let),因此不能在本地修改(并且它们的值不是 returned)

另请注意,您的示例并未真正捕获 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 参数的传递方式不同,因此很明显 return 上的值可能不同。

David 的回答是完全正确的,但我想我会举例说明捕获的实际工作原理:

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 = [=10=]
        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. 

这样做的结果是您不必担心闭包会改变捕获的值 - 是的,它可以改变它,但仅针对返回的闭包的特定实例。 返回的闭包的所有其他实例将获得它们自己的、独立的捕获值副本,并且只有它们可以更改。

以下是闭包捕获局部上下文之外的变量的几个用例,可能有助于了解此功能为何有用:

假设您要从数组中过滤出重复项。有一个 filter 函数接受一个过滤谓词和 returns 一个仅包含与该谓词匹配的条目的新数组。但是如何传递已经看到条目并因此重复的状态呢?您需要谓词在调用之间保持状态——您可以通过让谓词捕获一个保存该状态的变量来做到这一点:

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: [=10=]) == 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)))

同样,可以在没有闭包的情况下做这种事情——在没有闭包的语言中,您可能有一个生成器 protocol/interface 并创建一个保存状态的对象并有一个提供值的方法.但是闭包表达式提供了一种灵活的方式来用最少的样板来做到这一点。

能够在外部范围内修改捕获变量的闭包在各种语言中非常普遍。这是 C#、JavaScript、Perl、PHP、Ruby、Common Lisp、Scheme、Smalltalk 和许多其他语言中的默认行为。如果外部变量是 __block,这也是 Objective-C 中的行为,如果外部变量是 nonlocal,在 Python 3 中,如果外部变量是用 [ 捕获的,在 C++ 中也是如此=12=]