Kovariance a kontravariance (programování)

Kovariance a kontravariance [1] v programování jsou způsoby, jak přenést dědičnost typu na deriváty [2] z nich typy - kontejnery , generické typy , delegáti atd. Termíny vznikly z podobných konceptů teorie kategorií "kovariant" a "kontravariantní funktor" .

Definice

Kovariance je zachování hierarchie dědičnosti zdrojových typů v odvozených typech ve stejném pořadí. Pokud tedy třída Catdědí z třídy Animal, pak je přirozené předpokládat, že výčet IEnumerable<Cat>bude potomkem výčtu IEnumerable<Animal>. Ve skutečnosti je „seznam pěti koček“ zvláštním případem „seznamu pěti zvířat“. V tomto případě je typ (v tomto případě generické rozhraní) považován za IEnumerable<T> kovariantní s jeho parametrem typu T.

Kontravariance je obrácení hierarchie zdrojových typů v odvozených typech. Pokud je tedy třída Stringzděděna z třídy Objecta delegát Action<T>je definován jako metoda, která přijímá objekt typu T, pak Action<Object>se dědí od delegáta Action<String>a ne naopak. Pokud „všechny řetězce jsou objekty“, pak „jakákoli metoda, která pracuje s libovolnými objekty, může provést operaci s řetězcem“, ale ne naopak. V takovém případě je typ (v tomto případě obecný delegát) považován za Action<T> kontravariantní k jeho parametru typu T.

Nedostatek dědičnosti mezi odvozenými typy se nazývá invariance .

Kontravariance umožňuje správně nastavit typ při vytváření podtypování (podtypování), tedy nastavit sadu funkcí, která umožňuje nahradit jinou sadu funkcí v libovolném kontextu. Kovariance zase charakterizuje specializaci kódu , to znamená nahrazení starého kódu novým v určitých případech. Kovariance a kontravariance jsou tedy nezávislé typy bezpečnostních mechanismů , které se vzájemně nevylučují a mohou a měly by být používány v objektově orientovaných programovacích jazycích [3] .

Použití

Pole a jiné kontejnery

V kontejnerech , které umožňují zapisovatelné objekty, je kovariance považována za nežádoucí, protože umožňuje obejít kontrolu typu. Opravdu, zvažte kovariantní pole. Nechte třídy Cata Dogdědí z třídy Animal(zejména typové proměnné Animallze přiřadit proměnnou typu Catnebo Dog). Vytvořme pole Cat[]. Díky kontrole typu Catlze do tohoto pole zapisovat pouze objekty typu a jeho potomků. Poté přiřadíme odkaz na toto pole do typové proměnné Animal[](kovariance polí to umožňuje). Nyní do tohoto pole, již známého jako Animal[], zapíšeme proměnnou typu Dog. Zapsali Cat[]jsme tedy do pole Dog, přičemž jsme obešli řízení typu. Proto je žádoucí vyrobit nádoby, které umožňují zápis invariantní. Zapisovatelné kontejnery mohou také implementovat dvě nezávislá rozhraní, kovariantní Producer<T> a kontravariantní Consumer<T>, v takovém případě výše popsané vynechání kontroly typu selže.

Vzhledem k tomu, že kontrola typu může být narušena pouze tehdy, když je prvek zapsán do kontejneru, pro neměnné kolekce a iterátory je kovariance bezpečná a dokonce užitečná. Například s jeho pomocí v jazyce C# lze jakékoli metodě, která přebírá argument typu IEnumerable<Object>, předat jakoukoli kolekci jakéhokoli typu, například IEnumerable<String>nebo dokonce List<String>.

Pokud se v této souvislosti kontejner používá naopak pouze pro zápis do něj a nedochází ke čtení, pak může být kontravariantní. Pokud tedy existuje hypotetický typ WriteOnlyList<T>, který od něj dědí List<T>a zakazuje v něm operace čtení, a funkce s parametrem WriteOnlyList<Cat>, kam zapisuje objekty typu , pak je buď bezpečné Catmu předat - kromě objektů tam nic nezapíše. třídy dědice, ale pokuste se číst ostatní objekty nebudou. List<Animal>List<Object>

Typy funkcí

V jazycích s prvotřídními funkcemi existují obecné typy funkcí a delegované proměnné . Pro obecné typy funkcí jsou užitečné kovariance návratového typu a kontravariance argumentů. Pokud je tedy delegát definován jako „funkce, která vezme řetězec a vrátí objekt“, pak do něj lze zapsat také funkci, která vezme objekt a vrátí řetězec: pokud funkce může přijmout jakýkoli objekt, může také vzít provázek; a z toho, že výsledkem funkce je řetězec, vyplývá, že funkce vrací objekt.

Implementace v jazycích

C++

C++ podporuje kovariantní návratové typy v přepsaných virtuálních funkcích od standardu z roku 1998 :

