Kovarians och kontravarians [1] i programmering är sätt att överföra typarv till derivator [2] från dem typer - behållare , generiska typer , delegater , etc. Termerna härstammar från liknande begrepp om kategoriteorin "kovariant" och "kontravariant funktion" .
Kovarians är bevarandet av arvshierarkin av källtyper i härledda typer i samma ordning. Så om en klass Catärver från en klass Animalär det naturligt att anta att uppräkningen IEnumerable<Cat>kommer att vara en ättling till uppräkningen IEnumerable<Animal>. Faktum är att "listan över fem katter" är ett specialfall av "listan över fem djur." I det här fallet sägs typen (i detta fall det generiska gränssnittet) vara IEnumerable<T> samvariant med dess typparameter T.
Kontravarians är omkastningen av källtypshierarkin i härledda typer. Så om en klass Stringärvs från klassen Objectoch delegaten Action<T>definieras som en metod som accepterar ett objekt av typen T, så Action<Object>ärvs den från delegaten Action<String>och inte vice versa. Faktum är att om "alla strängar är objekt", så "kan vilken metod som helst som fungerar på godtyckliga objekt utföra en operation på en sträng", men inte vice versa. I ett sådant fall sägs typen (i detta fall en generisk delegat) vara i Action<T> strid med dess typparameter T.
Bristen på arv mellan härledda typer kallas invarians .
Kontravarians låter dig ställa in typen korrekt när du skapar subtyping (subtyping), det vill säga att ställa in en uppsättning funktioner som låter dig ersätta en annan uppsättning funktioner i vilket sammanhang som helst. I sin tur kännetecknar kovarians kodens specialisering , det vill säga ersättningen av den gamla koden med en ny i vissa fall. Således är kovarians och kontravarians oberoende typ av säkerhetsmekanismer , som inte utesluter varandra, och kan och bör användas i objektorienterade programmeringsspråk [3] .
I behållare som tillåter skrivbara objekt anses kovarians vara oönskat eftersom det tillåter dig att kringgå typkontroll. Tänk faktiskt på kovarianta arrayer. Låt klasser Catoch Dogärva från en klass Animal(särskilt en typvariabel Animalkan tilldelas en typvariabel Cateller Dog). Låt oss skapa en array Cat[]. Tack vare typkontroll kan endast objekt av typen Catoch dess avkomlingar skrivas till denna array. Sedan tilldelar vi en referens till denna array till en typvariabel Animal[](kovariansen av arrayer tillåter detta). Nu i denna array, redan känd som Animal[], kommer vi att skriva en variabel av typen Dog. Därför skrev Cat[]vi till arrayen Dogoch förbigick typkontroll. Därför är det önskvärt att göra behållare som tillåter skrivning invariant. Dessutom kan skrivbara behållare implementera två oberoende gränssnitt, en kovariant Producer<T> och en kontravariant Consumer<T>, i vilket fall den typkontrollförbikoppling som beskrivs ovan kommer att misslyckas.
Eftersom typkontroll endast kan överträdas när ett element skrivs till behållaren, för oföränderliga samlingar och iteratorer , är kovarians säker och till och med användbar. Till exempel, med dess hjälp i C#-språket, kan vilken metod som helst som tar ett argument av typ IEnumerable<Object>passeras vilken samling som helst av vilken typ som helst, till exempel IEnumerable<String>eller till och med List<String>.
Om, i detta sammanhang, behållaren tvärtom endast används för att skriva till den, och det inte finns någon läsning, kan det vara motsatt. Så om det finns en hypotetisk typ WriteOnlyList<T>som ärver från List<T>och förbjuder läsoperationer i den, och en funktion med en parameter WriteOnlyList<Cat>där den skriver objekt av typen , så är det antingen säkert Catatt överföra till det - det kommer inte att skriva något där förutom objekt av arvsklassen, men försök att läsa andra objekt kommer inte det. List<Animal>List<Object>
I språk med förstklassiga funktioner finns generiska funktionstyper och delegeringsvariabler . För generiska funktionstyper är returtypskovarians och argumentkontravarians användbara. Således, om en delegat definieras som "en funktion som tar en sträng och returnerar ett objekt", så kan en funktion som tar ett objekt och returnerar en sträng också skrivas till den: om en funktion kan ta vilket objekt som helst, kan den också ta ett snöre; och av det faktum att resultatet av funktionen är en sträng, följer att funktionen returnerar ett objekt.
C++ har stödt kovarianta returtyper i åsidosatta virtuella funktioner sedan 1998 års standard :
classX { }; klass A { offentliga : virtuell X * f () { returnera nytt X ; } }; klass Y : offentligt X {}; klass B : offentlig A { offentliga : virtuell Y * f () { returnera nytt Y ; } // kovarians låter dig ställa in en förfinad returtyp i den åsidosatta metoden };Pekare i C++ är kovarianta: till exempel kan en pekare till en basklass tilldelas en pekare till en underklass.
C++-mallar är generellt sett invarianta, arvsrelationerna för parameterklasser överförs inte till mallar. Till exempel skulle en kovariant behållare vector<T>tillåta att typkontroll bryts. Men med hjälp av parametriserade kopieringskonstruktorer och tilldelningsoperatorer kan du skapa en smart pekare som är samvariant med dess typparameter [4] .
Kovarians av metodreturtyp har implementerats i Java sedan J2SE 5.0 . Det finns ingen kovarians i metodparametrar: för att åsidosätta en virtuell metod måste typen av dess parametrar matcha definitionen i den överordnade klassen, annars kommer en ny överbelastad metod med dessa parametrar att definieras istället för åsidosättningen.
Arrayer i Java har varit samvarierande sedan den allra första versionen, då det ännu inte fanns några generiska typer i språket . (Om detta inte var fallet, då för att använda till exempel en biblioteksmetod som tar en array av objekt Object[]för att arbeta med en array av strängar String[], skulle det först vara nödvändigt att kopiera den till en ny array Object[].) Eftersom, som nämnts, ovan, när du skriver ett element till en sådan array, kan du kringgå typkontroll, JVM har ytterligare körtidskontroll som ger ett undantag när ett ogiltigt element skrivs.
Generiska typer i Java är invarianta, för istället för att skapa en generisk metod som fungerar med objekt kan du parametrisera den, förvandla den till en generisk metod och behålla typkontroll.
Samtidigt, i Java, kan du implementera ett slags sam- och kontravarians av generiska typer med hjälp av jokertecken och kvalificerande specifikationer: List<? extends Animal>kommer att vara samvariant med inline-typen och List<? super Animal> kontravariant.
Sedan den första versionen av C# har arrayer varit kovarianta. Detta gjordes för kompatibilitet med Java-språket [5] . Ett försök att skriva ett element av fel typ till en array ger ett runtime- undantag .
De generiska klasserna och gränssnitten som dök upp i C# 2.0 blev, som i Java, typparameterinvarianta.
Med introduktionen av generiska delegater (parametriserade av argumenttyper och returtyper) tillät språket automatisk konvertering av vanliga metoder till generiska delegater med kovarians på returtyper och kontravarians på argumenttyper. Därför, i C# 2.0, blev kod som denna möjlig:
void ProcessString ( String s ) { /* ... */ } void ProcessAnyObject ( Object o ) { /* ... */ } String GetString () { /* ... */ } Object GetAnyObject () { /* ... */ } //... Åtgärd < String > process = ProcessAnyObject ; process ( myString ); // rättsliga åtgärder Func < Objekt > getter = GetString ; Objekt obj = getter (); // rättsliga åtgärderdock är koden Action<Object> process = ProcessString;felaktig och ger ett kompileringsfel, annars skulle denna delegat kunna anropas som process(5), och skicka en Int32 till ProcessString.
I C# 2.0 och 3.0 tillät denna mekanism endast att skriva enkla metoder till generiska delegater och kunde inte automatiskt konvertera från en generisk delegat till en annan. Med andra ord, koden
Func < String > f1 = GetString ; Func < Objekt > f2 = f1 ;kompilerade inte i dessa versioner av språket. Således var generiska delegater i C# 2.0 och 3.0 fortfarande invarianta.
I C# 4.0 togs denna begränsning bort, och från och med den här versionen började koden f2 = f1i exemplet ovan att fungera.
Dessutom blev det i 4.0 möjligt att explicit specificera variansen av parametrar för generiska gränssnitt och delegater. För att göra detta används nyckelorden outrespektive in. Eftersom i en generisk typ är den faktiska användningen av typparametern endast känd av dess författare, och eftersom den kan ändras under utvecklingen ger denna lösning den största flexibiliteten utan att kompromissa med robustheten i att skriva.
Vissa biblioteksgränssnitt och delegater har omimplementerats i C# 4.0 för att dra nytta av dessa funktioner. Till exempel är gränssnitt IEnumerable<T>nu definierat som IEnumerable<out T>, gränssnitt IComparable<T> som IComparable<in T>, delegera Action<T> som Action<in T>, etc.