通过Swift和inout参数中的闭包进行变量捕获

时间:2015-02-02 21:07:44

标签: cocoa swift closures

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

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

g()
capture

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

是什么需要inout,甚至捕获的变量已经可以修改而不受惩罚?? !!

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

4 个答案:

答案 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 ++中如果外部变量是用&捕获的