Covarianță și contravarianță (programare)

Covarianța și contravarianța [1] în programare sunt modalități de transfer al moștenirii de tip la derivate [2] din ele tipuri - containere , tipuri generice , delegați etc. Termenii provin din concepte similare ale teoriei categoriilor „covariant” și „functor contravariant” .

Definiții

Covarianța este păstrarea ierarhiei de moștenire a tipurilor sursă în tipuri derivate în aceeași ordine. Deci, dacă o clasă Catmoștenește dintr-o clasă Animal, atunci este firesc să presupunem că enumerarea IEnumerable<Cat>va fi un descendent al enumerației IEnumerable<Animal>. Într-adevăr, „lista celor cinci pisici” este un caz special al „listei celor cinci animale”. În acest caz, se spune că tipul (în acest caz, interfața generică) este IEnumerable<T> covariant cu parametrul său de tip T.

Contravarianța este inversarea ierarhiei tipului sursă în tipurile derivate. Deci, dacă o clasă Stringeste moștenită de la clasă Object, iar delegatul Action<T>este definit ca o metodă care acceptă un obiect de tip T, atunci Action<Object>este moștenit de la delegat Action<String>, și nu invers. Într-adevăr, dacă „toate șirurile sunt obiecte”, atunci „orice metodă care operează pe obiecte arbitrare poate efectua o operație pe un șir”, dar nu invers. Într-un astfel de caz, se spune că tipul (în acest caz, un delegat generic) este Action<T> contravariant cu parametrul său de tip T.

Lipsa de moștenire între tipurile derivate se numește invarianță .

Contravariance vă permite să setați corect tipul atunci când creați subtipări (subtyping), adică să setați un set de funcții care vă permite să înlocuiți un alt set de funcții în orice context. La rândul său, covarianța caracterizează specializarea codului , adică înlocuirea vechiului cod cu unul nou în anumite cazuri. Astfel, covarianța și contravarianța sunt mecanisme de siguranță de tip independent , care nu se exclud reciproc, și pot și ar trebui utilizate în limbaje de programare orientate pe obiecte [3] .

Utilizare

Matrice și alte containere

În containerele care permit obiecte inscriptibile, covarianța este considerată nedorită, deoarece vă permite să ocoliți verificarea tipului. Într-adevăr, luați în considerare tablourile covariante. Lăsați clase Catși Dogmoșteniți dintr-o clasă Animal(în special, unei variabile de tip Animali se poate atribui o variabilă de tip Catsau Dog). Să creăm o matrice Cat[]. Datorită controlului de tip, numai obiectele tipului Catși descendenții săi pot fi scrise în această matrice. Apoi atribuim o referință la această matrice unei variabile de tip Animal[](covarianța matricelor permite acest lucru). Acum, în această matrice, deja cunoscută ca Animal[], vom scrie o variabilă de tip Dog. Astfel, Cat[]am scris în matrice Dog, ocolind controlul tipului. Prin urmare, este de dorit să se facă containere care să permită scrierea invariabilă. De asemenea, containerele care pot fi scrise pot implementa două interfețe independente, un Producer<T> covariant și un Consumer<T> contravariant, caz în care bypass-ul de verificare a tipului descris mai sus va eșua.

Deoarece verificarea tipului poate fi încălcată numai atunci când un element este scris în container, pentru colecții imuabile și iteratoare , covarianța este sigură și chiar utilă. De exemplu, cu ajutorul său în limbajul C#, oricărei metode care ia un argument de tip i IEnumerable<Object>se poate trece orice colecție de orice tip, de exemplu, IEnumerable<String>sau chiar List<String>.

Dacă, în acest context, containerul este folosit, dimpotrivă, doar pentru a scrie în el și nu există nicio citire, atunci poate fi contravariant. Deci, dacă există un tip ipotetic WriteOnlyList<T>care moștenește List<T>și interzice operațiunile de citire în el și o funcție cu un parametru WriteOnlyList<Cat>în care scrie obiecte de tipul , atunci este fie sigur Catsă treci la el - nu va scrie nimic acolo, cu excepția obiectelor din clasa moștenitor, dar încercați să citiți alte obiecte nu vor. List<Animal>List<Object>

Tipuri de funcții

În limbile cu funcții de primă clasă, există tipuri de funcții generice și variabile delegate . Pentru tipurile de funcții generice, sunt utile covarianța tipului returnat și contravarianța argumentului. Astfel, dacă un delegat este definit ca „o funcție care ia un șir și returnează un obiect”, atunci o funcție care ia un obiect și returnează un șir poate fi de asemenea scrisă în el: dacă o funcție poate prelua orice obiect, poate de asemenea ia o sfoară; iar din faptul că rezultatul funcției este un șir, rezultă că funcția returnează un obiect.

Implementare în limbi

C++

C++ a acceptat tipuri de returnare covariante în funcțiile virtuale suprascrise începând cu standardul din 1998 :

