• Breaking Swift with Reference counted structs
  • The Nuggets translation Project
  • Translator: Tuccuay
  • Proofread by Jing KE, Jack King

In Swift, a class type is assigned to the heap, and reference counts are used to track its life cycle and remove it from the heap when it is destroyed. Structs, on the other hand, do not allocate extra memory in the heap, do not use reference counters, and do not have destruction steps.

Isn’t it?

In fact, “heap,” “reference count,” and “cleanup behavior” also apply to “structure” types. Beware, though: inappropriate behavior can cause problems, and I’ll show you how you might use “structs” as “classes,” and why this can lead to memory leaks, faulty behavior, and compiler errors.

Warning: This article uses some anti-patterns (you don’t really want to do this). I did this to highlight some of the hidden risks of structs using closures, and the best way to avoid them is to master them, unless you’re comfortable with knowing the risks.

Directory:

  1. Scope of a class in a structure
  2. Attempt to access a structure from a closure
  3. Crazy cycle
  4. How do we break this cycle?
  5. Copying doesn’t work, how about sharing references?
  6. Some views
  7. Said in the last

Scope of a class in a structure

Although a “struct” does not normally have a deinit method, like other Swift types, it needs to be reference-counted correctly. When a member variable in a structure is referenced or the entire structure is destroyed, the reference count must be increased or decreased correctly.

In fact, we can do this by using the OnDelete class. When a structure meets certain conditions, its reference count will be reduced with the behavior of the structure, as if it had a deinit method

public final class OnDelete {
    var closure: (a) -> Void
    public init(_ c: () -> Void) {
        closure = c
    }
    deinit {
        closure(a)}}Copy the code

And use the OnDelete class like this:

struct DeletionLogger {
    let od = OnDelete { print("DeletionLogger deleted")}}do {
    let dl = DeletionLogger()
    print("Not deleted, yet")
    withExtendedLifetime(dl) {}
}Copy the code

You get output like this:

Not deleted, yet
DeletionLogger deletedCopy the code

When DeletionLogger is deleted (that is, after withExtendedLifetime runs after print), the closure of OnDelete will be executed.

Attempt to access a structure from a closure

Now that everything looks fine, an OnDelete object can execute a function before the structure is destroyed, which looks a bit like a deinit method. But while it looks like it can mimic the deinit behavior of a “class,” deinit has one important feature that the OnDelete method can’t: it runs within the scope of a structure.

Although this is a bad idea, let’s try to access the structure and see what happens. We’ll use a simple structure that will have an Int value and an OnDelete closure, and finally output an Int value.

struct Counter {
    let count = 0
    let od = OnDelete { print("Counter value is \(count)")}}Copy the code

Instance member ‘count’ cannot be used on type ‘SomeStruct’ This isn’t surprising: we’re not allowed to do this, and you can’t access another space from one class’s initializer.

Let’s initialize a structure correctly and try to get one of its member variables:

struct Counter {
    let count = 0
    var od: OnDelete? = nil
    init() {
        od = OnDelete { print("Counter value is \(self.count)")}}}Copy the code

The compiler reported a “memory segment fault” in Swift 2.2, A fatal error was reported in Swift Development Snapshot 2016-03-26.

“Excellent!” I’m Angry! .

Of course, I can avoid all compilation errors like this:

struct Counter {
    var count: Int
    let od: OnDelete
    init() {
        let c = 0
        count = c
        od = OnDelete { print("Counter value is \(c)")}}}Copy the code

Or in another, less common way, in which case they are equivalent:

struct Counter {
    var count = 0
    let od: OnDelete?
    init() {
        od = OnDelete { [count] in print("Counter value is \(count)")}}}Copy the code

But these two methods don’t really give us access to the structure itself. Both methods capture only an immutable copy of count, but we want to get the latest variable value of count.

struct Counter {
    var count = 0
    var od: OnDelete?
    init() {
        od = OnDelete { print("Counter value is \(self.count)")}}}Copy the code

Long live! It’s even more perfect. Everything is mutable and shared. We captured the count variable and compiled it.

We should try to use this code because it works fine, right?

Crazy cycle

Obviously not if we run the code as before:

do {
    let c = Counter()
    print("Not deleted, yet")
    withExtendedLifetime(c) {}
}Copy the code

We only get output like this:

Not deleted, yetCopy the code

The OnDelete closure is not called. Why?

