I'm very new to Swift but I have some experience with OO-programming. I've started to try and use parameterized classes in Swift and I have come across a strange design feature when overloading methods. If I define the following classes:
class ParameterClassA {
}
class ParameterClassB: ParameterClassA {
}
class WorkingClassA<T: ParameterClassA> {
func someFunction(param: T) -> Void {
}
}
class WorkingClassB: WorkingClassA<ParameterClassB> {
override func someFunction(param: ParameterClassA) {
}
}
Then the code compiles fine. However, as you'll notice, I've overloaded the function that normally uses the parameter type, which in my example is ParameterClassB
, and given it a parameter of type ParameterClassA
. How is that supposed to work? I know that it's not allowed in Java, and I'm wondering how the type parameter is interpreted. Can it be anything from the class hierarchy of the type parameter?
Also note that the problem is exactly the same if I remove the type parameter constraint : ParameterClassA
in WorkingClassA
.
If I remove the override
keyword, then I get a compiler error requesting that I add it.
Thanks a lot for any explanation!
It has nothing at all to do with the generics (what you call "parameterized"). It has to do with how one function type is substitutable for another in Swift. The rules is that function types are contravariant with respect to their parameter types.
To see this more clearly, it will help to throw away all the misleading generic stuff and the override stuff, and instead concentrate directly on the business of substituting one function type for another:
class A {}
class B:A {}
class C:B {}
func fA (x:A) {}
func fB (x:B) {}
func fC (x:C) {}
func f(_ fparam : B -> Void) {}
let result1 = f(fB) // ok
let result2 = f(fA) // ok!
// let result3 = f(fC) // not ok
We are expected to pass to the function f
as its first parameter a function of type B -> Void
, but a function of type A -> Void
is acceptable instead, where A is superclass of B.
But a function of type C -> Void
is not acceptable, where C is subclass of B. Functions are contravariant, not covariant, on their parameter types.
@matt is completely right about why this works – it's due to the fact that method inputs are contravariant, instead of covariant.
This means that you can only override a given function with another function that has a broader (or the same) input type – meaning that you can substitute in a superclass argument in place of a subclass argument. This may seem completely backwards at first – but it makes total sense if you think about it a bit.
The way I would explain it in your situation is with a slightly stripped down version of your code:
class ParameterClassA {}
class ParameterClassB: ParameterClassA {}
class WorkingClassA {
func someFunction(param: ParameterClassB) {}
}
class WorkingClassB: WorkingClassA {
override func someFunction(param: ParameterClassA) {}
}
Note here that WorkingClassB
is overriding the someFunction
with ParameterClassA
– the superclass of ParameterClassB
.
Now, imagine you have an instance of WorkingClassA
, and then call someFunction
on this instance, with an instance of ParameterClassB
:
let workingInstanceA = WorkingClassA()
workingInstanceA.someFunction(ParameterClassB()) // expects ParameterClassB
So far, nothing unusual. We passed a ParameterClassB
instance to a function that expects a ParameterClassB
.
Now let's assume that you swap your WorkingClassA
instance with a WorkingClassB
instance. This is perfectly legal in OOP – as the subclass can do everything the superclass could do.
let workingInstanceB = WorkingClassB()
workingInstanceB.someFunction(ParameterClassB()) // expects ParameterClassA
So what happens now? We're still passing a ParameterClassB
instance into the function. However, now the function expects a ParameterClassA
instance. Passing a subclass into an argument that expects a superclass is legal in OOP (this is covariance), as the subclass can do everything that the superclass could do – therefore this doesn't break anything.
Because the function signature can only get more broad (or remain unchanged) as you override it, it ensures that you can always pass it the original argument type that the function defined, as any superclass argument in an overridden version can accept it.
If you think about the reverse for a second, you'll see why it couldn't possibly work. As the function would get more restrictive as it gets overridden, it won't be able to accept arguments that the superclass could originally accept – therefore it cannot work.