[swift-evolution] deinit and failable initializers
Douglas Gregor
dgregor at apple.com
Tue Jan 26 11:39:09 CST 2016
> On Jan 26, 2016, at 9:15 AM, Chris Eidhof via swift-evolution <swift-evolution at swift.org> wrote:
>
> Now that we can return nil from a failable initializer without having initialized all the properties, it’s easier to make a mistake. For example, consider the following (artificial) code:
>
> class MyArray<T> {
> var pointer: UnsafeMutablePointer<T>
> var capacity: Int
>
> init?(capacity: Int) {
> pointer = UnsafeMutablePointer.alloc(capacity)
> if capacity > 100 {
> // Here we should also free the memory. In other words, duplicate the code from deinit.
> return nil
> }
> self.capacity = capacity
>
> }
>
> deinit {
> pointer.destroy(capacity)
> }
> }
>
> In the `return nil` case, we should really free the memory allocated by the pointer. Or in other words, we need to duplicate the behavior from the deinit.
>
> Before Swift 2.2, this mistake wasn’t possible, because we knew that we could count on deinit being called, *always*. With the current behavior, return `nil` is easier, but it does come at the cost of accidentally introducing bugs. As Joe Groff pointed out, a solution would be to have something like “deferOnError” (or in this case, “deferOnNil”), but that feels a bit heavy-weight to me (and you still have to duplicate code).
>
> In any case, I think it’s nice that we can now return nil earlier. I don’t like that it goes at the cost of safety, but I realize it’s probably only making things less safe in a small amount of edge cases.
Let’s re-order the statements in your example:
class MyArray<T> {
var pointer: UnsafeMutablePointer<T>
var capacity: Int
init?(capacity: Int) {
if capacity > 100 {
// Here we should also free the memory. In other words, duplicate the code from deinit.
return nil
}
self.capacity = capacity
pointer = UnsafeMutablePointer.alloc(capacity)
}
deinit {
pointer.destroy(capacity)
}
}
If the initializer returns ‘nil’ and we still call deinit, we end up destroying a pointer that was never allocated.
If you come from an Objective-C background, you might expect implicit zeroing of the allocated block to help here. However, Swift doesn’t have that, because many Swift types don’t have a “zero” state that’s safe to destroy. For example, anything with a member of non-optional reference type, e.g.,
class ClassWrapper {
var array: MyArray<String>
init(array: MyArray<String>) {
self.array = array
}
deinit {
print(array) // array is a valid instance of MyArray<String>
}
}
A valid ClassWrapper instance will always have an instance of MyArray<String>, even throughout its deinitializer.
The basic property here is that one cannot run a deinitializer on an instance that hasn’t been fully constructed (all the way up the class hierarchy).
- Doug
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160126/16638c3a/attachment.html>
More information about the swift-evolution
mailing list