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.
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
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
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.
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 asself.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.
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,
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With