By looking at SIL(Swift Intermediate Language, returned via swiftc-EMa-SIL), it is clear that the closure of OnDelete prevents self from being optimized to the heap. This means that instead of using alloc_stack, the self variable is allocated by alloc_box:

%1 = alloc_box $Counter, var, name "self", argno 1 // users: %2%,20%,22%,29Copy the code

And the closure of OnDelete refers to the alloc_box.

What’s the problem? This is a reference-counting loop:

The closure refers to the encapsulated Counter → the encapsulated Counter refers to OnDelete → OnDelete refers to the closure

When this loop is generated, our OnDelete object is never released, and therefore the closure will not be called.

How do we break this cycle?

If Counter were a class, we could use the [weak Self] closure to avoid this loop strong-referencing, but Counter is a structure, not a class, and trying to do so would only get an error, which is bad.

Can we manually break this loop and set the OD property to nil after construction?

var c = Counter()
c.od = nilCopy the code

No, it still doesn’t work. Why is that?

When Counter. Init ends, alloc_box’s creation is copied onto the stack. This means that the copy referenced by OnDelete is different from the copy we accessed. The copy of the OnDelete reference is now inaccessible to us.

We’ve created an unbreakable cycle.

As Joe Groff pointed out above, the Swift development process SE-0035 should avoid this problem by limiting maximum inout capture (the kind used by the counter.init method), Until the @noescape closure (which will prevent OnDelete’s trailing closure from being caught).

Copying doesn’t work, how about sharing references?

The problem arises because our method returns a different copy than the one from self’s counter.init. What we need is for the version returned to be the same as the version referenced.

Let’s avoid doing anything in the init method and use a static method instead.

struct Counter {
    var count = 0
    var od: OnDelete? = nil
    static func construct(a) -> Counter {
        var c = Counter()
        c.od = OnDelete{
            print("Value loop break is \(c.count)")}return c
    }
}

do {
    var c = Counter.construct()
    c.count += 1
    c.od = nil
}Copy the code

Same problem: we get a Counter, it’s permanently embedded in OnDelete, it’s not the version that was returned.

Let’s change the static method…

struct Counter {
    var count = 0
    var od: OnDelete? = nil
    static func construct(a) -> (a) -> () {
        var c = Counter()
        c.od = OnDelete{
            print("Value loop break is \(c.count)")}return {
            c.count += 1
            c.od = nil
        }
    }
}

do {
    var loopBreaker = Counter.construct()
    loopBreaker()
}Copy the code

The output now looks like this:

Counter value is 1Copy the code

This finally works, and you can see that our loopBreaker closure correctly affects the printing of the OnDelete closure.

Now we don’t have to return Counter instance, we don’t have to make a separate copy. Now that there is only one copy of the Counter instance and its version of alloc_box is shared between the two closures, we reference the struct in the heap, and the OnDelete method can properly access its member variables when the struct is destroyed.

Some views

The code technically “works,” but in reality it’s a mess. We’ve created a circular strong reference that we can only break manually, we can just set construct in the closure of Counter and there’s only one instance based on it, we now have 4 Spaces allocated in the heap. The closure in OnDelete, the OnDelete object itself, the wrapped C variable, and the loopBreaker closure.

If you’re not aware of the problem… Then we’ve wasted all this time.

We can start by creating Counter as a “class” to keep the number of allocated heaps at 1.

class Counter {
    var count = 0
    deinit {
        print("Counter value is \(count)")}}Copy the code

Long story short: If you need to access mutable data from a different scope, structs are probably not a good choice.

Said in the last

Closure capture is used when we have written something and expect the compiler to do it. However, capturing mutable values has multiple consequences, with subtle differences that need to be understood to avoid these problems. We used sophisticated methods to fix these small problems, and hopefully Swift 3 will fix them.

Don’t forget to consider circular references when capturing structures in class attributes. You cannot weakly reference the capture structure, so if a loop strong reference occurs, you need to break it in some other way.

By all accounts, this article takes you through a very foolish attempt to capture itself with a structure. Don’t do that. Like other structures that use reference counting, it shouldn’t be a loop. If you find yourself trying to create a loop, you might want to use the class type and weak to connect the parent element from the child element.

Finally, I had a good idea for using the OnDelete class (I’ll use it in the next article), but I shouldn’t have started with the idea of making it work like the deinit method — that’s where it got into trouble (its properties are out of scope).