A kovariancia és a kontravariancia [1] a programozásban a típus öröklődésének származékaira [2] történő átvitelének módjai ezekből - konténerek , általános típusok , delegáltak stb .
A kovariancia a forrástípusok öröklődési hierarchiájának megőrzése a származtatott típusokban, ugyanabban a sorrendben. Tehát, ha egy osztály Catörököl egy osztálytól Animal, akkor természetes az a feltételezés, hogy a felsorolás a felsorolás IEnumerable<Cat>leszármazottja lesz IEnumerable<Animal>. Valójában az "öt macska listája" az "öt állat listája" speciális esete. Ebben az esetben a típust (jelen esetben az általános interfész) IEnumerable<T> kovariánsnak mondjuk a T típusparaméterével.
A kontravariancia a forrástípus-hierarchia megfordítása a származtatott típusokban. Tehát, ha egy osztály Stringaz osztálytól öröklődik Object, és a delegált olyan metódusként Action<T>van definiálva, amely elfogad egy T típusú objektumot, akkor Action<Object>a delegálttól öröklődik Action<String>, és nem fordítva. Valójában, ha "minden karakterlánc objektum", akkor "bármilyen metódus, amely tetszőleges objektumokon működik, végrehajthat műveletet egy karakterláncon", de fordítva nem. Ilyen esetben a típust (jelen esetben általános delegált) ellentétesnek mondjuk a T Action<T> típusparaméterével .
A származtatott típusok közötti öröklődés hiányát invarianciának nevezzük .
Az ellentét lehetővé teszi a típus helyes beállítását az altípus (altípus) létrehozásakor , vagyis olyan függvénykészlet beállítását, amely lehetővé teszi egy másik függvénykészlet cseréjét bármilyen környezetben. A kovariancia pedig a kód specializációját jellemzi , vagyis bizonyos esetekben a régi kód újjal való helyettesítését. Így a kovariancia és a kontravariancia független típusú biztonsági mechanizmusok , amelyek nem zárják ki egymást, és használhatók és kell használni az objektum-orientált programozási nyelvekben [3] .
Az írható objektumokat lehetővé tevő tárolókban a kovariancia nem kívánatos, mert lehetővé teszi a típusellenőrzés megkerülését. Valóban, vegyük figyelembe a kovariáns tömböket. Legyen osztályok Catés Dogörököljenek egy osztályból Animal(különösen egy típusváltozóhoz Animalrendelhető típusváltozó Catvagy Dog). Hozzunk létre egy tömböt Cat[]. A típusvezérlésnek köszönhetően csak a típus objektumai Catés leszármazottai írhatók ebbe a tömbbe. Ezután ehhez a tömbhöz rendelünk egy hivatkozást egy típusváltozóhoz Animal[](a tömbök kovarianciája ezt lehetővé teszi). Most ebbe a tömbbe, amelyet már néven ismerünk, Animal[]egy típusú változót fogunk írni Dog. Így a tömbbe Cat[]írtunk Dog, megkerülve a típusvezérlést. Ezért kívánatos olyan konténereket készíteni, amelyek lehetővé teszik az invariáns írást. Az írható tárolók két független interfészt is megvalósíthatnak, egy kovariáns Producer<T>-t és egy kontravariáns Consumer<T>-t, ebben az esetben a fent leírt típus-ellenőrzési bypass sikertelen lesz.
Mivel a típusellenőrzést csak akkor lehet megsérteni, ha egy elemet a tárolóba írnak, a változtathatatlan gyűjtemények és iterátorok esetében a kovariancia biztonságos, sőt hasznos is. Például a C# nyelv segítségével bármilyen típusú argumentumot felvevő metódus IEnumerable<Object>átadható bármilyen típusú gyűjteménynek, például, IEnumerable<String>vagy akár List<String>.
Ha ebben a kontextusban a tárolót éppen ellenkezőleg, csak írásra használjuk, és nincs olvasás, akkor kontravariáns lehet. Tehát, ha van egy hipotetikus típus WriteOnlyList<T>, amely örökli List<T>és tiltja benne az olvasási műveleteket, és egy olyan paraméterrel rendelkező függvény, amely WriteOnlyList<Cat>típusú objektumokat ír , akkor vagy biztonságos Catátadni neki - nem ír semmit, kivéve az objektumokat. az inheritor osztályból, de próbálja meg beolvasni a többi objektumot, ez nem fog sikerülni. List<Animal>List<Object>
Az első osztályú függvényekkel rendelkező nyelvekben vannak általános függvénytípusok és delegált változók . Az általános függvénytípusoknál hasznos a visszatérési típusú kovariancia és az argumentumkontravariancia. Így, ha egy delegált úgy van definiálva, mint "egy függvény, amely egy karakterláncot vesz fel és egy objektumot ad vissza", akkor egy olyan függvény is írható rá, amelyik egy objektumot vesz és egy karakterláncot ad vissza: ha egy függvény bármilyen objektumot el tud venni, akkor azt is. vegyen egy húrt; abból pedig, hogy a függvény eredménye egy karakterlánc, az következik, hogy a függvény egy objektumot ad vissza.
A C++ az 1998-as szabvány óta támogatja a kovariáns visszatérési típusokat felülírt virtuális függvényekben :
classX { }; A osztály { nyilvános : virtual X * f () { return new X ; } }; Y osztály : nyilvános X {}; B osztály : nyilvános A { nyilvános : virtual Y * f () { return new Y ; } // a kovariancia lehetővé teszi finomított visszatérési típus beállítását a felülbírált metódusban };A C++ mutatói kovariánsak: például egy alaposztályra mutató mutató hozzárendelhető egy utódosztályhoz.
A C++ sablonok általában invariánsak, a paraméterosztályok öröklődési relációi nem kerülnek át a sablonokba. Például egy kovariáns tároló vector<T>lehetővé tenné a típusellenőrzés megszakítását. Paraméterezett másoláskonstruktorok és hozzárendelési operátorok használatával azonban létrehozhat egy intelligens mutatót , amely kovariáns a típusparaméterével [4] .
Method return típusú kovariancia a J2SE 5.0 óta van implementálva a Java nyelven . A metódusparaméterekben nincs kovariancia: egy virtuális metódus felülbírálásához a paraméterei típusának meg kell egyeznie a szülőosztály definíciójával, ellenkező esetben a felülírás helyett egy új túlterhelt metódus kerül meghatározásra ezekkel a paraméterekkel .
A Java tömbök a legelső verzió óta kovariánsak, amikor még nem voltak általános típusok a nyelvben . (Ha nem ez lenne a helyzet, akkor például egy olyan könyvtári metódus használatához, amely egy objektumtömböt használ, Object[]hogy működjön egy stringek tömbjével , String[]először át kell másolni egy új tömbbe Object[].) Mivel, mint említettük, fent, amikor egy elemet ír egy ilyen tömbbe, megkerülheti a típusellenőrzést, a JVM rendelkezik további futásidejű ellenőrzéssel, amely kivételt dob, ha érvénytelen elemet írnak.
A Java általános típusai változatlanok, mert ahelyett, hogy létrehoznánk egy általános metódust, amely az Objects-szel működik, paraméterezhető, általános metódussá alakítva és megtartva a típusvezérlést.
Ugyanakkor a Java-ban megvalósíthatja az általános típusok egyfajta ko- és kontravarianciáját a helyettesítő karakter és a minősítő specifikációk használatával: List<? extends Animal>a soron belüli típushoz kovariáns lesz, és List<? super Animal> kontravariáns.
A C# első verziója óta a tömbök kovariánsak. Ez a Java nyelvvel való kompatibilitás érdekében történt [5] . Ha egy rossz típusú elemet próbál meg írni egy tömbbe , az futásidejű kivételt eredményez .
A C# 2.0-ban megjelent általános osztályok és interfészek, akárcsak a Java-ban, típusparaméter-invariánssá váltak.
Az általános delegátusok (argumentumtípusok és visszatérési típusok szerint paraméterezve) bevezetésével a nyelv lehetővé tette a közönséges metódusok automatikus konvertálását általános delegátussá, a visszatérési típusok kovariancia és az argumentumtípusok ellentmondása mellett. Ezért a C# 2.0-ban lehetővé vált az ehhez hasonló kód:
void ProcessString ( String s ) { /* ... */ } void ProcessAnyObject ( Object o ) { /* ... */ } String GetString ( ) { /* ... */ } Object GetAnyObject ( ) { /* ... */ } //... Művelet < String > process = ProcessAnyObject ; folyamat ( myString ); // jogi eljárás Func < Object > getter = GetString ; Objektum obj = getter (); // jogi eljárásazonban a kód Action<Object> process = ProcessString;hibás, és fordítási hibát ad, különben ez a delegált a következőként hívható meg process(5), és egy Int32-t ad át a ProcessStringnek.
A C# 2.0-ban és 3.0-ban ez a mechanizmus csak egyszerű metódusok írását tette lehetővé az általános küldötteknek, és nem tudott automatikusan konvertálni egyik általános delegáltról a másikra. Más szóval a kód
Func < String > f1 = GetString ; Func < Object > f2 = f1 ;nem a nyelv ezen változataiban fordította le. Így a C# 2.0 és 3.0 általános küldöttei továbbra is változatlanok voltak.
A C# 4.0-ban ezt a korlátozást eltávolították, és ettől a verziótól kezdve a f2 = f1fenti példában szereplő kód elkezdett működni.
Emellett a 4.0-ban lehetővé vált az általános interfészek és delegáltak paramétereinek varianciájának explicit megadása. Ehhez a és a kulcsszavakat outhasználják in. Mivel egy általános típusban a típusparaméter tényleges használatát csak a szerzője ismeri, és mivel ez a fejlesztés során változhat, ez a megoldás biztosítja a legnagyobb rugalmasságot anélkül, hogy a gépelés robusztusságát veszélyeztetné.
Egyes könyvtári felületeket és delegáltakat a C# 4.0-ban újra implementálták, hogy kihasználják ezeket a funkciókat. Például az interfész IEnumerable<T>most a következőképpen van definiálva: IEnumerable<out T>, interface IComparable<T> mint IComparable<in T>, delegate Action<T> as Action<in T>stb.