值类型的引用循环?

Reference cycles with value types?

Swift 中的引用循环发生在引用类型的属性彼此具有强所有权(或具有闭包)时。

但是,是否有可能只有 具有值类型的引用循环?


我在 playground 上试过这个但没有成功(错误:不允许递归值类型 'A')。

struct A {
  var otherA: A? = nil
  init() {
    otherA = A()
  }
}

正如编译器告诉您的那样,您的尝试是非法的。正是因为此 值类型,所以没有连贯、有效的方法来实现您所描述的内容。如果一个类型需要引用自身(例如,它有一个与自身类型相同的 属性),请使用 class,而不是结构。

或者,您可以使用枚举,但只能以一种特殊的、有限的方式使用:枚举案例的关联值可以是该枚举的实例,前提是案例(或整个枚举)被标记为 indirect:

enum Node {
    case None(Int)
    indirect case left(Int, Node)
    indirect case right(Int, Node)
    indirect case both(Int, Node, Node)
}

A reference cycle(或retain cycle)之所以如此命名是因为它表示一个cycle in the object graph:

每个箭头表示一个对象 retaining 另一个(强引用)。除非循环被打破,否则这些对象的内存永远不会被释放。

捕获和存储值类型(结构和枚举)时,不存在引用。值是 复制的 ,而不是引用的,尽管值可以包含对对象的引用。

换句话说,值在对象图中可以有传出箭头,但没有传入箭头。这意味着他们不能参与循环。

但是,是否有可能仅具有值类型的引用循环?

取决于您对 "value types only" 的理解。 如果你的意思是完全没有参考,包括里面隐藏的参考,那么答案是否定的。 要制作参考循环,您至少需要一个参考。

但在Swift中,Array、String或其他一些类型是值类型,它们的实例中可能包含引用。如果您的 "value types" 包含此类类型,则答案是肯定的。

您通常不能使用值类型进行循环引用,因为 Swift 通常不允许对值类型的引用。一切都被复制了。

但是,如果您很好奇,实际上可以通过在闭包中捕获 self 来引发值类型引用循环。

下面是一个例子。请注意 MyObject class 的存在只是为了说明泄漏。

class MyObject {
    static var objCount = 0
    init() {
        MyObject.objCount += 1
        print("Alloc \(MyObject.objCount)")
    }

    deinit {
        print("Dealloc \(MyObject.objCount)")
        MyObject.objCount -= 1
    }
}

struct MyValueType {
    var closure: (() -> ())?
    var obj = MyObject()

    init(leakMe: Bool) {
        if leakMe {
            closure = { print("\(self)") }
        }
    }
}

func test(leakMe leakMe: Bool) {
    print("Creating value type.  Leak:\(leakMe)")
    let _ = MyValueType(leakMe: leakMe)
}

test(leakMe: true)
test(leakMe: false)

输出:

Creating value type.  Leak:true
Alloc 1
Creating value type.  Leak:false
Alloc 2
Dealloc 2

免责声明:我正在对Swift编译器的内部工作原理进行(希望受过教育的)猜测,所以请撒些盐。

除了值语义之外,问问自己:为什么我们有结构?有什么优势?

一个优点是我们可以(阅读:想要)将它们存储在堆栈上(分别在object帧中),即就像原始值一样在其他语言中。特别是,我们不想在堆上分配专用 space 指向。这使得访问结构值更加高效:我们(阅读:编译器)总是知道它在内存中找到值的确切位置,相对于当前帧或 object 指针。

为了让编译器解决这个问题,它需要知道在确定结构的结构时要为给定的结构值保留多少space堆栈或 object 帧。只要结构值是固定大小的树(忽略对 objects 的传出引用;它们指向我们不感兴趣的堆),那没问题:编译器可以将它找到的所有大小相加.

如果你有一个递归结构,这会失败:你可以用这种方式实现列表或二叉树。编译器无法静态地找出如何将这些值存储在内存中,因此我们必须禁止它们。

注意事项: 同样的推理解释了为什么结构是 pass-by-value:我们 需要 它们 身体 在他们的新环境中。

快速简便的黑客解决方法:只需将其嵌入数组即可。

struct A {
  var otherA: [A]? = nil
  init() {
    otherA = [A()]
  }
}