在Swift中混淆强引用循环的例子

时间:2015-12-18 09:37:54

标签: ios objective-c swift

这是Apple's document的一个例子:

@place.all_versions.order('created_at DESC')

我理解为什么这个闭包属性会导致强引用周期,我知道如何解决它。我不打算这么说。

让我感到困惑的是以下代码:

class HTMLElement {

    let name: String
    let text: String?

    lazy var asHTML: Void -> String = {
        if let text = self.text {
            return "<\(self.name)>\(text)</\(self.name)>"
        } else {
            return "<\(self.name) />"
        }
    }

    init(name: String, text: String? = nil) {
        self.name = name
        self.text = text
    }

    deinit {
        print("\(name) is being deinitialized")
    }

}

据我所知Swift documentation以及我自己对Objective-c的经验,闭包将捕获变量var heading: HTMLElement? = HTMLElement(name: "h1") let defaultText = "some default text" heading!.asHTML = { // confusing, this closure are supposed to retain heading here, but it does not return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>" } print(heading!.asHTML()) heading = nil // we can see the deinialization message here, // it turns out that there is not any strong reference cycle in this snippet. ,因此应该引起一个强大的参考周期。但事实并非如此,这让我很困惑。

我还写了一个这个例子的Objective-c对应物,它确实引起了我预期的强引用周期。

heading

typedef NSString* (^TagMaker)(void);

@interface HTMLElement : NSObject

@property (nonatomic, strong) NSString      *name;
@property (nonatomic, strong) NSString      *text;

@property (nonatomic, strong) TagMaker      asHTML;

@end

@implementation HTMLElement

- (void)dealloc {
    NSLog(@"%@", [NSString stringWithFormat:@"%@ is being deinitialized", self.name]);
}

@end

任何提示或指南都将不胜感激。

5 个答案:

答案 0 :(得分:5)

因为,在后一种情况下

Swift 关闭保留heading的强引用,而不是指向

heading实例

在图片中,它显示如下

如果我们按设置heading = nil打破红线,那么参考圆就会被打破。

更新开始:

但是,如果你没有将标题设置为nil,那么仍然有一个参考圆圈,就像我在上面发布的图像一样。你可以像这样测试它

func testCircle(){
    var heading: HTMLElement? = HTMLElement(name: "h1")
    let defaultText = "some default text"
    heading.asHTML = {
        return "<\(heading.name)>\(heading.text ?? defaultText)</\(heading.name)>"
    }
    print(heading.asHTML())
}
testCircle()//No dealloc message is printed

更新结束

我还编写了下面的测试代码来证明闭包没有对内存中实例的强引用

var heading: HTMLElement? = HTMLElement(name: "h1")
var heading2 = heading
let defaultText = "some default text"
heading!.asHTML = {
// confusing, this closure are supposed to retain heading here, but it does not
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
let cloureBackup = heading!.asHTML
print(heading!.asHTML())

heading = HTMLElement(name: "h2")

print(cloureBackup())//<h2>some default text</h2>

因此,测试代码的图像是 enter image description here

你会看到来自游乐场的日志

<h1>some default text</h1>
<h2>some default text</h2>

从我的测试和理解中找不到任何关于此的文档,希望它会有所帮助

答案 1 :(得分:3)

我认为devi是详细的。 documentation州:

  

...可能会发生捕获,因为闭包的主体访问实例的属性,例如self.someProperty,或者因为闭包调用实例上的方法,例如self.someMethod()

请注意 self ,我相信这是问题的关键。

另一篇documentation建议:

  

闭包可以从定义它的周围上下文中捕获常量和变量。然后闭包可以引用并修改其体内的常量和变量的值,即使定义常量和变量的原始范围不再存在。

因此,换句话说,它是被捕获的常量和变量,而不是对象本身。它只是self是一种特殊情况,因为当{<1}}用于从对象中初始化的闭包中时,那么契约是这样的self是当触发闭包时,总是。换句话说,在任何情况下都不会发生其身体中self的闭包确实执行但此self指向的对象消失的情况。考虑一下:这种封闭可以在其他地方进行,例如:分配给另一个对象的另一个属性,因此即使被捕获对象的原始所有者“忘记”它,它也必须能够运行。要求开发人员检查self是否self是对的,对吗?因此需要保持强有力的参考。

现在,如果你转向另一个案例,其中闭包没有使用nil,但有些(明确解开)可选,那么它完全是另一个球类游戏。这样的可选可以self,开发人员必须接受这个事实,并负责处理。当这样的闭包运行时,可能就是它正在使用的可选属性实际上从不甚至被分配了具体的值!那么,持有强有力的参考是什么意思?

举例说明。这是基础课:

nil

这是强参考周期的镜像:

class Foo {
    let name: String

    lazy var test: Void -> Void = {
        print("Running closure from \(self.name)")
    }

    init(name: String) {
        self.name = name
    }
}

现在,在对象外部使用可选属性的下一个案例:

var closure: Void -> Void

var captureSelf: Foo? = Foo(name: "captureSelf")
closure = captureSelf!.test
closure()                       // Prints "Running closure from captureSelf"
captureSelf = nil
closure()                       // Still prints "Running closure from captureSelf"

..即我们仍然“记住”闭包,但闭包应该能够处理它所使用的属性实际为var tryToCaptureOptional: Foo? = Foo(name: "captureSomeOptional") tryToCaptureOptional?.test = { print("Running closure from \(tryToCaptureOptional?.name)") } closure = tryToCaptureOptional!.test closure() // Prints "Running closure from Optional("captureSomeOptional")" tryToCaptureOptional = nil closure() // Prints "Running closure from nil" 的情况。

但“乐趣”现在才开始。例如,我们可以这样做:

nil

..换句话说,即使对象真的“消失”,但只是某个特定的属性不再指向它,所讨论的闭包仍然会适应并采取行动事实上,该属性没有对象 (原始对象仍然存在,它刚刚移动到另一个地址)。

总结一下,我认为“占位符”属性var tryToCaptureAnotherOptional: Foo? = Foo(name: "tryToCaptureAnotherOptional") var holdItInNonOptional: Foo = tryToCaptureAnotherOptional! tryToCaptureAnotherOptional?.test = { print("Running closure from \(tryToCaptureAnotherOptional?.name)") } closure = tryToCaptureAnotherOptional!.test closure() // Prints "Running closure from Optional("tryToCaptureAnotherOptional")" tryToCaptureAnotherOptional = nil closure() // Prints "Running closure from nil" print(holdItInNonOptional.name) // Prints "tryToCaptureAnotherOptional" (!!!) holdItInNonOptional.test() // Also prints "Running closure from nil" 与其他“具体”属性之间存在差异。后者附有隐含的合同,而前者只有或不是。

答案 2 :(得分:3)

灵感来自@Leo@Anton Bronnikov的答案,以及Turton先生的这篇文章:

Understanding Optionals in Swift

我发现我所有的困惑来自于我对Swift中Optional Types的外在理解。

我们可以在Swift文档中看到有关OptionalImplicitlyUnwrappedOptional的说明以及Optional的定义:

public enum Optional<Wrapped> : _Reflectable, NilLiteralConvertible {
    case None
    case Some(Wrapped)
    /// Construct a `nil` instance.
    public init()
    /// Construct a non-`nil` instance that stores `some`.
    public init(_ some: Wrapped)
    /// If `self == nil`, returns `nil`.  Otherwise, returns `f(self!)`.
    @warn_unused_result
    public func map<U>(@noescape f: (Wrapped) throws -> U) rethrows -> U?
    /// Returns `nil` if `self` is nil, `f(self!)` otherwise.
    @warn_unused_result
    public func flatMap<U>(@noescape f: (Wrapped) throws -> U?) rethrows -> U?
    /// Create an instance initialized with `nil`.
    public init(nilLiteral: ())
}

可选类型,无论是显式展开还是隐式展开,实际上都是 Enumeration ,它被称为值类型。

所以在上面的示例代码中:

var heading: HTMLElement? = HTMLElement(name: "h1")

我们所讨论的变量heading实际上是枚举,值类型不是对HTMLElement实例的引用。因此,它是一个枚举而不是一个被闭包捕获的引用类型。当然,HTMLElement实例的引用计数还没有在闭包内加起来。

heading!.asHTML = {
    return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>"
}
print(heading!.asHTML())

此时,HTMLElement实例的保留计数为+1,实例由枚举heading保存。关闭的保留计数为+1,由HTMLElement实例保存。虽然枚举heading是通过关闭捕获的。参考周期与他的答案中所示的图像@Leo完全相同,

Leo's illustration

当我们设置heading = nil时,枚举heading持有的HTMLElement实例的引用将被释放,实例的引用计数将变为0,然后闭包的引用计数将变为0同样,随后,枚举本身将通过关闭释放。最后,所有东西都会被正确释放。

总结:对于曾经像我这样的Objective-c开发人员的Swift初学者来说,深入了解两种语言之间的差异对我们来说非常重要。非常感谢所有的回复者,你的答案非常鼓舞人心,也很有帮助,谢谢。

作为一个快速的初学者,在这个回复中不可避免地会有一些错误,如果你找到了,请告诉我,因为这对后来的读者来说非常重要。

答案 3 :(得分:1)

您的asHTML是一个包含闭包的变量。闭包拥有对HTMLElement对象的强引用。并且该闭包被存储,再次保持强大的参考。所以你有自己的周期。

您需要做的只是拥有一个返回闭包的函数而不是变量。

或者,您可以声明闭包捕获的值,因此让它捕获self的弱副本。

答案 4 :(得分:0)

我认为swift在这方面的表现略有不同:

来自this

  

如果在闭包的作用域之外声明了任何变量,则在闭包的作用域内引用该变量会创建另一个对该对象的强引用。对此唯一的例外是使用值语义的变量,例如 Swift 中的Ints,Strings,Arrays和Dictionaries。