[swift-evolution] deinit and failable initializers

Chris Eidhof chris at eidhof.nl
Tue Jan 26 15:39:41 CST 2016


Absolutely, I agree with you. Of course, this was a simplified example, you
can easily construct an example where just reordering isn’t going to cut it.

It’s not hard to do things right in the current model. We can reorder
statements, write wrappers, or think of different solutions altogether.
Once you understand how it works, it’s very easy to write correct code.

However, my complaint is that it’s now also easy to make a mistake. In the
previous model, it wasn’t so easy (the previous model was simpler: an init
would always be matched by a deinit).

On Tue, Jan 26, 2016 at 6:39 PM, Douglas Gregor <dgregor at apple.com> wrote:

>
> 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
>
>
>


-- 
Chris Eidhof
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20160126/d1f4bb41/attachment.html>


More information about the swift-evolution mailing list