Kovarians og kontravarians

I objektorientert programmering skiller kovarians og kontravarans om et aspekt (dvs. en typedeklarasjon) ligner arveretningen (kovariant) eller motsatt av det (kontravariant). Hvis det ikke er noen endring i underklassen sammenlignet med overklassen , kalles dette invarians .

Begrepene er basert på betraktningene i substituerbarhetsprinsippet : Objekter fra superklassen må være utskiftbare med objekter fra en av underklassene. Dette betyr for eksempel at metodene i underklassen må akseptere i det minste parametrene som superklassen også vil akseptere (kontravarians). Metodene i underklassen må også returnere verdier som er kompatible med superklassen, dvs. er aldri av en mer generell type enn returklassen til superklassen (kovarians).

Opprinnelsen til begrepet

I objektorientering er begrepene kontravarians og kovarians avledet fra det faktum at typene av parametrene som er underlagt er knyttet til arvshierarkiet for erstatning (kovariant) eller motsatt av arvshierarkiet (kontravariant).

Forekomst av avvik

Du kan velge mellom co-, contra- og invariance på

  • Metoder
    • Argumenttyper (typene passerte parametere)
    • Resultattyper (typene av returverdien)
    • andre signaturutvidelser (f.eks. unntakstyper i kaste- setningen i Java )
  • generiske klasseparametere

skille.

Substitusjonsprinsippet resulterer i følgende mulige forekomster av avvik i arvshierarkiet til objektorientert programmering:

Kontravaranse Inndataparametere
Kovarians Returverdi og unntak
Invarians Inngangs- og utgangsparametere

Kovarians, kontravarans og invarians

Kovarians betyr at typehierarkiet har samme retning som arvshierarkiet til klassene som skal vurderes. Hvis du vil tilpasse en arvelig metode, er tilpasningen medvariant hvis typen av en metodeparameter i superklassen er en supertype av parametertypen for denne metoden i underklassen.

Hvis typehierarkiet går i motsatt retning av arvshierarkiet til klassene som skal vurderes, snakker man om kontravarans. Hvis typene i øvre og nedre klasse ikke lar seg endre, snakker man om uforanderlighet.

I objektorientert modellering er det ofte ønskelig at inngangsparametrene til metoder også er samvariante. Dette bryter imidlertid med substitusjonsprinsippet. Den overbelastning håndteres annerledes i dette tilfellet ved de ulike programmeringsspråk.

Eksempel basert på programkode

I hovedsak, i programmeringsspråk som C ++ og C #, er variabler og parametere motstridende, mens metodeavkastning er kovariant. Java, derimot, krever kovariansen til metodeparametrene og variablene, der returparameteren må være kovariant:

Eksempel i C #
Kontravaranse Kovarians Invarians
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);
}

Eksempel basert på illustrasjoner

Følgende forklarer når typesikkerhet er garantert når du vil erstatte en funksjon med en annen. Dette kan deretter overføres til metoder i objektorientering hvis metoder erstattes av objekter i henhold til Liskovs substitusjonsprinsipp.

Være og funksjoner som har følgende signatur , for eksempel :

, hvor og , og
, hvor og .

Som du kan se, er det et supersett av , men en delmengde av . Hvis funksjonen brukes i stedet for , kalles inngangstypen C kontravariant, utgangstypen D samvariant. I eksemplet kan erstatningen skje uten typebrudd, siden oppføringen av dekker hele oppføringsområdet for . Det gir også resultater som ikke overstiger verdiområdet for .

Korrekthet av kontravarans og samvarians

UML- notasjonen brukes som en modell for å representere arvshierarkiet:

                       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 : Substitusjonsprinsippet er overholdt, fordi man kan metoden (t: T) av Division ClassB slik anvendelse som om det var av fremgangsmåten i overklassen Classa .
Sjekk: Du kan overføre en variabel av en mer spesiell type T ' til metode (t: T) , siden T' inneholder all informasjon som også er i T på grunn av arv .

