Best practice to implement a failable initializer in Swift
Asked Answered
L

8

100

With the following code I try to define a simple model class and it's failable initializer, which takes a (json-) dictionary as parameter. The initializer should return nil if the user name is not defined in the original json.

1. Why doesn't the code compile? The error message says:

All stored properties of a class instance must be initialized before returning nil from an initializer.

That doesn't make sense. Why should I initialize those properties when I plan to return nil?

2. Is my approach the right one or would there be other ideas or common patterns to achieve my goal?

class User: NSObject {

    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        if let value: String = dictionary["user_name"] as? String {
            userName = value
        }
        else {
           return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()
    }
}
Luxuriance answered 21/10, 2014 at 20:21 Comment(1)
I had a similar issue, with mine I concluded that each dictionary value should be expected and so I force unwrap the values. If the property isn’t there I’ll be able to catch the bug. Additionally, I added a canSetCalculableProperties boolean parameter allowing my initialiser to calculate properties that can or can’t be created on the fly. For example, if a dateCreated key is missing and I can set the property on the fly because canSetCalculableProperties parameter is true, then I just set it to the current date.Littleton
M
72

Update: From the Swift 2.2 Change Log (released March 21, 2016):

Designated class initializers declared as failable or throwing may now return nil or throw an error, respectively, before the object has been fully initialized.


For Swift 2.1 and earlier:

According to Apple's documentation (and your compiler error), a class must initialize all its stored properties before returning nil from a failable initializer:

For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place.

Note: It actually works fine for structures and enumerations, just not classes.

The suggested way to handle stored properties that can't be initialized before the initializer fails is to declare them as implicitly unwrapped optionals.

Example from the docs:

class Product {
    let name: String!
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

In the example above, the name property of the Product class is defined as having an implicitly unwrapped optional string type (String!). Because it is of an optional type, this means that the name property has a default value of nil before it is assigned a specific value during initialization. This default value of nil in turn means that all of the properties introduced by the Product class have a valid initial value. As a result, the failable initializer for Product can trigger an initialization failure at the start of the initializer if it is passed an empty string, before assigning a specific value to the name property within the initializer.

In your case, however, simply defining userName as a String! does not fix the compile error because you still need to worry about initializing the properties on your base class, NSObject. Luckily, with userName defined as a String!, you can actually call super.init() before you return nil which will init your NSObject base class and fix the compile error.

class User: NSObject {

    let userName: String!
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        super.init()

        if let value = dictionary["user_name"] as? String {
            self.userName = value
        }
        else {
            return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            self.isSuperUser = value
        }

        self.someDetails = dictionary["some_details"] as? Array
    }
}
Musetta answered 21/10, 2014 at 20:48 Comment(5)
Thank you very much not only right, but also well explainedLuxuriance
in swift1.2, Example from the docs make an error "All stored properties of a class instance must be initialized before returning nil from an initializer"Laoighis
@Laoighis That's correct, the example from the documentation (Product class) can not trigger a initialization failure before assigning a specific value, even though the docs say it can. The docs are not in sync with the latest Swift version. It is advised to make it a var for now instead let. source: Chris Lattner.Sharasharai
The documentation has this piece of code little bit different: you first set the property, and then check if it's present. See “Failable Initializers for Classes”, “The Swift Programming Language.” ``` class Product { let name: String! init?(name: String) { self.name = name if name.isEmpty { return nil } } } ```Canada
I read this too in the Apple docs but I fail to see why this would be required. A failure would mean returning nil anyway, what does it matter then whether the properties have been initialized?Electronegative
D
132

That doesn't make sense. Why should I initialize those properties when I plan to return nil?

According to Chris Lattner this is a bug. Here is what he says:

This is an implementation limitation in the swift 1.1 compiler, documented in the release notes. The compiler is currently unable to destroy partially initialized classes in all cases, so it disallows formation of a situation where it would have to. We consider this a bug to be fixed in future releases, not a feature.

