Why can't we call superclass convenience initializer from subclass?

I am subclassing UIKit class and I would like to be able to call it's convenience initializer (because it does things I can't do myself) and then do additional step related to my subclass only. I can't do that because I can only call designated initializers from a superclass.

What is the reason for this limitation? Documentation only says we can't do it but doesn't go into detail about why. Is it likely to change in future?

I think it's to establish a hierarchical call chain and avoid infinite loops.
imagine a convenience init calling a designated init that calls the convenience init again. That would be catastrophic, to avoid that convenience can call required and not viceversa.

That's my guess, I might be wrong.

That makes sense, not allowing designated initializer to call convenience is ok. My question is why I can't call superclass convenience from subclass convenience. I can't see any problems with chaining and loops in this scenario.

a subclass might have properties that the superclass doesn't. Calling a super convenience does not explicitly guarantee that all properties will be initialised as needed. For this reason, a convenience must call a designated initialiser of the current subclass.

That's good point. In my case there is no subclass designated initializer so I don't need to initialize any additional stuff myself (not sure what exactly happens under the hood).

This relationship is enforced so that all variables are always initialised. The fact that your subclass doesn't have additional vars cannot overrule this.

That doesn't make sense. Swift already force you to initialize all properties before calling super designated initializer. I don't see convenience initializer make any difference.

convenience do not call super. Convenience must call designated initializers. In convenience init you are not enforced to set all variables. that's an enforcement of the designated initializer (and that's the difference between the two).
Now, given the fact that a convenience is not enforced to init all parameters, giving the ability to call super in them will potentially cause some parameters to not be initialized.
If you enforce convenience to have all params set, then they are designated initializers and no longer convenience, by definition.
Please lower the level of your tones. "That doesn't make sense" to you. It perfectly makes sense to the rest of the dev community. If you have a better proposal, swift is open source and contributions are Wellcome.

Sorry for the word. but I’m talking about why designated initializer can not call super convenience, which will call super designated after all.

class MyButton: NSButton {
    
    /* designated */ init() {
        // initialize my properties
        super.init(title: "Button", target: nil, action: nil)
        // error: must call a designated initializer of the superclass 'NSButton'
    }
}
1 Like

How can Swift be sure that's the case? After all, if your subclass is calling super's convenience init instead of a designated init, why couldn't the superclass be doing the same?

Because convenience initializer must call designated initializer.

According to swift language guide, we have following rules:
Rule 1: A designated initializer must call a designated initializer from its immediate superclass.
Rule 2: A convenience initializer must call another initializer from the same class.
Rule 3: A convenience initializer must ultimately call a designated initializer.

Rule 2 and 3 are reasonable to me. I just don’t understand the designated constraint in rule 1.

I think if there's any takeaway from this thread, it is that Swift's init rules are quite complicated ;-)

There's also the ability to inherit convenience initializers - but as long as you call super.init, those shouldn't be a problem either.
I'm curious for examples that demonstrate the need for rule 1 as well.

2 Likes

There is a much simpler set of rules lost somewhere behind all this current nonsense. Ignore the rules of both Objective-C and Swift for a second. The basic semantic rules for getting initialization right in the presence of inheritance are simple to understand:

  • A subobject initializer is responsible for initializing just an instance of the lexically-containing class and its base classes.

    A subobject initializer can either (1) delegate to another subobject initializer in the lexically-containing class or (2) initialize any stored properties of the lexically-containing class and then delegate to a subobject initializer of its superclass. No other kind of delegation is valid. In particular, you cannot delegate to an initializer provided by Self if that might be different from the current class.

    A subobject initializer cannot be inherited. You can have language features that synthesize a subobject initializer in a subclass from a subobject initializer in a superclass, but that's not the same as inheritance.

  • A complete object initializer is responsible for initializing an instance of Self, which may be an arbitrary subclass of the lexically-containing class.

    A complete object initializer can (1) delegate to any other complete-object initializer as long as the same Self type is preserved or (2) delegate to a subobject initializer of Self. No other kind of delegation is valid.

    There are no restrictions on how complete object initializers can be inherited.

Swift's required initializer feature is a mechanism for allowing both (1) construction of a class from an unknown type value and (2) Self-delegation in a complete-object initializer. Because the initializer invoked by the mechanism is always associated with the actual dynamic Self class, either a complete object initializer or a subobject initializer will suffice. (You can think of this as a special-case synthesis of a complete-object initializer from a subobject initializer if you like. Such synthesis is not generally valid, but it works for the required feature.)

2 Likes

I think the easiest way to get to that as a basis for Swift's initializer rules would be to say:

  • convenience initializers are always complete-object initializers. They have two choices: they can super.init(...) or self.init(...) to another convenience initializer, or they can self = Self(...) delegate to a required initializer. Note that self.init(...) would be a peer-delegation rather than a dispatch down to Self, which is a semantic change.

  • Other initializers are always subobject initializers. They can super.init(...) or self.init(...) to another subobject initializer and that's it.

  • convenience initializers are inherited unless the subclass provides an initializer with the same signature. Other initializers are never inherited.

  • required initializers can be satisfied by either kind of initializer.

  • We can continue to provide implicit (and ideally explicit) code-synthesis features for "inheriting" subobject initializers when all the stored properties have default initializers.

  • Most of these restrictions can be lifted in final classes.

  • We can consider weakening the annotation requirements in non-public initializers. I'm not sure this is a good idea, though.

  • If we wanted to add factory initializers, they would be uninherited complete-object initializers which, by virtue of not being inherited, would always be allowed to create a subclass of the lexically-containing class.

5 Likes

One other thing that might not be obvious to a Swift-only programmer is that convenience initializers call a designated initializer on the dynamic type, not the same class. This is a design Swift got from Objective-C, and it's what allows them to be inherited. (Other languages with self.init(…)-like features, like C++, don't have this dynamic behavior and therefore don't have a separate notion of "convenience initializers".)

6 Likes

Oh I see. With automatic initializer inheritance, a convenience initializer may call designated initializers from subclass:

class A {
    
    init(x: Int) {
        print("A.init(x:)")
    }
    
    convenience init(y: Int) {
        print("A.init(y:)")
        self.init(x: y)
    }
}

class B: A {
    
    override init(x: Int) {
        print("B.init(x:)")
        super.init(x: x)
    }
}

B(y: 0) // A.init(y:) -> B.init(x:) -> A.init(x:)
// Allowing `B.init(x:)` to call `A.init(y:)` will cause an infinite loop.

(That's quite complicated...I found the Initialization section is the longest section in Swift Language Guide, about 1.5 times longer than Generics section)

3 Likes

Indeed. If the compiler did not provide clear error messages, I think a lot of developers would be turned off. Luckily, I think most common cases "just work".

Yes you can. Since the superclass's convenience initializer became the subclass's. It will need to call your designated initializer, if it doesn't find one, you won't use it.

Add this to you subclass then it will work:

override init() {
    super.init()
        
}
Terms of Service

Privacy Policy

Cookie Policy