[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