Source

EDIT:

So swift is now open source and according to this changelog it is fixed now in snapshots of swift 2.2

Designated class initializers declared as failable or throwing may now return nil or throw an error, respectively, before the object has been fully initialized.

Duchess answered 21/10, 2014 at 22:8 Comment(3)
Thank's for addressing my point that the idea of initializing properties that won't be of any need any more seems not very reasonable. And +1 for sharing a source, which proves that Chris Lattner feels like I do ;).Luxuriance
FYI: "Indeed. This is still something we'd like to improve, but didn't make the cut for Swift 1.2". - Chris Lattner 10. Feb 2015Hairdo
FYI: In Swift 2.0 beta 2 this is still an issue, and it is also an issue with an initializer that throws.Charged
M
72

Update: From the Swift 2.2 Change Log (released March 21, 2016):

Designated class initializers declared as failable or throwing may now return nil or throw an error, respectively, before the object has been fully initialized.


For Swift 2.1 and earlier:

According to Apple's documentation (and your compiler error), a class must initialize all its stored properties before returning nil from a failable initializer:

For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place.

Note: It actually works fine for structures and enumerations, just not classes.

The suggested way to handle stored properties that can't be initialized before the initializer fails is to declare them as implicitly unwrapped optionals.

Example from the docs:

class Product {
    let name: String!
    init?(name: String) {
        if name.isEmpty { return nil }
        self.name = name
    }
}

In the example above, the name property of the Product class is defined as having an implicitly unwrapped optional string type (String!). Because it is of an optional type, this means that the name property has a default value of nil before it is assigned a specific value during initialization. This default value of nil in turn means that all of the properties introduced by the Product class have a valid initial value. As a result, the failable initializer for Product can trigger an initialization failure at the start of the initializer if it is passed an empty string, before assigning a specific value to the name property within the initializer.

In your case, however, simply defining userName as a String! does not fix the compile error because you still need to worry about initializing the properties on your base class, NSObject. Luckily, with userName defined as a String!, you can actually call super.init() before you return nil which will init your NSObject base class and fix the compile error.

class User: NSObject {

    let userName: String!
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: NSDictionary) {
        super.init()

        if let value = dictionary["user_name"] as? String {
            self.userName = value
        }
        else {
            return nil
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            self.isSuperUser = value
        }

        self.someDetails = dictionary["some_details"] as? Array
    }
}
Musetta answered 21/10, 2014 at 20:48 Comment(5)
Thank you very much not only right, but also well explainedLuxuriance
in swift1.2, Example from the docs make an error "All stored properties of a class instance must be initialized before returning nil from an initializer"Laoighis
@Laoighis That's correct, the example from the documentation (Product class) can not trigger a initialization failure before assigning a specific value, even though the docs say it can. The docs are not in sync with the latest Swift version. It is advised to make it a var for now instead let. source: Chris Lattner.Sharasharai
The documentation has this piece of code little bit different: you first set the property, and then check if it's present. See “Failable Initializers for Classes”, “The Swift Programming Language.” ``` class Product { let name: String! init?(name: String) { self.name = name if name.isEmpty { return nil } } } ```Canada
I read this too in the Apple docs but I fail to see why this would be required. A failure would mean returning nil anyway, what does it matter then whether the properties have been initialized?Electronegative
U
7

I accept that Mike S's answer is Apple's recommendation, but I don't think it's best practice. The whole point of a strong type system is to move runtime errors to compile time. This "solution" defeats that purpose. IMHO, better would be to go ahead and initialize the username to "" and then check it after the super.init(). If blank userNames are allowed, then set a flag.

