当我遇到捕获嵌套函数的这种奇怪行为时,我试图在自定义集合中实现copy-on-write行为self.
在下面的代码中,isKnownUniquelyReferenced(_:)尽管在嵌套函数被定义之前调用,但调用始终会返回false :
class Foo {}
struct Bar {
var foo = Foo()
public mutating func bar() {
print(isKnownUniquelyReferenced(&foo))
func nestedFunc() {
_ = self // capture self
}
nestedFunc()
}
}
var b = Bar()
b.bar() // false ?!
Run Code Online (Sandbox Code Playgroud)
为什么这是这种情况,我该怎么做才能解决它(假设nestedFunc实际上做了一些有用的事情self)?
我知道捕获self可能会干扰呼叫isKnownUniquelyReferenced(_:)- 但nestedFunc()在这种情况下肯定会发生这种情况吗?
从Swift 3.1开始,可以使用Xcode 8.3 beta,这已得到修复.self在方法中根本不再装箱,因此按预期isKnownUniquelyReferenced(_:)返回true.
我认为这是一个错误,并提交了一个错误报告(SR-3530).然而,我对这个问题的原因感兴趣,所以做了一些挖掘 - 这就是我发现的.
看一下为该bar()方法生成的规范SIL (对于-Onone构建),可以看出Swift是在方法的最开始时为堆分配一个box(alloc_box)- 以便可以捕获它.selfnestedFunc()
// Bar.bar() -> ()
sil hidden @main.Bar.bar () -> () : $@convention(method) (@inout Bar) -> () {
// %0 // users: %10, %3
bb0(%0 : $*Bar):
// create new heap-allocated box, and store self in it.
// this is where the problem stems from – there are now two copies of the Bar instance, thus isKnownUniquelyReferenced will return false.
%1 = alloc_box $Bar, var, name "self", argno 1, loc "main.swift":15:26, scope 9 // users: %11, %9, %7, %2
%2 = project_box %1 : $@box Bar, loc "main.swift":15:26, scope 9 // users: %10, %5, %3
copy_addr %0 to [initialization] %2 : $*Bar, scope 9 // id: %3
// call isKnownUniquelyReferenced (I removed the print() function call as it generates a bunch of unrelated SIL).
// function_ref isKnownUniquelyReferenced<A where ...> (inout A) -> Bool
%4 = function_ref @Swift.isKnownUniquelyReferenced <A where A: Swift.AnyObject> (inout A) -> Swift.Bool : $@convention(thin) <?_0_0 where ?_0_0 : AnyObject> (@inout ?_0_0) -> Bool, loc "main.swift":17:9, scope 10 // user: %6
%5 = struct_element_addr %2 : $*Bar, #Bar.foo, loc "main.swift":17:35, scope 10 // user: %6
%6 = apply %4<Foo>(%5) : $@convention(thin) <?_0_0 where ?_0_0 : AnyObject> (@inout ?_0_0) -> Bool, loc "main.swift":17:39, scope 10
// retain the heap-allocated box containing self, in preparation for applying nestedFunc() with it.
// (as it's passed as an @owned parameter).
strong_retain %1 : $@box Bar, loc "main.swift":27:9, scope 10 // id: %7
// call the nested function with the box as the argument.
// function_ref Bar.(bar() -> ()).(nestedFunc #1)() -> ()
%8 = function_ref @main.Bar.(bar () -> ()).(nestedFunc #1) () -> () : $@convention(thin) (@owned @box Bar) -> (), loc "main.swift":27:9, scope 10 // user: %9
%9 = apply %8(%1) : $@convention(thin) (@owned @box Bar) -> (), loc "main.swift":27:20, scope 10
// once called, copy the contents of the box back to the address of the Bar instance that was passed into the method, and release the box.
copy_addr %2 to %0 : $*Bar, scope 10 // id: %10
strong_release %1 : $@box Bar, loc "main.swift":29:5, scope 10 // id: %11
// so cute.
%12 = tuple (), loc "main.swift":29:5, scope 10 // user: %13
return %12 : $(), loc "main.swift":29:5, scope 10 // id: %13
}
Run Code Online (Sandbox Code Playgroud)
(这里全SIL)
由于这个装箱,现在方法中有两个Bar实例副本bar(),因此意味着isKnownUniquelyReferenced(_:)将返回false,因为有两个对该Foo实例的引用.
从我所知,self方法开头的拳击似乎是mutating从拷入复制输出中优化方法的结果(self在方法调用开始时获取框,然后将突变应用于框,然后在方法结束时写回被调用者)以通过引用传递(此优化发生在原始SIL和规范SIL之间).
现在使用与在self方法中创建变异副本相同的框来捕获以调用嵌套函数.我没有理由为什么不应该在调用之前创建捕获框,因为这是捕获的逻辑位置(而不是在方法的开头).selfnestedFunc()self
虽然,无论如何,首先创建一个盒子是完全多余的,但是nestedFunc()并没有也无法逃脱.尝试nestedFunc()从该方法返回会产生以下编译器错误:
嵌套函数无法捕获inout参数和转义
所以它看起来真的只是一个尚未优化的角落案例.即使在-O构建中,尽管Bar实例的堆分配能够仅针对foo属性优化为堆栈分配,但这仍然导致对该Foo实例的不必要的第二引用.
一种解决方案是只添加一个inout self参数nestedFunc(),允许self仅通过引用传递,而不是被捕获:
func nestedFunc(_ `self`: inout Bar) {
_ = self // do something useful with self
}
// ...
nestedFunc(&self)
Run Code Online (Sandbox Code Playgroud)
现在生成SIL(-Onone):
// function_ref Bar.(bar() -> ()).(nestedFunc #1)(inout Bar) -> ()
%5 = function_ref @main.Bar.(bar () -> ()).(nestedFunc #1) (inout main.Bar) -> () : $@convention(thin) (@inout Bar) -> (), loc "main.swift":31:9, scope 10 // user: %6
%6 = apply %5(%0) : $@convention(thin) (@inout Bar) -> (), loc "main.swift":31:25, scope 10
Run Code Online (Sandbox Code Playgroud)
这个解决方案的优点是它只是一个简单的参考传递(Bar参数被标记@inout).因此,只存在一个实例的副本Bar- 因此isKnownUniquelyReferenced(_:)可以返回true.
另一种可能的解决方案,如果self不在其中进行变异nestedFunc(),则是self通过值而不是引用.这可以通过本地闭包中的捕获列表来完成:
let nestedFunc = { [`self` = self] in // copy self into the closure.
_ = self // the self inside the closure is immutable.
}
// ...
nestedFunc()
Run Code Online (Sandbox Code Playgroud)
优点是您不需要明确地将任何内容传递给nestedFunc()调用.因为在Bar闭包创建之前,实例不会通过值传递 - 它不会干扰调用isKnownUniquelyReferenced(_:),假设调用在闭包创建之前.
| 归档时间: |
|
| 查看次数: |
351 次 |
| 最近记录: |