classX { }; třída A { veřejnost : virtuální X * f () { return new X ; } }; třída Y : public X {}; třída B : veřejná A { veřejnost : virtuální Y * f () { return new Y ; } // kovariance umožňuje nastavit upřesněný návratový typ v přepsané metodě };

Ukazatele v C++ jsou kovariantní: například ukazatel na základní třídu lze přiřadit ukazatel na podřízenou třídu.

Šablony C++ jsou obecně invariantní, dědičné vztahy tříd parametrů se do šablon nepřenášejí. Například kovariantní kontejner vector<T>by umožnil přerušit kontrolu typu. Pomocí parametrizovaných konstruktorů kopírování a operátorů přiřazení však můžete vytvořit inteligentní ukazatel , který je kovariantní s parametrem typu [4] .

Java

Kovariance návratového typu metody je v Javě implementována od J2SE 5.0 . V parametrech metody neexistuje žádná kovariance: pro přepsání virtuální metody musí typy jejích parametrů odpovídat definici v nadřazené třídě, jinak bude místo přepsání definována nová přetížená metoda s těmito parametry .

Pole v Javě jsou kovariantní od první verze, kdy v jazyce ještě nebyly žádné generické typy . (Pokud by tomu tak nebylo, pak pro použití například metody knihovny, která přebírá pole objektů Object[]pro práci s polem řetězců String[], by bylo nejprve nutné zkopírovat ji do nového pole Object[].) Protože, jak již bylo zmíněno výše, když zapisujete prvek do takového pole, můžete obejít kontrolu typu, JVM má další kontrolu za běhu, která vyvolá výjimku , když je zapsán neplatný prvek.

Obecné typy v Javě jsou invariantní, protože místo vytváření obecné metody, která pracuje s objekty, můžete ji parametrizovat, přeměnit ji na obecnou metodu a zachovat kontrolu typu.

Zároveň můžete v Javě implementovat jakousi ko- a kontravarianci generických typů pomocí zástupného znaku a kvalifikačních specifikátorů: List<? extends Animal>bude kovariantní k inline typu a List<? super Animal> kontravariantní.

C#

Od první verze C# byla pole kovariantní. To bylo provedeno kvůli kompatibilitě s jazykem Java [5] . Pokus o zápis prvku nesprávného typu do pole vyvolá výjimku za běhu .

Obecné třídy a rozhraní, které se objevily v C# 2.0, se staly, stejně jako v Javě, invarianty typu-parametr.

Se zavedením obecných delegátů (parametrizovaných podle typů argumentů a návratových typů) jazyk umožnil automatickou konverzi běžných metod na obecné delegáty s kovariancí na návratových typech a kontravariancí na typech argumentů. Proto se v C# 2.0 stal možným tento kód:

void ProcessString ( String s ) { /* ... */ } void ProcessAnyObject ( Object o ) { /* ... */ } String GetString () { /* ... */ } Object GetAnyObject () { /* ... */ } //... Akce < String > process = ProcessAnyObject ; proces ( myString ); // legální akce Func < Object > getter = GetString ; Object obj = getter (); // legální akce

kód je však Action<Object> process = ProcessString;nesprávný a poskytuje chybu kompilace, jinak by tento delegát mohl být volán jako process(5), předávající Int32 do ProcessString.

V C# 2.0 a 3.0 tento mechanismus umožňoval pouze zápis jednoduchých metod do generických delegátů a nemohl automaticky převádět z jednoho generického delegáta na druhého. Jinými slovy, kód

Func < String > f1 = GetString ; Func < Objekt > f2 = f1 ;

nebyly zkompilovány v těchto verzích jazyka. Takže generičtí delegáti v C# 2.0 a 3.0 byli stále invariantní.

V C# 4.0 bylo toto omezení odstraněno a od této verze f2 = f1začal fungovat kód ve výše uvedeném příkladu.

Kromě toho bylo ve verzi 4.0 možné explicitně specifikovat rozptyl parametrů generických rozhraní a delegátů. K tomu se používají klíčová slova resp out. inProtože u generického typu zná skutečné použití parametru typu pouze jeho autor, a protože se může během vývoje měnit, poskytuje toto řešení největší flexibilitu, aniž by byla ohrožena robustnost psaní.

Některá rozhraní knihoven a delegáti byla reimplementována v C# 4.0, aby využila těchto funkcí. Například rozhraní je IEnumerable<T>nyní definováno jako IEnumerable<out T>, rozhraní IComparable<T> jako IComparable<in T>, delegát Action<T> jako Action<in T>atd.

Viz také

Poznámky

  1. Dokumentace Microsoftu v ruské archivní kopii ze dne 24. prosince 2015 na Wayback Machine používá termíny kovariance a kontravariace .
  2. Dále slovo „derivát“ neznamená „dědic“.
  3. Castagna, 1995 , Abstrakt.
  4. O kovarianci a šablonách C++ (8. února 2013). Získáno 20. června 2013. Archivováno z originálu 28. června 2013.
  5. Eric Lippert. Kovariance a kontravariance v C#, část druhá (17. října 2007). Získáno 22. června 2013. Archivováno z originálu 28. června 2013.

Literatura

  • Castagna, Giuseppe. Kovariance a rozporuplnost: Konflikt bez příčiny  //  ACM Trans. program. Lang. Syst.. - ACM, 1995. - Sv. 17 , č. 3 . — S. 431-447 . — ISSN 0164-0925 . - doi : 10.1145/203095.203096 .