class User: NSObject {
    let userName: String = ""
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init?(dictionary: [String: AnyObject]) {
        if let user_name = dictionary["user_name"] as? String {
            userName = user_name
        }

        if let value: Bool = dictionary["super_user"] as? Bool {
            isSuperUser = value
        }

        someDetails = dictionary["some_details"] as? Array

        super.init()

        if userName.isEmpty {
            return nil
        }
    }
}
Undertone answered 21/10, 2014 at 21:50 Comment(3)
Thank you, but I do not see how the ideas of strong type systems are corrupted by Mike's answer. All in all you present the same solution with the difference that the initial value is set to "" instead of nil. Moreover, you code takes away to use "" as a username (which might seem quite academically, but at least it's different from being not set in the json/dictionary)Luxuriance
Upon review, I see that you are right, but only because userName is a constant. If it was a variable, then the accepted answer would be worse than mine because userName could be later set to nil.Undertone
I like this answer. @KaiHuppmann, if you want to allow empty user names, you could also just have a simple Bool needsReturnNil. If the value does not exist in the dictionary, set needsReturnNil to true and set userName to whatever. After super.init(), check needsReturnNil and return nil if necessary.Idler
T
6

Another way to circumvent the limitation is to work with a class-functions to do the initialisation. You might even want to move that function to an extension:

class User: NSObject {

    let username: String
    let isSuperUser: Bool
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {

         self.userName = userName
         self.isSuperUser = isSuperUser
         self.someDetails = someDetails

         super.init()
    }
}

extension User {

    class func fromDictionary(dictionary: NSDictionary) -> User? {

        if let username: String = dictionary["user_name"] as? String {

            let isSuperUser = (dictionary["super_user"] as? Bool) ?? false
            let someDetails = dictionary["some_details"] as? [String]

            return User(username: username, isSuperUser: isSuperUser, someDetails: someDetails)
        }

        return nil
    }
}

Using it would become:

if let user = User.fromDictionary(someDict) {

     // Party hard
}
Thrush answered 13/7, 2015 at 10:23 Comment(1)
I like this; I prefer constructors be transparent about what they want, and passing in a dictionary is very opaque.Vanhouten
L
3

Although Swift 2.2 has been released and you no longer have to fully initialize the object before failing the initializer, you need to hold your horses until https://bugs.swift.org/browse/SR-704 is fixed.

Lenwood answered 22/3, 2016 at 22:17 Comment(0)
C
1

I found out this can be done in Swift 1.2

There are some conditions:

  • Required properties should be declared as implicitly unwrapped optionals
  • Assign a value to your required properties exactly once. This value may be nil.
  • Then call super.init() if your class is inheriting from another class.
  • After all your required properties have been assigned a value, check if their value is as expected. If not, return nil.

Example:

class ClassName: NSObject {

    let property: String!

    init?(propertyValue: String?) {

        self.property = propertyValue

        super.init()

        if self.property == nil {
            return nil
        }
    }
}
Corkscrew answered 6/8, 2015 at 8:18 Comment(0)
G
0

A failable initializer for a value type (that is, a structure or enumeration) can trigger an initialization failure at any point within its initializer implementation

For classes, however, a failable initializer can trigger an initialization failure only after all stored properties introduced by that class have been set to an initial value and any initializer delegation has taken place.

Excerpt From: Apple Inc. “The Swift Programming Language.” iBooks. https://itun.es/sg/jEUH0.l

Gratify answered 23/3, 2015 at 1:20 Comment(0)
J
0

You can use convenience init:

class User: NSObject {
    let userName: String
    let isSuperUser: Bool = false
    let someDetails: [String]?

    init(userName: String, isSuperUser: Bool, someDetails: [String]?) {
        self.userName = userName
        self.isSuperUser = isSuperUser
        self.someDetails = someDetails
    }     

    convenience init? (dict: NSDictionary) {            
       guard let userName = dictionary["user_name"] as? String else { return nil }
       guard let isSuperUser = dictionary["super_user"] as? Bool else { return nil }
       guard let someDetails = dictionary["some_details"] as? [String] else { return nil }

       self.init(userName: userName, isSuperUser: isSuperUser, someDetails: someDetails)
    } 
}
Japanese answered 9/3, 2016 at 5:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.