繁体   English   中英

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

[英]Confusing example of strong reference cycle in Swift

这是Apple的文档中的一个示例:

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")
    }

}

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

让我困惑的是以下代码:

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.

据我所知,从Swift文档和我自己的Objective-c经验来看,变量heading将被闭包捕获,因此应该引起一个强大的参考周期。 但事实并非如此,这让我很困惑。

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

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

;

HTMLElement *heading = [[HTMLElement alloc] init];
heading.name = @"h1";
heading.text = @"some default text";

heading.asHTML = ^ {
    return [NSString stringWithFormat:@"<%@>%@</%@>", heading.name, heading.text, heading.name];
};

NSLog(@"%@", heading.asHTML());

heading = nil;
// heading has not been deinitialized here

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

因为,在后一种情况下

斯威夫特封闭持有很强的借鉴heading的不是实例heading那点

在图像中,它显示如下

如果我们通过set heading = 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>

所以,测试代码的图像是 在此输入图像描述

你会看到游乐场的日志

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

没有找到任何关于这个的文件,只是从我的测试和理解,希望它会有所帮助

我认为devi是详细的。 文件说明:

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

在这里注意self ,我认为,这是问题的关键。

另一篇文档建议:

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

因此,换句话说,它是被捕获的常量和变量 ,而不是对象本身。 这只是self是一个特例,因为当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"

..换句话说,即使对象并没有真正“消失”,但只是一些特定的属性不再指向它,所讨论的闭包仍然会适应并完成这样一个事实:没有任何对象持有属性 (原始对象仍然存在,它刚刚移动到另一个地址)。


总而言之,我认为区别在于“占位符”属性self与其他“具体”属性。 后者附有隐含的合同,而前者只有或不是。

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

了解Swift中的Optionals

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

我们可以在Swift文档和Optional定义中看到有关OptionalImplicitlyUnwrappedOptional的描述:

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: ())
}

可选类型,无论是显式展开还是隐式展开,实际上是一种称为值类型的枚举

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

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图像完全相同

狮子座的插图

当我们设置heading = nil ,将释放由枚举heading保存的HTMLElement实例的引用,实例的引用计数将变为0,然后闭包的引用计数也将变为0,随后,枚举本身将通过关闭释放。 一切都将在最后正确释放。

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


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

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

您需要做的就是不使用变量,只需要一个返回闭包的函数。

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

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

由此

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM