Covariant & Contravariant
While writing code in various languages, you must have been noticed by IDEs that the properties, functions, or parameters don’t support covariant
or contravariant.
I usually feel confused about them. They have a slight similarity. Today, I will use a simple example to explain it to you. Actually, it is not so difficult to understand as you think.
In my opinion, the covariant and the contravariant are the relationship between subtype and supertype. We declare three classes below:
1 | class Animal {} |
The inheritance relationship is Student < Person | Dog < Animal,
we say Person
is the subclass of Animal,
and Student
is the subclass of Person.
Then we define a function that accepts a function parameter.
1 | func execute(_ block: (Person) -> Person) {} |
The block takes in a Person
value and outputs a Person
value. If you want to invoke the execute
function, you should pass a (Person) -> Person
block to it by default. But we know that we can pass a subtype value into the function if the parameter type is simply a Person
type. Is (Student) -> Student
the subtype of (Person) -> Person
?
I list four situations for that:
(Animal) -> Animal
In the execute
function body, maybe passes a Person
or Student
value to the bloc. Our bloc takes in an Animal
parameter. Of course, the subtypes of it are also OK.
Our bock returns an Animal
value, and the execute function thinks it will get a Person
value via executing the block.
In the execute
function body, we use the Person
(actually is Animal) result to invoke the speak
method, but this usage is unsafe. The block might return a Dog
object, and the dog doesn’t have a speak
method. Invoking this method will lead to the app crash.
(Student) -> Student
(Student) -> Animal
Just like above, If we pass a Person
object to the bloc in the execute
function body, the block might use the parameter person to invoke the doHomework
method. But a normal Person
object doesn’t have the method. Passing these two blocs are also unsafe.
(Animal) -> Student
In the first case, we know the input parameter is Animal
is no problem.
If we invoke a Person
method to the result of the block in the execute
function body, the result type of the block is Student.
A student is certainly a person, so passing this bloc is safe.
Conclusion
As you can see, a function parameter, its input params can only take in superclass types(or same type). We term it contravariant.
its output params must be subclass types(or same type). We term it covariant.
In Swift, we can not really assign an (Animal) -> Student
object to a (Person) -> Person
variable.
1 | var v1: (Person) -> Person |
The only superclass type we can use is the
Any
type.
1 | var v1: (Person) -> Any |
Is List<Person>
the subclass of List<Animal>
?
This question is a bit complicated. Let’s see two examples.
1 | func execute(_ animals: [Animal]) { |
We can pass a [Person]
value to an [Animal]
variable. It looks like [Person]
is the subclass of [Animal].
If the animals is immutable, that’s right.
In another case, if the animals is mutable, we can append a dog to it, but its actual type is [Person],
insert a dog to a person list? So only an immutable array type can be a subtype of another array type. But the Swift doesn’t support this feature.
Thank you for reading my post. Leave a comment if you have any questions.