Covariance and contravariance

from Wikipedia, the free encyclopedia

In object-oriented programming , covariance and contravariance mean whether an aspect (i.e. a type declaration) is similar to the direction of inheritance (covariant) or opposite to it (contravariant). If there is no change in the subclass compared to the upper class , this is called invariance .

The terms are based on the principle of replaceability : objects of the superclass must be replaceable by objects of one of its subclasses. This means, for example, that the methods of the subclass must accept at least the parameters that the superclass would also accept (contravariance). The methods of the subclass must also return values ​​that are compatible with the superclass, i.e. are never of a more general type than the return type of the superclass (covariance).

Origin of the term

The terms contravariance and covariance are derived in object orientation from the fact that the types of the parameters under consideration relate to the inheritance hierarchy of replacement (covariant) or opposite to the inheritance hierarchy (contravariant).

Occurrence of variances

You can choose between co-, contra- and invariance at

  • Methods
    • Argument types (the types of parameters passed)
    • Result types (the types of the return value)
    • other signature extensions (e.g. exception types in the throws clause in Java )
  • generic class parameters

distinguish.

The substitution principle results in the following possible occurrences of variances in the inheritance hierarchy of object-oriented programming:

Contravariance Input parameters
Covariance Return value and exceptions
Invariance Input and output parameters

Covariance, contravariance and invariance

Covariance means that the type hierarchy has the same direction as the inheritance hierarchy of the classes to be considered. If you want to adapt an inherited method, the adaptation is covariant if the type of a method parameter in the superclass is a supertype of the parameter type of this method in the subclass.

If the type hierarchy runs in the opposite direction to the inheritance hierarchy of the classes to be considered, one speaks of contravariance. If the types in the upper and lower classes cannot be changed, one speaks of invariance.

In object-oriented modeling it is often desirable that the input parameters of methods are also covariant. However, this violates the substitution principle. The overloading is handled differently in this case by the various programming languages.

Example based on program code

Basically, in programming languages ​​like C ++ and C #, variables and parameters are contravariant, while method returns are covariant. Java, on the other hand, requires the novariance of the method parameters and variables, whereby the return parameter must be covariant:

Example in C #
Contravariance Covariance Invariance
public abstract class Animal
{
   public abstract string Name { get; }
}

public class Giraffe : Animal
{
   public Giraffe(string name)
   {
      Name = name;
   }
   public string Name { get; private set; }
}

public string GetNameFromAnimal(Animal animal)
{
   return animal.Name;
}

[Test]
public void Contravariance()
{
    var herby = new Giraffe("Herby");
    // kontravariante Umwandlung von Giraffe nach Animal
    var name = GetNameFromAnimal(herby);
    Assert.AreEqual("Herby", name);
}
public abstract class Animal
{
   public abstract string Name { get; }
}

public class Giraffe : Animal
{
   public Giraffe(string name)
   {
      Name = name;
   }
   public string Name { get; private set; }
}

public string GetNameFromGiraffe(Giraffe animal)
{
   return animal.Name;
}

[Test]
public void Covariance()
{
    var herby = new Giraffe("Herby");
    // kovariante Umwandlung des Rückgabewerts von String nach Object
    object name = GetNameFromGiraffe(herby);
    Assert.AreEqual((object)"Herby", name);
}
public abstract class Animal
{
   public abstract string Name { get; }
}

public class Giraffe : Animal
{
   public Giraffe(string name)
   {
      Name = name;
   }
   public string Name { get; private set; }
}

public string GetNameFromGiraffe(Giraffe animal)
{
   return animal.Name;
}

[Test]
public void Invariance()
{
    var herby = new Giraffe("Herby");
    // keine Umwandlung der Datentypen
    string name = GetNameFromGiraffe(herby);
    Assert.AreEqual("Herby", name);
}

Example based on illustrations

The following explains when type safety is guaranteed when you want to replace one function with another. This can then be transferred to methods in object orientation if methods are replaced by objects according to Liskov's principle of substitution.

Be and functions that have the following signature , for example :

, where and , and
, where and .

As you can see, it is a superset of , but a subset of . If the function is used instead of , then the input type C is called contravariant, the output type D covariant. In the example, the replacement can take place without a type violation, since the input of covers the entire range of the input of . It also provides results that do not exceed the value range of .

Correctness of contravariance and covariance

