Issue with overridden properties / functions for generic constraints during initialization

Hey folks,

when I try to access a computed property during init(), Swift always executes the "default" implementation instead of the constrained version. When I access the property on an already initialized instance, the (imho) "correct" version gets called.

The same happens when defining check as a function as opposed to a computed property.

Is this working as designed? How would I ensure that the constrained version gets called during init?

Reproducible example: (copy to any playground)

struct NonEquatableStruct {
    var a: Int
    init() {
        a = 10
    }
}

struct EquatableStruct: Equatable {
    var a: Int
    init() {
        a = 10
    }
}

class EquatableClass: Equatable {
    static func == (lhs: EquatableClass, rhs: EquatableClass) -> Bool {
        return lhs.a == rhs.a
    }
    var a: Int
    init() {
        a = 10
    }
}

class Checker<C> {
    init() {
        print("[\(C.self)]")
        print("--- Checking in init ---")
        print(check)
    }

    var check: Bool {
        print("Default; neither Equatable nor AnyObject")
        return false
    }
}

extension Checker where C: Equatable {
    var check: Bool {
        print("Equatable")
        return true
    }
}

extension Checker where C: AnyObject {
    var check: Bool {
        print("AnyObject")
        return true
    }
}

extension Checker where C: AnyObject, C: Equatable {
    var check: Bool {
        print("AnyObject and Equatable")
        return true
    }
}

let checkNonEquatable = Checker<NonEquatableStruct>()
print("--- Accessor: ---")
print(checkNonEquatable.check, "\n")

let checkEquatable = Checker<EquatableStruct>()
print("--- Accessor: ---")
print(checkEquatable.check, "\n")

let checkEquatableClass = Checker<EquatableClass>()
print("--- Accessor: ---")
print(checkEquatableClass.check, "\n")

Console output:

[NonEquatableStruct]
--- Checking in init ---
Default; neither Equatable nor AnyObject
false
--- Accessor: ---
Default; neither Equatable nor AnyObject
false 

[EquatableStruct]
--- Checking in init ---
Default; neither Equatable nor AnyObject
false
--- Accessor: ---
Equatable
true 

[EquatableClass]
--- Checking in init ---
Default; neither Equatable nor AnyObject
false
--- Accessor: ---
AnyObject and Equatable
true 

Expected behaviour:

The same accessor implementation gets called both during and after initialization.

It is working as designed. You have not overridden any methods; rather, you are creating multiple methods with the same name that shadow each other.

Your initializer is for all possible generic constraints; therefore, it calls the method named “check” that is most specific, which in this case is the one with no constraints.

As you have structured the code, there is no way to get the behavior that you want. You will need to have actual overrides, which requires dynamic dispatch, which in Swift means protocol requirements or non-final class methods.

1 Like

I am trying to understand this, but it seems I'm at a loss here: When I implement a protocol Checkable requiring an implementation of var check: Bool { get } in combination with a protocol extension serving as default implementation, only the default implementation gets called during init. What would your suggested solution look like?

protocol Checkable {
    var check: Bool { get }
}

extension Checkable {
    var check: Bool {
        print("Protocol extension")
        return false
    }
}

class Checker<C>: Checkable {
    init() {
        print("[\(C.self)]")
        print("--- Checking in init ---")
        print(check)
    }
}

// ... rest as above

As I wrote, the way you have structured the code, there is no way to get the behavior you want.

In Swift, a type can conform to a protocol in only one way. When you conform Checker<C> to Checkable, any call to check is dynamically dispatched to the one implementation that satisfies the protocol requirement. In this case, you provided no such implementation, so the default implementation is called.

The remaining methods named check are not overrides, but merely methods that have the same name which shadow the protocol requirement. You can demonstrate this for yourself because removing the default implementation causes your code not to compile, because there would be no method that satisfies the protocol requirement.

If you want to use protocols to get the behavior you want, then you will need to have distinct conforming types for each distinct implementation of check.

1 Like