I'm trying to understand why the where clause of a generic method is ignored
I've made a simple use case in Swift 3 (you can copy the code in a playground if you wanna fiddle with it):
//MARK: - Classes
protocol HasChildren {
var children:[Human] {get}
}
class Human {}
class SeniorHuman : Human, HasChildren {
var children: [Human] {
return [AdultHuman(), AdultHuman()]
}
}
class AdultHuman : Human, HasChildren {
var children: [Human] {
return [YoungHuman(), YoungHuman(), YoungHuman()]
}
}
class YoungHuman : Human {}
//MARK: - Generic Methods
/// This method should only be called for YoungHuman
func sayHelloToFamily<T: Human>(of human:T) {
print("Hello \(human). You have no children. But do you conform to protocol? \(human is HasChildren)")
}
/// This method should be called for SeniorHuman and AdultHuman, but not for YoungHuman...
func sayHelloToFamily<T: Human>(of human:T) where T: HasChildren {
print("Hello \(human). You have \(human.children.count) children, good for you!")
}
Ok, so now let's run some tests. If we have :
let senior = SeniorHuman()
let adult = AdultHuman()
print("Test #1")
sayHelloToFamily(of: senior)
print("Test #2")
sayHelloToFamily(of: adult)
if let seniorFirstChildren = senior.children.first {
print("Test #3")
sayHelloToFamily(of: seniorFirstChildren)
print("Test #4")
sayHelloToFamily(of: seniorFirstChildren as! AdultHuman)
}
The output is:
Test #1
Hello SeniorHuman. You have 2 children, good for you!
Test #2
Hello AdultHuman. You have 3 children, good for you!
Test #3
Hello AdultHuman. You have no children. But do you conform to protocol? true
//Well, why are you not calling the other method then?
Test #4
Hello AdultHuman. You have 3 children, good for you!
//Oh... it's working here... It seems that I just can't use supertyping
Well... apparently, for the where protocol clause to work, we need to pass a strong type that conforms to the protocol in its definition.
Just using supertyping is not enough, even if it is obvious in the test #3 that the given instance is actually conforming to the HasChildren protocol.
So, what am I missing here, is this just not possible? Do you have some links giving more information about what's going on, or more information about where clauses, or subtyping and its behavior in general?
I've read a few useful resources but none seems to have an exhaustive explanation about why it's not working:
A generic method may be overloaded like any other method. A class can provide two or more generic methods that specify the same method name but different method parameters.
To create an overload, create a new function within your class and use the same name. To differentiate, you need to have it accept a different number of parameters.
The overload definitions are a list of the different sets of arguments that can be used with a method. For example, the Substring method of System. String has two definitions: PS> 'thisString'.
The type of the method to be called is selected during compile time. What does the compiler know about your types?
if let seniorFirstChildren = senior.children.first {
seniorFirstChildren is Human because that's how children are declared. We have no information whether the child is adult or senior.
However, consider this:
if let seniorFirstChildren = senior.children.first as? AdultHuman {
Now the compiler knows seniorFirstChildren is AdultHuman and it will call the method you expect.
You have to distinguish between static types (types known during compilation) and dynamic types (types known at runtime).
From the Language Guide - Type Casting:
Checking Type
Use the type check operator (
is) to check whether an instance is of a certain subclass type. The type check operator returnstrueif the instance is of that subclass type andfalseif it is not.
The type check operator is is resolved at runtime, whereas the overload resolution of the call to sayHelloToFamily using the first children member of your instance of AdultHuman (binded to seniorFirstChildren) as argument is resolved at compile time (in which case it's typed as Human, which does not conform to HasChildren). If you explicitly tell the compiler that seniorFirstChildren is an AdultHuman instance (using the unsafe as! AdultHuman), then naturally the compiler will make use of this information to choose the more specific overload.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With