The UML notation is used as a model to represent the inheritance hierarchy:

                       Kontravarianz           Kovarianz             Invarianz
 ┌─────────┐         ┌───────────────┐     ┌───────────────┐     ┌───────────────┐
 │    T    │         │ ClassA        │     │ ClassA        │     │ ClassA        │
 ├─────────┤         ├───────────────┤     ├───────────────┤     ├───────────────┤
 │         │         │               │     │               │     │               │
 ├─────────┤         ├───────────────┤     ├───────────────┤     ├───────────────┤
 │         │         │ method(t':T') │     │ method():T    │     │ method(t :T&) │
 └─────────┘         └───────────────┘     └───────────────┘     └───────────────┘
      ↑                      ↑                     ↑                     ↑
 ┌─────────┐         ┌───────────────┐     ┌───────────────┐     ┌───────────────┐
 │    T'   │         │ ClassB        │     │ ClassB        │     │ ClassB        │
 ├─────────┤         ├───────────────┤     ├───────────────┤     ├───────────────┤
 │         │         │               │     │               │     │               │
 ├─────────┤         ├───────────────┤     ├───────────────┤     ├───────────────┤
 │         │         │ method(t :T ) │     │ method():T'   │     │ method(t :T&) │
 └─────────┘         └───────────────┘     └───────────────┘     └───────────────┘

Contravariance : The substitution principle is adhered to, because you can method (t: T) of Division ClassB so use as if it were the method of upper class ClassA .
Check: You can transfer a variable of a more special type
T ' to method (t: T) , because T' contains all information that is also in T due to inheritance .

Covariance : The substitution principle is adhered to, because method (): T 'of the subclass ClassB can be used as if it were the method of the superclass ClassA .
Check: The return value of the method from ClassB is T ' . You can transfer this value to a variable declared of type T , because T ' has all the information that is also in T due to inheritance .

Type safety in methods

Due to the properties of the substitution principle, static type safety is guaranteed if the argument types are contravariant and the result types are covariant.

Type-uncertain covariance

The covariance of the method parameters, which is often desirable in object-oriented modeling, is supported in many programming languages ​​despite the resulting type uncertainty.

An example of the type uncertainty of covariate method parameters can be found in the following classes Personand Arzt, and their specializations Kindand Kinderarzt. The parameter of the method untersuchein the class Kinderarztis a specialization of the parameter of the same method of Arztand therefore covariant .

Type-uncertain covariance - general
┌─────────┐         ┌───────────────┐
│    T    │         │ ClassA        │
├─────────┤         ├───────────────┤
│         │         │               │
├─────────┤         ├───────────────┤
│         │         │ method(t :T ) │
└─────────┘         └───────────────┘
     ↑                      ↑
┌─────────┐         ┌───────────────┐
│    T'   │         │ ClassB        │
├─────────┤         ├───────────────┤
│         │         │               │
├─────────┤         ├───────────────┤
│         │         │ method(t':T') │
└─────────┘         └───────────────┘
   Example of type-uncertain covariance
┌────────────────┐         ┌───────────────────────┐
│ Person         │         │ Arzt                  │
├────────────────┤         ├───────────────────────┤
│                │         │                       │
├────────────────┤         ├───────────────────────┤
│ stillHalten()  │         │ untersuche(p: Person) │
└────────────────┘         └───────────────────────┘
         ↑                             ↑
┌────────────────┐         ┌───────────────────────┐
│ Kind           │         │ Kinderarzt            │
├────────────────┤         ├───────────────────────┤
│                │         │                       │
├────────────────┤         ├───────────────────────┤
│ tapferSein()   │         │ untersuche(k: Kind)   │
└────────────────┘         └───────────────────────┘
The implementation of the example in Java looks like this: A program using the classes could look like this: The output is then:
   public class Person {
       protected String name;
       public String getName() { return name; }
       public Person(final String n) { name = n; }
       public void stillHalten() {
           System.out.println(name + " hält still");
       }
   }

   public class Kind extends Person {
       boolean tapfer = false;
       public Kind(final String n) {super(n); }
       public void stillHalten() {
           if(tapfer)
               System.out.println(name + " hält still");
           else
               System.out.println(name + " sagt AUA und wehrt sich");
       }
       public void tapferSein() {
           tapfer = true;
           System.out.println(name + " ist tapfer");
       }
   }

   public class Arzt extends Person {
       public Arzt(final String n) { super(n); }
       public void untersuche(Person person) {
           System.out.println(name + " untersucht " + person.getName());
           person.stillHalten();
       }
   }

   public class Kinderarzt extends Arzt {
       public Kinderarzt(final String n) { super(n); }
       public void untersuche(Kind kind) {
           System.out.println(name + " untersucht Kind " + kind.getName());
           kind.tapferSein();
           kind.stillHalten();
       }
   }
