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

Ham*_*ish 9 swift

当我遇到捕获嵌套函数的这种奇怪行为时,我试图在自定义集合中实现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()在这种情况下肯定会发生这种情况吗?

Ham*_*ish 8

Swift 3.1更新

从Swift 3.1开始,可以使用Xcode 8.3 beta,这已得到修复.self在方法中根本不再装箱,因此按预期isKnownUniquelyReferenced(_:)返回true.


Pre Swift 3.1

我认为这是一个错误,并提交了一个错误报告(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(_:),假设调用在闭包创建之前.