当我使用捕获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(_:)
时肯定会发生这种情况吗?
答案 0 :(得分:8)
自Xcode 8.3 beta版提供的Swift 3.1以来,已经修复了这个问题。该方法中的self
已不再装箱,因此isKnownUniquelyReferenced(_:)
会按预期返回true
。
我认为这是一个错误,并提交了错误报告(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()
的调用,假设调用在闭包创建之前。