A pitfall with value type + escaping closure

I'm refactoring my app to use protocol and value type as much as possible, so I did a lot of experiments to understand how to use them properly. I find a pitfall when using value type and escaping closure together. See code below:

struct S {
    var n: Int
    var callback: (() -> Void)!

    init(_ n: Int) {
        self.n = n
        self.callback = { [self] in
            print(self.n)
        }
    }
}

var s1 = S(1)
s1.n = 2
s1.callback() // It outputs 1, not 2

The issue is different from the one that SE-0035 fixed. In SE-0035 case it's the escaping closure changes its copy. But in this case, it's the original copy that was changed but the escaping closure still uses the old copy.

If I understand it correctly, issue like this is almost unresolvable for value type, because there is no way to reference a single value. There have to be multiple copies of the value in above scenario and hence the inconsistency. If so, isn't value type and escaping closure a bad design that should be avoided? But I can't find any discussion about this. I wonder what's other people's experince on this? Sometimes I think it would be very helpful if Apple or someone with knowledge in this could write a book about value type and protocol programming best practice, the patterns and the scenarios to use them or to not use them.

2 Likes

This is how capture lists work, for value types. Whatever you put in brackets is frozen as a copy.

It will help you to wrap your mind around these being functionally equivalent, but only when self is a value type.

callback = { [self] in print(self.n) }
callback = { [n = self.n] in print(n) }

Agreed. Capture lists need more documentation.

3 Likes

Note that you can achieve what you want with a small change: pass the value as a parameter in the callback. Then you can add a convenience method that automatically passes self to the callback. Here's a modified example:

struct S {
    var n: Int
    var callback: ((S) -> Void)!

    func callTheCallback() {
        callback(self)
    }

    init(_ n: Int) {
        self.n = n
        self.callback = { (s) in
            print(s.n)
        }
    }
}

var s1 = S(1)
s1.n = 2
s1.callTheCallback() // It outputs 2
3 Likes

Actually, value types can be captured with reference semantics, as long as you declare it as a var. And in your case, you are capturing self, which is immutable, and a copy of self is created.

However, if you want to capture a mutating self, it is still impossible:

struct S {
    var n: Int
    var callback: (() -> Void)!

    init(_ n: Int) {
        self.n = n
    }

    mutating func setup() {
        callback = {
            print(self.n)  // compiler error here, cannot capture mutating self
        }
    }
}

The compiler will prevent you from doing this, because generally, capturing an inout value in an escaping closure can easily introduce a dangling pointer.

As far as I'm concerned, the best way in this callback scenario is to declare S as a class, because identity matters for callbacks.

Thank all for your suggestions.

That's a helpful way to think about it. It also works if the member n is a reference type.

But I think the fact that we need such a way to think about it says something. Ideally it should be straightforward on concept, without help like this. That said, I understand that Swift supports a combinations of different features (value type, reference type, mutable, immutable, etc.) and hence is complex.

Thanks for the suggestion! I considered this approach too (though I didn't go as far as thinking about that convenience method) and I'm going to use it. I wasn't sure at first because this approach seemed to defeat the purpose of closure. I mean, closure is effectively function encapsulating data. But the approach intentionally removes the data. However, it's the encapsulated data that caused the issue in the first place, so removing them makes sense.

I think this is only true for non-escaping closure.

2 Likes

I'm not sure I follow what you mean here, can you give an example of a value type being captured with reference semantics?

EDIT: Ah nvm if what you meant was eg:

var x = 1
let c = { print(x); x += 1 }
c() // 1
c() // 2
x = 100
c() // 100
c() // 101
2 Likes