为什么嵌套的自捕获函数会干扰isKnownUniquelyReferenced(_ :)?

时间:2017-01-03 17:31:17

标签: swift

当我使用捕获mv的嵌套函数遇到这种奇怪的行为时,我试图在自定义集合中实现copy-on-write行为。

在下面的代码中,对isKnownUniquelyReferenced(_:)的调用将始终返回false,尽管在之前调用了,甚至定义了嵌套函数:

self

为什么会出现这种情况,我该怎么做才能解决它(假设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 ?! 实际上对nestedFunc做了一些有用的事情?)

我知道捕获self可能会干扰对self的调用 - 但在这种情况下调用isKnownUniquelyReferenced(_:)时肯定会发生这种情况吗?

1 个答案:

答案 0 :(得分:8)

Swift 3.1更新

自Xcode 8.3 beta版提供的Swift 3.1以来,已经修复了这个问题。该方法中的self已不再装箱,因此isKnownUniquelyReferenced(_:)会按预期返回true

Pre Swift 3.1

我认为这是一个错误,并提交了错误报告(SR-3530)。然而,我对这个问题的原因感兴趣,所以进行了一些挖掘 - 这就是我找到的。

看一下为bar()方法生成的规范SIL(对于-Onone构建),可以看出Swift是为<{1}}的堆分配一个框(alloc_box)的< em>方法的开头 - 以便self可以捕获它。

nestedFunc()

Full SIL here)功能

由于这个装箱,// 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 } 方法中的Bar实例现在有两个副本,因此意味着bar()将返回false,如有两个对isKnownUniquelyReferenced(_:)实例的引用。

据我所知,方法开头的Foo拳击似乎是从拷入复制({{1}优化self方法的结果在方法调用的开始处获取框,然后将突变应用于该框,然后在方法结束时写回被调用方)以通过引用传递(此优化发生在原始SIL和规范SIL之间)。

为了在方法中创建mutating副本而使用的相同框是现在用于捕获self以调用嵌套函数用。我没有理由为什么不应该在调用self之前创建捕获框,因为这是捕获self的逻辑位置(而不是在方法的开头)。 / p>

虽然,无论如何,首先创建一个盒子是完全多余的,因为nestedFunc()没有,而不能逃脱。尝试从方法返回self会产生以下编译器错误:

  

嵌套函数无法捕获inout参数并转义

所以它看起来真的只是一个尚未优化的角落案例。即使在-O构建中,尽管nestedFunc()实例的堆分配能够针对仅nestedFunc()属性的堆栈分配进行优化,但这仍然会导致对{{{ 1}}实例。

解决方案

一种解决方案是将Bar foo参数添加到Foo,允许inout仅通过引用传递,而不是被捕获:

self

现在生成SIL(-Onone):

nestedFunc()

此解决方案的优点在于它只是一个简单的引用传递(因为self参数标记为@inout)。因此,只存在func nestedFunc(_ `self`: inout Bar) { _ = self // do something useful with self } // ... nestedFunc(&self) 实例的一个副本 - 因此// 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 可以返回true。

如果Bar未在Bar内变异,则另一种可能的解决方案是将isKnownUniquelyReferenced(_:)传递给,而不是引用。这可以通过本地闭包中的捕获列表来完成:

self

优点是您无需在nestedFunc()调用中明确传递任何内容。因为self的实例在创建闭包之前没有按值传递 - 它不会干扰对let nestedFunc = { [`self` = self] in // copy self into the closure. _ = self // the self inside the closure is immutable. } // ... nestedFunc() 的调用,假设调用在闭包创建之前。