Still can not sleep, trying to comprehend the concept of covariance and contravariance? Feel how they breathe in your back, but when you turn around you find nothing? There is a solution!
My name is Nikita, and today we will try to make the mechanism in the head work correctly. You will find the most accessible consideration of the topic of variation in the examples. Welcome under cat.
The variation in this post is understood without regard to any programming language. The examples in the practice section are written in pseudo-language (it miraculously turned out to be similar to C #) and therefore do not have to be compiled by your favorite compiler. Let's get started
In the documentation, technical literature and other sources you could meet with various names for variance phenomena. No need to get scared and confused.
The terms covariance and covariance are equivalent (at least in programming). Moreover, the terms contravariance and contravariance are also equivalent. For example, the terms covariance and contravariance are used in Wikipedia and in Troelsen (in translation). And the terms covariance and contravariance are found, for example, on MSDN and in Skit (in translation).
In English, everything is easier - covariance and contravariance.
Variance is a transfer of the inheritance of the original types to the types derived from them. Derived types are containers, delegates, generalizations, and not types associated by ancestor-child relationships. Various types of variation are covariance, contravariance and invariance.
Covariance is a transfer of the inheritance of the original types to the types derived from them in direct order.
Contravariance is a transfer of the inheritance of the original types to the types derived from them in the reverse order.
Invariance is a situation when inheritance of initial types is not transferred to derivatives.
If derived types have covariance, they are said to be covariant to the original type. If derivative types show contravariance, they are said to be contravariant to the original type. If the derived types have neither the one nor the other, they are said to be invariant.
That's all you need to know. Of course, for those who first encounter variation, it is difficult to grasp. Therefore, we consider specific examples.
The whole point of variation is the use of inheritance advantages in derived types. It is known that if two types are connected by a parent-child relationship, then the child object can be stored in an ancestor-type variable. In practice, this means that we can use descendant objects for any operations instead of ancestor objects. Thus, you can write more flexible and short code to perform actions supported by different descendants with a common ancestor.
To begin with we will describe type hierarchy with which we will operate. At the top of the hierarchy we have a Device
(device), the descendants of which are Mouse
(mouse), Keyboard
(keyboard). Mouse
in turn, also has descendants - WiredMouse
(wired mouse), WirelessMouse
(wireless mouse).
Everyone loves containers. Using their example, it is easiest to explain what is meant by derived types. If we talk about lists as derived types, then for the type Device
derivative will be
List<Device>
(list of devices). Similarly, for the Keyboard
type, the derivative is List<Keyboard>
(the list of keyboards). I think if there were doubts, now they are not.
Covariance is also easier to study using containers. To do this, select the part of the hierarchy (branch) - Keyboard : Device
(the keyboard is a device, the keyboard is a special case of the device). Again, take lists and build a covariant derivative branch - List<Keyboard> : List<Device>
(the list of keyboards is a special case of the list of devices). As you can see, inheritance passed in direct order.
Consider the sample code. There is a function that accepts the List<Device>
list and performs some manipulations on them. As you have already guessed, you can transfer the List<Keyboard>
keyboards to this function:
void DoSmthWithDevices(List<Device> devices) { /* */ } ... List<Keyboard> keyboards = new List<Keyboard> { /* */ }; DoSmthWithDevices(keyboards);
Canonical for the study of contravariance is its consideration on the basis of delegates. Suppose we have a generic delegate:
delegate void Action<T>(T something);
For the original Device
type, the derivative will be Action<Device>
, and for the Keyboard
, Action<Keyboard>
. Received delegates can represent functions that perform some actions on the device or mouse, respectively. For the Keyboard : Device
branch, we construct a derivative contravariant branch - Action<Device> : Action<Keyboard>
(the action on the device is a special case of the action on the keyboard - it sounds strange, but it is). If you can press a key on the keyboard, this does not mean that you can press it on the device (it may not have a clue about what a key is). But if you can connect the device, you can connect the keyboard using the same method (method, function). As you can see, inheritance is transferred in the reverse order.
From the above, it is logical that if a function can perform something on the device, then it can also perform it on the keyboard. This means we can pass the delegate object Action<Device>
to the function that accepts the delegate object Action<Keyboard>
. Consider the code:
void DoSmthWithKeyboard(Action<Keyboard> actionWithKeyboard) { /* actionWithKeyboard */ } ... Action<Device> actionWithDevice = device => device.PlugIn(); DoSmthWithKeyboard(actionWithDevice);
If the derived types are invariant to the original types, then neither a covariant ( List<Keyboard> : List<Device>
) nor a contravariant ( Action<Device> : Action<Keyboard>
) branch is formed for the Keyboard : Device
branch. This means that there is no connection between derived types. As you can see, inheritance is not transferred.
Delegates of type Action<T>
may be covariant. This means that for the Keyboard : Device
branch, a covariant branch is formed - Action<Keyboard> : Action<Device>
. Thus, in the function that accepts the object delegate Action<Device>
, you can pass the object delegate Action<Keyboard>
.
void DoSmthWithDevice(Action<Device> actionWithDevice) { /* actionWithDevice */ } ... Action<Keyboard> actionWithKeyboard = keyboard => ((Device)keyboard).PlugIn(); DoSmthWithDevice(actionWithKeyboard);
Containers can be contravariant. This means that for the Keyboard : Device
branch a contravariant branch is formed - List<Device> : List<Keyboard>
. Thus, in a function that accepts List<Keyboard>
, you can pass List<Device>
:
void FillListWithKeyboards(List<Keyboard> keyboards) { /* */ } ... List<Devices> devices = new List<Devices>(); FillListWithKeyboards(devices);
The above exotic types of variation have, perhaps, academic value. It is difficult to come up with a real task, which is easier to solve with these kinds of opportunities. It is worth remembering that covariance and contravariance can cause run-time errors. To eliminate them, certain restrictions are required. Compilers, as a rule, do not impose such restrictions.
If the derived type is covariant, then for security, the container must be read only. Otherwise, it remains possible to write to the List<Keyboard>
an object of the wrong type ( Device
, Mouse
, etc.) by casting to the List<Device>
:
List<Device> devices = new List<Keyboard>(); devices.Add(new Device()); //
If the derived type is contravariant, then for security, the container must be write only. Otherwise, it remains possible to read from the List<Device>
object of the wrong type ( Keyboard
, Mouse
and others) by casting it to the appropriate list ( List<Keyboard>
, List<Mouse>
and others):
List<Keyboard> keyboards = new List<Device>(); keyboards.Add(new Keyboard()); keyboards[0].PressSpace(); //
For delegates, the covariance for the output value and contravariance for the input parameters (excluding transmission by reference) are reasonable for the delegates. If these conditions are met, run-time errors do not occur.
The examples presented are sufficient to understand the principles of how variation works. For information on its support for different types of your favorite language, look for the corresponding specification. If something went wrong - close your eyes, exhale and drink tea. After that, try again. Thanks for attention.
Perhaps a more correct definition of variation is proposed by Eric Lippert. Thanks to Alex_sik for the link to the article .
Compatibility assignment, assignment compatibility is the ability to assign a value of a more particular type to a compatible variable of a more general type.
Variance is the preservation of the compatibility of the assignment of the original types of derived types.
Covariance is the preservation of the compatibility of the assignment of the original types of derivatives in the direct order.
Contravariance is the preservation of the compatibility of the assignment of the original types of derivatives in the reverse order.
Source: https://habr.com/ru/post/218753/