Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Constant property cannot be initialized in a convenience initializer of a derived class

Below is sample code from a playground. What I don't understand is why the b variable in the subclass must be a var type and cannot be a let. Can someone help me understand?

class Base1 {
    init() { }
}

class Sub1: Base1 {
    let b: Int

    override init() {
        super.init()
    }

    convenience init(b: Int) {
        self.b = b  // Cannot assign to property: 'b' is a 'let' constant
        self.init()
    }
}
like image 956
Aaron Bratcher Avatar asked Nov 21 '17 13:11

Aaron Bratcher


3 Answers

In Swift, designated initializers are used to set up all the stored properties. This is from the official documentation:

Designated initializers are the primary initializers for a class. A designated initializer fully initializes all properties introduced by that class and calls an appropriate superclass initializer to continue the initialization process up the superclass chain.

Convenience initializer are secondary, and needs to call desginated initializer.

You can also define a convenience initializer to create an instance of that class for a specific use case or input value type. Create convenience initializers whenever a shortcut to a common initialization pattern will save time or make initialization of the class clearer in intent.

When you are trying your code, although the error message is not clear at this point, the issue is that you cannot use convenience init to initialize a stored property. You have to make b optional var to make it work.

class Sub1: Base1 {
    var b: Int?

    override init() {
        super.init()
    }

    convenience init(b: Int) {
        self.init()
        self.b = b
    }
}

Or better way is, as @Martin R suggested in one of the comments, to create another designated initializer and use convenience before overridden init, to give a default value:

class Sub1: Base1 {
    let b: Int

    init(b: Int) {
        self.b = b
        super.init()
    }

    convenience override init() {
        self.init(b:5)
    }
}
like image 128
Puneet Sharma Avatar answered Oct 27 '22 08:10

Puneet Sharma


I think this has less to do with subclassing and more to do with the class initializers.

The error only tells half the story, changing b from let to var would show other problems with this class:

  • Initializers must set an initial value for each stored property of the class. If you override init, you must provide a default value for b.

  • A convenience initializer must call a designated initializer before accessing self

This is how it looks (thanks to Martin R for the improvements suggested in the comments on how to maintain b as a let constant):

class Base1 {
    init() { }
}

class Sub1: Base1 {
    let b: Int

    convenience override init() {
        self.init(b: 5)
    }

    init(b: Int) {
        self.b = b
        super.init()
    }
}

let one = Sub1(b: 10)
one.b   // prints 10
let two = Sub1()
two.b   // prints 5
like image 31
paulvs Avatar answered Oct 27 '22 10:10

paulvs


First of all, imho, this has nothing with derived class, you could remove you subclassing and get the similar error.

Second, I think it is due to the "nature" of convenience initializers and their usage. They are "optional" (in usage) initializers, for example to provide shortcut way to initialize from some complex data (like other class or structure). Adding initialization of self.b to convenience init in your sample wouldn't bring anything helpful, as you anyway would need to initialize b in designated init:

class Sub1: Base1 {
    let b: Int

    // You must init b
    // Or ger error: property 'self.b' not initialized ...
   init(b: Int) {
        self.b = b
        super.init()
    }

    // Do you really need the code below now?
    //convenience init(b: Int) {
    //    self.b = b
    //    self.init()
    //}
}

Thus as designated init must initialize b, the convenience initializer as your wrote it becomes meaningless.

Third, assume it would be allowed. So, as convenience initializer are delegated across not necessarily calling the designated one. Thus it would be possible to do something like that:

...

convenience init(b: Int) {
    self.b = b
    self.init(c: 10)
}

// later some crazy guy adds his own "convenience" initializer and points your to this one.
convenience init(c: Int) {
    self.c = c
    if c == 7 {
       self.b = 11
    }
    self.init()
}

Now imagine that you are the "compiler" and tell me where the constant b is set to it's constant value and what value?

Finally, in my understanding the correct usage could be like that:

class Base1 {
    init() {}
}


class Sub1: Base1 {
    let b: Int

    init(b: Int) {
        self.b = b
        super.init()
    }

    // Convenience initializer providing default value
    override convenience init() {
        self.init(b: 7)
    }
}

So, my concern is that I don't understand clearly what you really wanted to achieve by allowing let b initialized in convenience init?

like image 45
Mikhail Churbanov Avatar answered Oct 27 '22 10:10

Mikhail Churbanov