Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Confusing example of strong reference cycle in Swift

This is an example from Apple's document:

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

}

I understand why this closure property would cause strong reference cycle and I know how to resolve it. And i'm not going to argue this.

What really confuses me is the follow code:

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.

As far as i know from Swift documentation and my own experience of Objective-c, variable heading will be captured by the closure, thus a strong reference cycle should have been caused. But it did not, this really confused me.

I also wrote an Objective-c counterpart of this example, and it did have caused strong reference cycle as I expected.

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

Any hint or guide will be greatly appreciated.

like image 611
Burrows Wang Avatar asked Dec 18 '15 09:12

Burrows Wang


4 Answers

Because,in the later case

Swift closure hold a strong reference of heading not the instance of heading that point to

In an image,it shows like this

if we break the red line by set heading = nil,then the reference circle is broken.

Update begin:

But,if you do not set heading to nil,there is still a reference circle like the image I post above.You can test it like this

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

Update end

I also write the below test code to prove the closure does not hold a strong reference to the instance in memory

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>

So,the image of the test code is enter image description here

And you will see log from playground

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

Have not find any document about this,just from my testing and understanding,hopes it will be helpful

like image 58
Leo Avatar answered Oct 16 '22 19:10

Leo


Your asHTML is a variable containing a closure. The closure holds a strong reference to the HTMLElement object. And that closure gets stored, again holding a strong reference. So you have your cycle.

All you need to do is instead of having a variable, just have a function that returns the closure.

Alternatively, you can declare which values a closure captures, so let it capture a weak copy of self.

like image 1
gnasher729 Avatar answered Oct 16 '22 19:10

gnasher729


I think the devi is in the detail. The documentation states:

... capture might occur because the closure’s body accesses a property of the instance, such as self.someProperty, or because the closure calls a method on the instance, such as self.someMethod().

Note self here, which, I believe, is the key to the question.

Another piece of documentation suggests:

A closure can capture constants and variables from the surrounding context in which it is defined. The closure can then refer to and modify the values of those constants and variables from within its body, even if the original scope that defined the constants and variables no longer exists.

So, in other words, it is constants and variables that are being captured, and not objects per se. It's just self is a special case because when self is used in a closure that is initialised from within an object, then the contract is that such self is always there when the closure is triggered. In other words, under no circumstances can it happen that a closure with self in its body does execute but the object that this self is pointing to is gone. Consider this: such closure can be taken elsewhere, e.g. assigned to another property of another object, and therefore it must be able to run even if the original owner of the object being captured "forgets" about it. It would be ridiculous to ask the developer to check whether self is nil, right? Hence the need to keep a strong reference.

Now, if you move on to another case, where there is no self being used by a closure, but some (explicitly unwrapped) optional, then it's totally another ball game. Such optional can be nil, and the developer must accept this fact, and take care of it. When such closure runs, it could have been the case that the optional property it is using was actually never even been assigned with a concrete value! So, what's the point of holding a strong reference?


To illustrate. Here is the basic class:

class Foo {
    let name: String

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

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

And this is the mirror of a strong-reference cycle:

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"

Now, the next case with optional property outside of the object:

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"

.. i.e. we still "remember" the closure, but the closure should be able to handle the case that the property it is using is actually nil.

But "fun" only starts now. For example, we can do:

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"

.. in other words, even though the object is not really "gone", but just some specific property is no longer pointing to it, the closure in question would still adapt and act up to the fact that there is no object held by that property (while the original object still lives, it had just moved to another address).


To sum it up, I think the difference is between "placeholder" property self vs. other "concrete" properties. The latter has implicit contracts attached to it, while the former have only to be or not to be.

like image 3
0x416e746f6e Avatar answered Oct 16 '22 18:10

0x416e746f6e


Inspired by answers of @Leo and @Anton Bronnikov, as well as this article from Mr Turton:

Understanding Optionals in Swift

I found that all my confusion comes from my peripheral understanding of Optional Types in Swift.

As we can see the descriptions about Optional and ImplicitlyUnwrappedOptional in Swift documentation and definition of 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: ())
}

An optional type, no matter it is explicitly unwrapped or implicitly unwrapped, is actually an Enumeration which is known as a value type.

So in the sample code above:

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

the variable heading we are talking about is actually an enumeration, a value type not a reference to instance of HTMLElement. Therefore, it was an enumeration rather a reference type which has been captured by the closure. And of course the reference count of HTMLElement instance have not ben added up inside the closure.

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

At this point, the retain count of HTMLElement instance is +1, the instance is held by enumeration heading . And the retain count of closure is +1, held by the HTMLElement instance. Whilst the enumeration heading is captured by closure. The reference cycle is exactly like the image @Leo illustrated in his answer,

Leo's illustration

When we setting heading = nil, the reference of HTMLElement instance held by enumeration heading will be released, the reference count of instance will become to 0, and then the reference count of closure will become to 0 as well, subsequently, the enumeration itself will be released by closure. Everything will be freed up correctly at the end.

To conclude: for Swift beginners who used to be a Objective-c developer like me, it is extremely important for us to grab a deep understanding of the differences between two languages. And many thanks to all repliers again, your answers are really inspiring as well as helpful, thank you.


As a swift beginner, there will inevitably be some mistakes in this reply, if you have found any, please let me know as it is very important for the later readers.

like image 3
Burrows Wang Avatar answered Oct 16 '22 18:10

Burrows Wang