Kovarians : Substitusjonsprinsippet følges, fordi metode (): T ' i underklassen ClassB kan brukes som om det var metoden til superklassen ClassA .
Kontroll: Returverdien til metoden fra ClassB er T ' . Du kan overføre denne verdien til en variabel deklarert av typen T , fordi T ' har all informasjonen som også er i T på grunn av arv .

Skriv inn sikkerhet i metoder

På grunn av egenskapene til substitusjonsprinsippet er statisk type sikkerhet garantert hvis argumenttypene er kontravariant og resultattypene er kovariante.

Typesikker usikkerhet

Kovariansen til metodeparametrene, som ofte er ønskelig i objektorientert modellering, støttes på mange programmeringsspråk til tross for den resulterende usikkerheten.

Et eksempel på typen usikkerhet ved parametere for kovariatemetoder finnes i følgende klasser Personog Arzt, og deres spesialiseringer Kindog Kinderarzt. Parameteren til metoden untersuchei klassen Kinderarzter en spesialisering av parameteren for den samme metoden Arztog derfor kovariant .

Typesikker usikkerhet - generelt
┌─────────┐         ┌───────────────┐
│    T    │         │ ClassA        │
├─────────┤         ├───────────────┤
│         │         │               │
├─────────┤         ├───────────────┤
│         │         │ method(t :T ) │
└─────────┘         └───────────────┘
     ↑                      ↑
┌─────────┐         ┌───────────────┐
│    T'   │         │ ClassB        │
├─────────┤         ├───────────────┤
│         │         │               │
├─────────┤         ├───────────────┤
│         │         │ method(t':T') │
└─────────┘         └───────────────┘
   Eksempel på typesikker usikkerhet
┌────────────────┐         ┌───────────────────────┐
│ Person         │         │ Arzt                  │
├────────────────┤         ├───────────────────────┤
│                │         │                       │
├────────────────┤         ├───────────────────────┤
│ stillHalten()  │         │ untersuche(p: Person) │
└────────────────┘         └───────────────────────┘
         ↑                             ↑
┌────────────────┐         ┌───────────────────────┐
│ Kind           │         │ Kinderarzt            │
├────────────────┤         ├───────────────────────┤
│                │         │                       │
├────────────────┤         ├───────────────────────┤
│ tapferSein()   │         │ untersuche(k: Kind)   │
└────────────────┘         └───────────────────────┘
Implementeringen av eksemplet i Java ser slik ut: Et program som bruker klassene kan se slik ut: Resultatet er da:
   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

Det er viktig at objektet arztmå erklæres riktig fordi en metode ikke blir overskrevet her, men snarere overbelastet, og prosessen med overbelastning er knyttet til objektets statiske type. Du kan se resultatet når du sammenligner utgiftene: Meier kan ikke undersøke barn, Dr. Schulze, derimot, gjør det.

Eksemplet fungerer riktig i Java: Metoden untersuchefor Arztblir Kinderarztikke overskrevet, men bare overbelastet på grunn av de forskjellige parametrene, noe som betyr at riktig metode blir kalt i hvert tilfelle. Når den Arzt untersuchekalles, kalles metoden alltid der; men når Kinderarzt untersuchekalt, kalles det en gang untersuchetil Arztog en gang til , avhengig av type Kinderarzt. I følge språkdefinisjonen til Java, må en metode som skal overskrives ha samme signatur (i Java bestående av parametere + mulige unntak).


Det samme eksemplet kan også kodes i Python, men vær oppmerksom på at parametere ikke er skrevet. Koden vil se slik ut:


#!/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)

Kovarians på arrays

Med array-datatyper kan kovarians forårsake et problem på språk som C ++, Java og C #, ettersom de internt beholder datatypen selv etter konverteringen:

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;
}

For å unngå slike kjøretidsfeil, kan generiske datatyper brukes som ikke tilbyr noen modifiseringsmetoder. Grensesnittet som IEnumerable<T>blant annet er implementert av array-datatypen , brukes ofte i C # . Siden a IEnumerable<Tier>ikke kan endres, z. For eksempel kan en ny forekomst opprettes fra LINQ ved hjelp av utvidelsesmetoder for å imøtekomme elementet . 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);
}

Se også