clasaX { }; clasa A { public : virtual X * f () { return new X ; } }; clasa Y : public X {}; clasa B : public A { public : virtual Y * f () { return new Y ; } // covarianța vă permite să setați un tip de returnare rafinat în metoda suprascrisă };

Pointerii în C++ sunt covarianți: de exemplu, unui pointer către o clasă de bază i se poate atribui un pointer către o clasă copil.

Șabloanele C++ sunt, în general, invariante; relațiile de moștenire ale claselor de parametri nu sunt transferate în șabloane. De exemplu, un container covariant vector<T>ar permite întreruperea verificării tipului. Cu toate acestea, folosind constructori de copiere parametrizați și operatori de atribuire, puteți crea un pointer inteligent care este covariant cu parametrul său de tip [4] .

Java

Covarianța tipului de returnare a metodei a fost implementată în Java începând cu J2SE 5.0 . Nu există covarianță în parametrii metodei: pentru a suprascrie o metodă virtuală, tipurile parametrilor acesteia trebuie să se potrivească cu definiția din clasa părinte, altfel va fi definită o nouă metodă supraîncărcată cu acești parametri în locul suprascrierii.

Matricele în Java au fost covariante încă de la prima versiune, când încă nu existau tipuri generice în limbaj . (Dacă nu ar fi cazul, atunci pentru a folosi, de exemplu, o metodă de bibliotecă care preia o matrice de obiecte Object[]pentru a lucra cu o matrice de șiruri de caractere String[], ar fi mai întâi necesar să o copiați într-o matrice nouă Object[].) Deoarece, așa cum am menționat mai sus, atunci când scrieți un element într-o astfel de matrice, puteți ocoli verificarea tipului, JVM -ul are o verificare suplimentară în timpul rulării care aruncă o excepție atunci când este scris un element invalid.

Tipurile generice în Java sunt invariante, deoarece în loc să creați o metodă generică care să funcționeze cu Obiecte, o puteți parametriza, transformând-o într-o metodă generică și păstrând controlul tipului.

În același timp, în Java, puteți implementa un fel de co- și contravarianță a tipurilor generice folosind caracterul wildcard și specificatorii de calificare: List<? extends Animal>va fi covariant cu tipul inline și List<? super Animal> contravariant.

C#

De la prima versiune de C# , tablourile au fost covariante. Acest lucru a fost făcut pentru compatibilitate cu limbajul Java [5] . Încercarea de a scrie un element de tip greșit într-o matrice generează o excepție de rulare .

Clasele și interfețele generice care au apărut în C# 2.0 au devenit, ca și în Java, invariante tip-parametru.

Odată cu introducerea delegaților generici (parametrizați după tipuri de argument și tipuri de returnare), limbajul a permis conversia automată a metodelor obișnuite în delegați generici cu covarianță pe tipurile returnate și contravarianță pe tipurile de argument. Prin urmare, în C# 2.0, cod ca acesta a devenit posibil:

void ProcessString ( String s ) { /* ... */ } void ProcessAnyObject ( Object o ) { /* ... */ } String GetString () { /* ... */ } Object GetAnyObject () { /* ... */ } //... Acțiune < String > process = ProcessAnyObject ; proces ( myString ); // acțiune legală Func < Object > getter = GetString ; Object obj = getter (); // acțiune legală

cu toate acestea, codul este Action<Object> process = ProcessString;incorect și dă o eroare de compilare, altfel acest delegat ar putea fi apelat ca process(5), trecând un Int32 la ProcessString.

În C# 2.0 și 3.0, acest mecanism permitea doar scrierea unor metode simple către delegații generici și nu puteau converti automat de la un delegat generic la altul. Cu alte cuvinte, codul

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

nu a compilat în aceste versiuni ale limbajului. Astfel, delegații generici în C# 2.0 și 3.0 erau încă invarianți.

În C# 4.0, această restricție a fost eliminată și pornind de la această versiune, codul f2 = f1din exemplul de mai sus a început să funcționeze.

În plus, în 4.0 a devenit posibilă specificarea explicit a variației parametrilor interfețelor generice și a delegaților. Pentru a face acest lucru, sunt folosite cuvintele cheie outși inrespectiv. Deoarece într-un tip generic, utilizarea efectivă a parametrului tip este cunoscută doar de autorul său și pentru că se poate modifica în timpul dezvoltării, această soluție oferă cea mai mare flexibilitate fără a compromite robustețea tastării.

Unele interfețe de bibliotecă și delegați au fost reimplementate în C# 4.0 pentru a profita de aceste caracteristici. De exemplu, interfața este IEnumerable<T>acum definită ca IEnumerable<out T>, interfață IComparable<T> ca IComparable<in T>, delegat Action<T> ca Action<in T>, etc.

Vezi și

Note

  1. Documentația Microsoft în copie de arhivă rusă din 24 decembrie 2015 la Wayback Machine folosește termenii covarianță și contravariație .
  2. În continuare, cuvântul „derivat” nu înseamnă „moștenitor”.
  3. Castagna, 1995 , Rezumat.
  4. Despre covarianță și șabloane C++ (8 februarie 2013). Consultat la 20 iunie 2013. Arhivat din original pe 28 iunie 2013.
  5. Eric Lippert. Covarianță și contravarianță în C#, partea a doua (17 octombrie 2007). Consultat la 22 iunie 2013. Arhivat din original pe 28 iunie 2013.

Literatură

  • Castagna, Giuseppe. Covarianță și contravarianță: conflict fără cauză  //  ACM Trans. program. Lang. Syst.. - ACM, 1995. - Vol. 17 , nr. 3 . — P. 431-447 . — ISSN 0164-0925 . - doi : 10.1145/203095.203096 .