值类型引用循环?

15

在Swift中,当引用类型的属性彼此之间(或与闭包一起)拥有强烈的所有权时会发生引用循环。

然而,仅限于值类型是否存在引用循环的可能性?


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

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

我认为这与ARC无关;在这方面,结构体和类在概念上并没有区别。 - Raphael
6个回答

15
一个引用循环(或保留循环)之所以被称为这样,是因为它指示了循环对象图中:

retain cycle

每个箭头表示一个对象保留了另一个对象的强引用(请查看retaining)。除非打破这个循环,否则这些对象的内存将永远不会被释放。
当捕获和存储值类型(结构体和枚举)时,不存在引用这种东西。值是被复制而不是引用的,尽管值可以持有对对象的引用。
换句话说,值在对象图中可以具有出站箭头,但没有入站箭头。这意味着它们无法参与循环。

在这个上下文中,“closure + 8”和“8”分别代表什么? - mfaani

8
正如编译器所告诉您的那样,您试图做的是不合法的。正因为这是一个值类型,所以没有一种连贯有效的方法来实现您所描述的内容。如果一个类型需要引用自身(例如,它具有与其自身类型相同的属性),请使用类,而不是结构体。
或者,您可以使用枚举,但只能以特殊的、有限制的方式使用:枚举案例的关联值可以是该枚举的实例,前提是该案例(或整个枚举)被标记为“indirect”。
enum Node {
    case None(Int)
    indirect case left(Int, Node)
    indirect case right(Int, Node)
    indirect case both(Int, Node, Node)
}

1
你能详细说明一下为什么这是不可能的吗?(一个结构体拥有一个与自身类型相同的属性) - Alfie Hanssen
1
我可能漏掉了什么,或者jtbandes的答案没有提供我要找的信息。我知道什么是保留循环。我有点困惑的是:如果一个结构体(值类型)可以有一个 int 类型的属性(值类型),并且它可以有一个另一个结构体类型的属性(值类型),为什么它不能有一个 self 类型的属性(值类型)?这有意义吗? - Alfie Hanssen
5
这是一种思考方式。请记住这些是值类型:赋值会复制。现在想象一个结构体 struct Dog { var puppy : Dog? }。现在考虑以下代码:let d = Dog(); d.puppy = d。显然这是不连贯的:这就是巴伯悖论。因此,Swift直接阻止了整个情况。事实上,这背后还有更多的原因,与值类型在内存中的实际保存方式有关。但至少这给你一个动机。 - matt
@AlfieHanssen 在我的回答中可以找到稍微详细的解释。我认为保留循环和ARC在这里都不相关。 - Raphael
@Raphael FWIW,我同意;你会注意到我根本没有提到保留循环。这里没有任何引用计数涉及。 - matt
显示剩余3条评论

5

免责声明:我在这里对Swift编译器的内部工作进行了(希望是有根据的)猜测,因此请谨慎参考。

除了值语义之外,请问自己:我们为什么要使用结构体?有什么优点?

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

为了使编译器能够正常工作,它需要知道在确定堆栈或对象框架的结构时为给定的结构体值保留多少空间。只要结构体值是固定大小的树(不考虑对对象的外向引用;它们指向堆不是我们关心的),那就没问题:编译器可以将所有找到的大小相加。

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

请注意:相同的推理解释了为什么结构体是按值传递的:我们需要它们在新上下文中物理存在


建议观看2016年WWDC视频的416会话。 - matt
@matt 是因为我可能会觉得有趣,还是因为我写错了什么?前几分钟很有趣,肯定符合问题,但对我来说并没有新的东西。我应该继续看下去吗?为什么? - Raphael
你说过你必须“猜测Swift编译器的内部工作原理”。观看的原因是有关于结构体实际存储方式的大量细节。你可以在asciiwwdc上阅读它,而不是真正地观看。 - matt
@matt 谢谢你提供的参考,我找到时间后会去看一下! - Raphael

3

快速简便的解决方法:将其嵌入数组中。

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

2
通常情况下,值类型不会存在引用循环,因为Swift通常不允许对值类型进行引用。每个值都会被复制。
但是,如果你感兴趣的话,你实际上可以通过在闭包中捕获self来引发值类型引用循环。
以下是一个示例。请注意,MyObject类仅用于说明泄漏问题。
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

所以你可以在一个引用循环中捕获自身!如果我有一个包含委托的结构体,并且我想将其默认设置为自己作为该委托,我认为我不能将结构体协议属性声明为弱引用,对吧?那么如何解决这个循环问题呢? - blackjacx
我在 Swift 3 中检查了这个问题,你会得到编译器错误 Closure cannot implicitly capture a mutating self parameter。因此,编译器防止了这种保留循环! - blackjacx

1

然而,仅使用值类型是否存在引用循环的可能性?

这取决于您所谓的“仅使用值类型”是指什么。 如果您指的是完全没有包含隐藏引用在内的任何引用,那么答案就是否定的。 要创建一个引用循环,至少需要一个引用。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接