final
class C
{
init()
{
}
}
enum E
{
case empty
case c(C)
}
func foo()
{
var e:E = .c(.init())
switch e
{
case .c(var c):
e = .empty
print(isKnownUniquelyReferenced(&c))
default:
break
}
}
foo()
by all means it should be printing true since i am releasing the enum reference by setting it to .empty. But it’s printing false. Why?
this similar function does not have this issue:
func foo()
{
var e:E = .empty
switch e
{
case .c:
break
case .empty:
var c:C = .init()
print(isKnownUniquelyReferenced(&c))
e = .c(c)
print(isKnownUniquelyReferenced(&c))
e = .empty
print(isKnownUniquelyReferenced(&c))
}
}
[It is 12:45 AM in my timezone so this might be way off the mark but...]
I suspect that the reason is that matching is non-consuming. If c was uniquely referenced, then based on the scope, one would expect that it would be released after the print. However, that means that adding code which again matched on e afterwards will run into trouble.
func foo2()
var e:E = .c(.init())
switch e
{
case .c(var c):
// +2
e = .empty
print(isKnownUniquelyReferenced(&c))
// +1
default:
break
}
// We need to have at least +1 for later access.
switch e {
case .c(var c):
// +2
e = .empty
print(isKnownUniquelyReferenced(&c))
// +1
default:
break
}
}
This code should certainly work. However, if this also works, and foo() works as per your expectations, now the behavior of the first print is affected by what is present later in the function, possibly hundreds of lines below. Which would be strange from a user perspective.
One can imagine that you could have a move switch which avoids the extra retain/release but it prevents you from accessing e later so it is less flexible. IIUC, this is exactly what Rust's match does for non-trivially-copyable types.
@Michael_Gottesman has been looking at this kind of thing lately and might have some insight.
On the 5.3 compiler, it looks like it does return true when compiled with -O, even without the = .empty assignment, because the unused enum wrapper is completely eliminated. This kind of observable behavior at different optimization levels makes me a little uneasy, tbh. It would be great to make more formal guarantees about this even in unoptimized code (and also to provide in-place accessors for enum associated values).
Okay, i was able to get the correct refcounting behavior with the class directly in the enum payload, but this slightly more realistic example, where the class is wrapped in a struct doesnt:
final
class C
{
init()
{
}
}
struct S
{
var c:C
init()
{
self.c = .init()
}
mutating
func foo()
{
print(isKnownUniquelyReferenced(&self.c))
}
}
enum E
{
case empty
case s(S)
}
func foo()
{
var e:E = .s(.init())
switch e
{
case .s(var s):
e = .empty
s.foo()
default:
break
}
}
foo()
the assignments that happen inside the switch still work as expected:
func foo()
{
var e:E = .empty
switch e
{
case .s:
break
default:
var s:S = .init()
s.foo()
e = .s(s)
s.foo()
e = .empty
s.foo()
}
}
// true
// false
// true
what i really don’t get is why this, structurally identical code using Optional instead of E works but my first example doesn’t:
func foo()
{
var e:S? = .some(.init())
switch e
{
case .some(var s):
e = nil
s.foo()
case .none:
break
}
}
The observable semantics of isUniquelyReferenced have to be sensitive to optimization or else we'd almost never be able to shorten the lifetimes of objects. That's all that's happening here: the optimizer is destroying the variable e early because it's not being used. We do not want to codify a C++-like strict-lifetimes model.
Let's step back to look at the original question. There is no way to mutate case payloads in a switch in Swift today. That might seem like a non sequitur, but it's actually critical. The question answered by isUniquelyReferenced is really "does this variable hold the only reference to its current value?", and that only really works from the user's perspective if the variable passed in is really the variable that the user cares about, not a copy that happens to hold the same value. That's why it takes the variable by reference in the first place: it's not actually a useful question to ask about a value. So this function can only do the right thing for case payloads if we're able to bind the pattern variable directly to the storage of the payload, which means that the entire switch essentially has to be an exclusive access to wherever the enum is stored. If we could do that, then we could just as well allow payloads to be mutated. But those aren't the current semantics of switch.
So the right solution here is to add some way to do a mutating switch, and then you'll be able to bind pattern variables like c directly to the storage of the payload, and then it'll be reasonable to ask whether that storage holds a unique reference.
where pattern matching it with a var binding would mutate the original enum payload?
that still wouldn’t solve the case where completely replacing the enum case should release all references though. but can’t that already be done without changing the semantics of the language?
well, a switch is really just an elaborate if statement, so there’s no reason why the following should have different behavior, yet for some reason it does:
func foo()
{
var e:E = .s(.init())
if case .s(var s) = e
{
s.foo() // true
e = .empty
s.foo() // true
}
e = .s(.init())
switch e
{
case .s(var s):
s.foo() // false
e = .empty
s.foo() // false
default:
break
}
}
It wouldn’t make any sense to combine this with var or let.
Lantua is right, though: we’d want something up front in the switch to flag that the whole thing is a mutation of the variable. Once you have that, we could potentially just use var in the pattern itself.
i would support the switch &y syntax. but i think this thread (as usual) has drifted far from the original issue, which is that this assignment:
case .c(var c):
e = .empty
ought to decrement the reference count of c by 1, just as setting an equivalent Optional<C> to nil would. i don’t see why we need to invent new syntax and completely new semantics to fix what i would just have considered a bug.
It does, but the old enum value is copied to get the value to switch over, and then the enum payload is copied to initialize the var, so it’s still not uniquely referenced. This is why I emphasized the importance of doing an “in-place” switch that guarantees direct binding all the way down.
why does this retain persist through the entire execution of the case blocks? the reference is only needed for the pattern matching:
e = .s(.init()) // +1 (total: 1)
switch e // +1 (total: 2)
{
// +1 for var binding (total: +3)
case .s(var s):
// -1 for return from pattern match (total: +2)
s.foo()
e = .empty // -1 (total: +1)
s.foo() // should be uniquely referenced
default:
// -1 for return from pattern match (total: +1)
break
}
It’s not guaranteed either way, but yes, the implementation could absolutely do a better job “forwarding” the switched-over value into the pattern variable.
John already sorta said this, but the answer to "should I file a bug?" is always yes. If it makes sense to ask the question, the answer is yes. (Not just for Swift, for any project.)