public class Main {
    public static void main(String[] args) {
       Arzt arzt = new Kinderarzt("Dr. Meier");
       Person person = new Person("Frau Müller");
       arzt.untersuche(person);
       Kind kind = new Kind("kleine Susi");
       arzt.untersuche(kind);
       // und jetzt RICHTIG
       Kinderarzt kinderarzt = new Kinderarzt("Dr. Schulze");
       kinderarzt.untersuche(person);
       kinderarzt.untersuche(kind);
    }
}
Dr. Meier untersucht Frau Müller
Frau Müller hält still
Dr. Meier untersucht kleine Susi
kleine Susi sagt AUA und wehrt sich
Dr. Schulze untersucht Frau Müller
Frau Müller hält still
Dr. Schulze untersucht Kind kleine Susi
kleine Susi ist tapfer
kleine Susi hält still

It is important that the object arztmust be correctly declared because a method is not overwritten here, but rather overloaded, and the process of overloading is tied to the static type of the object. You can see the result when you compare the expenses: Dr. Meier cannot examine children, Dr. Schulze, however, does.

The example works correctly in Java: The method untersucheof Arztis Kinderarztnot overwritten, but simply overloaded due to the different parameters, which means that the correct method is called in each case. When Arzt untersuchecalled, the method is always called there; however, when Kinderarzt untersuchecalled, it is called once untersucheat Arztand once at , depending on the type Kinderarzt. According to the language definition of Java, a method that is to be overwritten must have the same signature (in Java consisting of parameters + possible exceptions).


The same example can also be coded in Python, but note that parameters are not typed. The code would look like this:


#!/usr/bin/env python

class Person:
    def __init__(self,name):
        self.name = name
    def stillHalten(self):
        print(self.name, " hält still")

class Arzt(Person):
    def __init__(self,name):
        super().__init__(name)
    def untersuche(self,person):
        print(self.name, " untersucht ", person.name)
        person.stillHalten()

class Kind(Person):
    def __init__(self,name):
        super().__init__(name)
        self.tapfer = False
    def tapferSein(self):
        self.tapfer = True
        print(self.name, " ist jetzt tapfer")
    def stillHalten(self):
        if self.tapfer:
            print(self.name, " hält still")
        else:
            print(self.name, " sagt AUA und wehrt sich")

class Kinderarzt(Arzt):
    def __init__(self,name):
        super().__init__(name)
    def untersuche(self,person):
        print(self.name, " untersucht ", person.name)
        if isinstance(person,Kind):
            person.tapferSein()
        person.stillHalten()


if __name__ == "__main__":
    frMüller = Person("Frau Müller")
    drMeier = Arzt("Dr. Meier")
    drMeier.untersuche(frMüller)
    kleineSusi = Kind("kleine Susi")
    drMeier.untersuche(kleineSusi)
    drSchulze = Kinderarzt("Dr. Schulze")
    drSchulze.untersuche(frMüller)
    drSchulze.untersuche(kleineSusi)

Covariance on arrays

With array data types, covariance can cause a problem in languages ​​like C ++, Java, and C #, as they internally retain the data type even after the conversion:

Java C #
@Test (expected = ArrayStoreException.class)
public void ArrayCovariance()
{
    Giraffe[] giraffen = new Giraffe[10];
    Schlange alice = new Schlange("Alice");

    // Kovarianz (Typumwandlung in Vererbungsrichtung)
    Tier[] tiere = giraffen;

    // führt zur Laufzeit zu einer Ausnahme,
    // da das Array intern vom Typ Giraffe ist
    tiere[0] = alice;
}
[Test, ExpectedException(typeof(ArrayTypeMismatchException))]
public void ArrayCovariance()
{
    var giraffen = new Giraffe[10];
    var alice = new Schlange("Alice");

    // Kovarianz
    Tier[] tiere = giraffen;

    // Ausnahme zur Laufzeit
    tiere[0] = alice;
}

To avoid such runtime errors, generic data types can be used that do not offer any modifying methods. The interface IEnumerable<T>implemented by the array data type, among other things , is often used in C # . Since a IEnumerable<Tier>cannot be changed, z. For example, a new instance can be created from LINQ using extension methods to accommodate the element . alice

[Test]
public void ArrayCovariance()
{
    var giraffen = new Giraffe[10];
    var alice = new Schlange("Alice");

    IEnumerable<Tier> tiere = new Tier[]{ alice }
       .Concat(giraffen.Skip(1).Take(9));

    Assert.Contains(alice, tiere);
}

See also