关于函数作为参数传递与逃逸自动闭包的关联及代码报错的疑问
嘿,这个问题我当初刚接触Swift值类型的时候也困惑过,咱们一点点拆解清楚~
首先先把你的代码贴出来方便参考:
struct MyStruct { var a = 0 func foo() { print("Ok") } mutating func increase() { a += 1 } } func runner(_ function: () -> Void) { function() } var myStruct = MyStruct() runner(myStruct.foo) // Ok runner(myStruct.increase) // Escaping autoclosure captures 'inout' parameter 'self'
第一个问题:自动闭包(autoclosure)在哪?
你看不到它,但它是Swift编译器偷偷帮你生成的!
原因是:increase()是一个mutating方法,对于值类型(比如这里的struct)来说,mutating方法本质上需要获取self的inout引用——简单说就是要直接修改原实例的内存。但runner函数接受的参数是() -> Void类型,也就是没有参数、不返回值的函数,没法直接接收需要inout self的方法引用。
这时候Swift就自动做了个转换:它生成了一个自动闭包,这个闭包的逻辑大概是这样的(伪代码):
{ &myStruct.increase() }
这个闭包会捕获myStruct的inout引用,然后在内部调用increase()。这个闭包是隐式创建的,所以你代码里看不到,但编译器能识别它的存在。
第二个问题:为什么它是逃逸(escaping)的?
这和Swift对inout参数的生命周期限制有关。
首先,runner的参数function默认是**非逃逸(non-escaping)**的——也就是这个闭包只能在runner函数内部执行,不能被存储或者传到函数外部。但这里编译器生成的自动闭包有个特殊点:它捕获了inout类型的self。
Swift有个严格的规则:捕获inout参数的闭包绝对不能是逃逸的。因为inout参数的生命周期被严格限制在当前函数调用内,一旦函数执行完毕,inout引用就失效了。如果闭包逃逸了(比如被存到全局变量里,过一会再执行),那时候再去访问这个inout引用就会导致内存错误。
那为什么这个自动闭包会被标记为逃逸呢?因为Swift在生成这个闭包的时候,没法确定它会不会被逃逸——哪怕runner的参数是非逃逸的,编译器为了安全起见,会默认把这种捕获inout的自动闭包当作可能逃逸的来处理,于是就抛出了这个错误。
怎么解决这个问题?
其实很简单,手动把mutating方法的调用包装成一个普通闭包就行:
runner { myStruct.increase() }
这时候你手动写的闭包不会捕获inout引用,而是会先复制一份myStruct,修改副本后再把副本赋值回原变量——这完全符合值类型的语义,也就不会触发报错啦。
备注:内容来源于stack exchange,提问作者Roman

