I'd like to discuss the semantics of forget self
. It's essentially implied that writing forget self
consumes the value of self
, and then it does a memberwise destruction of self's stored properties, bypassing its deinit. From an ownership perspective, the forget
statement would be modeled like the following function:
func forget<T>(_ t: consuming T) { .. do memberwise destruction .. }
Consumption of a var doesn't prevent you from reinitializing that var with a new value.
Since self is var bound in a number of contexts, like consuming
, mutating
, and even init
functions of value types, should we allow reinitialization of self
after a forget
? Should we allow forget
in an init
?
One big use case I see for this reinitialization is allowing you to avoid invoking the deinit
while in an init
, which happens after self
has been initialized within that init
. For example, just like for a class today, you can invoke the deinit
from the init
in this example.
@noncopyable struct Data {
var data: [Datum] = []
init(_ handle: NetworkHandle) throws {
while let raw = handle.next() {
self.data.append(raw)
}
if handle.incompleteMessage {
throw E.someError
}
}
deinit {
// Some code assuming we have a complete message.
assert(isCompleteMessage(data))
}
}
The reason is subtle: because after self
been fully initialized (e.g., because data
has a default value), if you throw an Error
out of an init
(or return nil
in a failable init) you will trigger self
's deinit
. But if you hadn't yet fully initialized self
's properties, then you'll get memberwise destruction of only the initialized fields in that failure-exit scenario.
There are a number of workarounds to the above that require rewriting the init
, or the deinit
. For example, you could write the data results into a local var
and only initialize self.data
once we have a complete message. This is one situation where forget
can be useful in a non-consuming
function, which is outside of what's proposed:
init(_ handle: NetworkHandle) throws {
// ...
if handle.incompleteMessage {
forget self // just add this to avoid the deinit!
throw E.someError
}
}
But, unlike a class, structs and enums has a var-bound self. That means you can reassign self
in the init. So if instead of throwing an error, one just wants to default initialize self
, you're allowed to do that:
init(_ handle: NetworkHandle) {
while let raw = handle.next() {
self.data.append(raw)
}
if handle.incompleteMessage {
self = .init()
}
}
The problem in this example is that overwriting self
after it's been initialized is also going to invoke the deinit
on the old value. Again, we can fix this by writing forget self
before the reinitialization:
if handle.incompleteMessage {
forget self
self = .init()
}
Allowing a forget self
before reinitialization can also be useful for a mutating method:
@noncopyable enum MessageBox {
case empty
case holding(Message)
mutating func overwrite(_ newMessage: Message) {
forget self // don't trigger deinit when we're just replacing the message.
self = .holding(newMessage)
}
deinit {
assert(isEmpty(self), "message box held message during destruction!")
}
}
So, it seems like forget self
can be very useful for noncopyable types in mutating
and init
methods, in addition to consuming
ones. And that reinitialization of self
after forget
is not terribly bizarre. One important thing to note is that methods like overwrite
here do not consume self
on all paths, which is what's currently proposed for consuming
methods.
Thoughts?