Swift 5.2 Generics - Adding further type constraints in subclass causes build error now?

EDIT: This is expected behavior called out in the Xcode 11.4 release notes and not the Swift release post. Next time I'll remember Xcode release notes are more comprehensive :sweat_smile:

I've created a simplified example of something that has worked for a while. Trying to run this code in Xcode 11.4 now causes a build error.

Example, builds in Xcode 11.3.1 and earlier:

protocol A {}
protocol B {}

protocol AA: A {}
protocol BB: B {}

public class AClass: AA {
    public init() {}

class BClass: BB {
    public init() {}

class ThingDoer<Start: A, End: B> {
    public init() {}
    func doThing<Start: A, End: B>(arg1: Start, arg2: End) {}

class SubThingDoer: ThingDoer<AClass, BClass> {
    override func doThing<Start: AA, End: BB>(arg1: Start, arg2: End) { // error here

SubThingDoer().doThing(arg1: AClass(), arg2: BClass())

Unfortunately this now causes the error:

Overridden method 'doThing' has generic signature <Start, End where Start : AA, End : BB> which is incompatible with base method's generic signature <Start, End, Start, End where Start : A, End : B, Start : A, End : B>; expected generic signature to be <Start, End where Start : A, End : B>

Can we no longer tighten type constraints in subclass methods in Swift 5.2, or is this a bug? We have a lot of code that relies on this pattern. :confused:

I don't see how this could have ever worked.

Remember that the caller gets to choose any type that meets the generic constraints. In this case, for instance, the caller can call doThing choosing any Start that conforms to A. You can't override that with a doThing implementation that doesn't allow the caller to choose a Start that conforms to A but not AA.

Incidentally, I don't know if you meant to do it intentionally, but ThingDoer.Start is being shadowed by a generic parameter Start. I wouldn't name these two things the same thing, as that's inviting confusion.

I agree with you that the pattern used here is tough to understand, it took me a while to formulate this anonymized example. :slight_smile:

Yes, the shadowed generic param is intentional in this example (unfortunately).

The overriden doThing that specifies AA, which inherits from A. I could be misreading you, but I think you've got the types reversed?

No, I mean it precisely as-is. You can't override a method that takes an instance of any type that conforms to A with a method that only allows an instance of a type that conforms to AA. This is because a user can choose a type that conforms to A but not AA.

Actually, looks like I missed the Xcode 11.4 release notes. This is explicitly called out as not allowed. I had read the Swift 5.2 release post which does not mention this behavior.

I’d also suggest that you rename Start End, the class, to be different from Start, End, the function. Unless one is meant to be shadowing another, it’s already confusing just by the signature alone.

Remember that your SubThingDoer is in fact also a ThingDoer.

let doer: ThingDoer = SubThingDoer()
let a: A = ...
let b: B = ...
doer.doThing(arg1: a, arg2: b)

This second line should work for any a and b conforming to A and B even though they don’t conform to the stricter AA and BB.

This is because your sub doer, when treated as a (non-sub) doer, should accept the less strict arguments. What then? Should the super class’ implementation be called instead of the subclass’?

I guess that could make sense, but would probably require some kind of partial override to tell both the compiler as well as humans reading your code, that your subclass is only partly overriding the function for a subset of arguments. Such a feature is not available in Swift.