【问题标题】:Reference cycles with value types?带有值类型的引用循环?
【发布时间】:2017-09-23 03:57:36
【问题描述】:

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

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


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

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

【问题讨论】:

  • 我认为这与 ARC 无关;结构和类在这方面在概念上没有区别。

标签: swift automatic-ref-counting value-type reference-counting reference-type


【解决方案1】:

引用循环(或保留循环)之所以如此命名,是因为它在object graph 中表示cycle

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

在捕获和存储值类型(结构和枚举)时,没有引用这种东西。值被复制,而不是被引用,尽管值可以保存对对象的引用。

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

【讨论】:

  • closure + 8 & 8 在这种情况下意味着什么?
【解决方案2】:

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

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

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

【讨论】:

  • 您能否详细说明一下为什么这是不可能的(具有与 self 相同类型的属性的结构)?
  • @AlfieHanssen 阅读 jtbandes 的答案,它完全解释了。
  • 要么我遗漏了一些东西,要么 jtbandes 的答案没有提供我正在寻找的信息。我明白什么是保留周期。我有点困惑的是:如果一个struct(一个值类型)可以有一个int类型的属性(一个值类型),它可以有一个anotherStruct类型的属性(一个值类型),为什么不能呢?有 self 类型的属性(值类型)?这有意义吗?
  • @AlfieHanssen 这是一种思考方式。请记住,这些是值类型:赋值复制。现在想象一个结构struct Dog { var puppy : Dog? }。现在考虑这个代码:let d = Dog(); d.puppy = d。显然这是不连贯的:这是理发师悖论。因此,斯威夫特只是脚踏实地,禁止整个情况。 ——现在,实际上,不止于此;它与值类型在内存中的实际保存方式有关。但至少这会给你一个动力。
  • 这是一个很好的例子,很明显,因为涉及到 copy(),情况很快变得难以为继(无限对象图而不是保留循环?)。感谢您花时间解释这一点!
【解决方案3】:

免责声明:我在这里对 Swift 编译器的内部工作原理进行了(希望是受过教育的)猜测,所以请多加注意。

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

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

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

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

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

【讨论】:

  • 可能想观看 WWDC 2016 视频的 Session 416。
  • @matt 因为我可能觉得它很有趣,或者因为我写错了什么?前几分钟很有趣,绝对适合这个问题,但不要告诉我任何新内容。我应该看吗,为什么?
  • 您说您必须“猜测 Swift 编译器的内部工作原理”。观看的原因是关于结构的实际存储方式有很多细节。您可以在 asciiwwdc 上阅读而不是实际观看。
  • @matt 谢谢参考,有时间我去看看!
【解决方案4】:

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

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

【讨论】:

    【解决方案5】:

    您通常不能仅仅因为 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
    

    【讨论】:

    • 这样你就可以在一个引用循环中捕获 Self!如果我有一个具有委托的结构,并且我想将自己指定为默认的委托,我想我不能将结构协议属性声明为弱,对吧?那么如何绕过这个循环呢?
    • 我在 Swift 3 中检查了这个,你会得到编译器错误Closure cannot implicitly capture a mutating self parameter。所以编译器会阻止这种保留循环!
    【解决方案6】:

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

    取决于您对“仅值类型”的含义。 如果您的意思是完全没有参考,包括内部隐藏的参考,那么答案是否定的。 要创建一个引用循环,您至少需要一个引用。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-08-18
      • 1970-01-01
      • 2017-07-13
      • 1970-01-01
      • 2011-10-06
      • 1970-01-01
      • 2021-06-10
      相关资源
      最近更新 更多