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?