Kovarianssi ja kontravarianssi (ohjelmointi)

Kovarianssi ja kontravarianssi [1] ohjelmoinnissa ovat tapoja siirtää tyypin periytymistä johdannaisiksi [2] niistä tyypeiksi - kontit , geneeriset tyypit , delegaatit jne. Termit ovat peräisin samanlaisista luokkateorian käsitteistä "kovariantti" ja "kontravarianttifunktio" .

Määritelmät

Kovarianssi on lähdetyyppien periytymishierarkian säilyttäminen johdetuissa tyypeissä samassa järjestyksessä. Joten jos luokka Catperii luokasta Animal, on luonnollista olettaa, että luettelointi IEnumerable<Cat>on luettelon jälkeläinen IEnumerable<Animal>. "Viiden kissan luettelo" onkin "viiden eläimen luettelon" erikoistapaus. Tässä tapauksessa tyypin (tässä tapauksessa geneerisen rajapinnan) sanotaan olevan IEnumerable<T> kovariantti sen tyyppiparametrin T kanssa.

Kontravarianssi on lähdetyyppihierarkian kääntäminen johdetuissa tyypeissä. Joten jos luokka Stringon peritty luokasta Objectja delegaatti Action<T>on määritelty menetelmäksi, joka hyväksyy T-tyypin objektin, se Action<Object>peritään delegaatilta Action<String>, eikä päinvastoin. Todellakin, jos "kaikki merkkijonot ovat objekteja", "mikä tahansa menetelmä, joka toimii mielivaltaisilla objekteilla, voi suorittaa toiminnon merkkijonolle", mutta ei päinvastoin. Tällaisessa tapauksessa tyypin (tässä tapauksessa yleisen edustajan) sanotaan olevan ristiriidassa sen Action<T> tyyppiparametrin T kanssa.

Johdettujen tyyppien välisen periytymisen puutetta kutsutaan invarianssiksi .

Kontravarianssin avulla voit asettaa tyypin oikein luodessasi alatyyppiä (alatyyppiä), eli asettaa funktiojoukon, jonka avulla voit korvata toisen funktiojoukon missä tahansa kontekstissa. Kovarianssi puolestaan ​​kuvaa koodin erikoistumista, eli vanhan koodin korvaamista uudella tietyissä tapauksissa. Siten kovarianssi ja kontravarianssi ovat itsenäisiä turvamekanismeja , jotka eivät sulje toisiaan pois, ja niitä voidaan ja pitäisi käyttää olio-ohjelmointikielissä [3] .

Käyttö

Taulukot ja muut säiliöt

Säiliöissä , jotka sallivat kirjoitettavat objektit, kovarianssia ei pidetä toivottavana, koska sen avulla voit ohittaa tyyppitarkistuksen. Harkitse todellakin kovarianttitaulukoita. Olkoon luokat Catja Dogperiytyvät luokasta Animal(erityisesti tyyppimuuttujalle Animalvoidaan määrittää tyyppimuuttuja Cattai Dog). Luodaan taulukko Cat[]. CatTyypinhallinnan ansiosta tähän taulukkoon voidaan kirjoittaa vain tyypin objekteja ja sen jälkeläisiä. Sitten annamme viittauksen tähän taulukkoon Animal[]tyyppimuuttujalle (taulukoiden kovarianssi sallii tämän). Nyt tähän taulukkoon, joka tunnetaan jo nimellä Animal[], kirjoitamme muuttujan, jonka tyyppi on Dog. Näin ollen Cat[]kirjoitimme taulukkoon Dogohittaen tyypin ohjauksen. Siksi on toivottavaa tehdä säiliöitä, jotka sallivat kirjoittamisen muuttumattomina. Myös kirjoitettavat säiliöt voivat toteuttaa kaksi itsenäistä rajapintaa, kovariantin Producer<T> ja kontravariantin Consumer<T>, jolloin yllä kuvattu tyypintarkistuksen ohitus epäonnistuu.

Koska tyyppitarkistusta voidaan rikkoa vain, kun elementti kirjoitetaan säilöön, muuttumattomille kokoelmille ja iteraattoreille kovarianssi on turvallista ja jopa hyödyllistä. Esimerkiksi C#-kielen avulla mikä tahansa menetelmä, joka ottaa tyypin argumentin, IEnumerable<Object>voidaan välittää minkä tahansa tyyppiselle kokoelmalle, esimerkiksi IEnumerable<String>tai jopa List<String>.

Jos tässä yhteydessä konttia käytetään päinvastoin vain siihen kirjoittamiseen, eikä lukemista ole, se voi olla ristiriitainen. Joten jos on hypoteettinen tyyppi WriteOnlyList<T>, joka perii List<T>ja estää lukutoiminnot siinä, ja funktio, jonka parametri WriteOnlyList<Cat>kirjoittaa tyypin objekteja , on joko turvallista Catsiirtyä siihen - se ei kirjoita sinne mitään paitsi objekteja perijäluokan, mutta yritä lukea muiden objektit eivät. List<Animal>List<Object>

Funktiotyypit

Kielessä, jossa on ensiluokkaisia ​​toimintoja, on yleisiä funktiotyyppejä ja delegoituja muuttujia . Yleisille funktiotyypeille paluutyypin kovarianssi ja argumenttien kontravarianssi ovat hyödyllisiä. Jos siis delegaatti määritellään "funktioksi, joka ottaa merkkijonon ja palauttaa objektin", siihen voidaan kirjoittaa myös funktio, joka ottaa objektin ja palauttaa merkkijonon: jos funktio voi ottaa minkä tahansa objektin, se voi myös ota merkkijono; ja siitä tosiasiasta, että funktion tulos on merkkijono, seuraa, että funktio palauttaa objektin.

Toteutus kielillä

C++

C++ on tukenut kovarianttipalautustyyppejä ohitetuissa virtuaalifunktioissa vuoden 1998 standardista lähtien :

luokkaX { }; luokka A { julkinen : virtual X * f () { return new X ; } }; luokka Y : julkinen X {}; luokka B : julkinen A { julkinen : virtuaalinen Y * f () { return new Y ; } // kovarianssi mahdollistaa tarkennetun palautustyypin asettamisen ohitetussa menetelmässä };

C++:n osoittimet ovat kovariantteja: esimerkiksi perusluokan osoittimelle voidaan määrittää osoitin aliluokkaan.

C++-mallipohjat ovat yleisesti ottaen invariantteja, parametriluokkien periytyvyyssuhteita ei siirretä malleihin. Esimerkiksi kovarianttisäiliö vector<T>sallisi tyyppitarkistuksen rikkomisen. Kuitenkin käyttämällä parametroituja kopiokonstruktoreja ja osoitusoperaattoreita voit luoda älykäs osoittimen , joka on kovariantti sen tyyppiparametrin [4] kanssa .

Java

Method return -tyypin kovarianssi on otettu käyttöön Javassa J2SE 5.0 :sta lähtien . Metodiparametreissa ei ole kovarianssia: virtuaalisen menetelmän ohittamiseksi sen parametrien tyyppien on vastattava yläluokan määritelmää, muuten määritetään uusi ylikuormitettu menetelmä näillä parametreilla ohituksen sijaan .

Javan taulukot ovat olleet kovariantteja ensimmäisestä versiosta lähtien, jolloin kielellä ei vielä ollut yleisiä tyyppejä . (Jos näin ei ole, niin jos esimerkiksi käytetään kirjastomenetelmää, joka ottaa objektijoukon Object[]toimimaan merkkijonojoukon kanssa String[], se on ensin kopioitava uuteen taulukkoon Object[].) Koska, kuten mainittiin, edellä, kun kirjoitat elementtiä tällaiseen taulukkoon, voit ohittaa tyyppitarkistuksen, JVM :ssä on ylimääräinen ajonaikainen tarkistus, joka heittää poikkeuksen , kun virheellinen elementti kirjoitetaan.

Javassa yleiset tyypit ovat muuttumattomia, koska objektien kanssa toimivan yleisen menetelmän luomisen sijaan voit parametroida sen, muuttaa sen yleiseksi menetelmäksi ja säilyttää tyypin hallinnan.

Samaan aikaan Javassa voit toteuttaa eräänlaisen yleisten tyyppien yhteis- ja kontravarianssin käyttämällä jokerimerkkiä ja täyttäviä määritteitä: List<? extends Animal>tulee olemaan kovariantti rivityypin kanssa ja List<? super Animal> kontravarianssi.

C#

C# :n ensimmäisestä versiosta lähtien taulukot ovat olleet kovariantteja. Tämä tehtiin yhteensopivuuden vuoksi Java-kielen kanssa [5] . Väärän tyyppisen elementin kirjoittaminen taulukkoon aiheuttaa ajonaikaisen poikkeuksen .

Yleiset luokat ja rajapinnat, jotka ilmestyivät C# 2.0:ssa, muuttuivat, kuten Javassa, tyyppiparametrien muuttumattomiksi.

Yleisten delegaattien käyttöönoton myötä (parametrisoitu argumenttityyppien ja palautustyyppien mukaan), kieli mahdollisti tavallisten menetelmien automaattisen muuntamisen yleisiksi delegaatteiksi, joissa oli kovarianssi palautustyypeissä ja kontravarianssi argumenttityypeissä. Siksi C# 2.0:ssa tällainen koodi tuli mahdolliseksi:

void ProcessString ( String s ) { /* ... */ } void ProcessAnyObject ( Object o ) { /* ... */ } Merkkijono GetString ( ) { /* ... */ } Object GetAnyObject ( ) { /* ... */ } //... Toiminto < Merkkijono > process = ProcessAnyObject ; prosessi ( myString ); // oikeustoimet Func < Object > getter = GetString ; Objekti obj = getteri (); // oikeustoimet

koodi on kuitenkin Action<Object> process = ProcessString;virheellinen ja antaa käännösvirheen, muuten tätä edustajaa voidaan kutsua nimellä process(5), joka välittää Int32:n ProcessStringille.

C# 2.0:ssa ja 3.0:ssa tämä mekanismi salli vain yksinkertaisten menetelmien kirjoittamisen yleisille edustajille, eikä se voinut automaattisesti muuntaa yleisestä edustajasta toiseen. Toisin sanoen koodi

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

ei kääntänyt näillä kielen versioilla. Siten yleiset edustajat C# 2.0:ssa ja 3.0:ssa olivat edelleen muuttumattomia.

C# 4.0:ssa tämä rajoitus poistettiin ja tästä versiosta alkaen f2 = f1yllä olevan esimerkin koodi alkoi toimia.

Lisäksi 4.0:ssa tuli mahdolliseksi määritellä geneeristen rajapintojen ja delegaattien parametrien varianssi eksplisiittisesti. Tätä varten käytetään avainsanoja outja invastaavasti. Koska geneerisessä tyypissä tyyppiparametrin varsinainen käyttö on vain sen tekijän tiedossa ja koska se voi muuttua kehityksen aikana, tämä ratkaisu tarjoaa eniten joustavuutta tinkimättä kirjoittamisen kestävyydestä.

Jotkut kirjastoliitännät ja edustajat on toteutettu uudelleen C# 4.0:ssa näiden ominaisuuksien hyödyntämiseksi. Esimerkiksi käyttöliittymä on IEnumerable<T>nyt määritelty nimellä IEnumerable<out T>, käyttöliittymä IComparable<T> nimellä IComparable<in T>, delegoi Action<T> nimellä Action<in T>jne.

Katso myös

Muistiinpanot

  1. Microsoftin venäjänkielinen arkistokopio , joka on päivätty 24. joulukuuta 2015 Wayback Machinessa , käyttää termejä kovarianssi ja kontravariaatio .
  2. Jatkossa sana "johdannainen" ei tarkoita "perillistä".
  3. Castagna, 1995 , Abstrakti.
  4. Kovarianssi- ja C++-malleista (8. helmikuuta 2013). Haettu 20. kesäkuuta 2013. Arkistoitu alkuperäisestä 28. kesäkuuta 2013.
  5. Eric Lippert. Kovarianssi ja kontravarianssi C#:ssa, osa kaksi (17. lokakuuta 2007). Haettu 22. kesäkuuta 2013. Arkistoitu alkuperäisestä 28. kesäkuuta 2013.

Kirjallisuus

  • Castagna, Giuseppe. Kovarianssi ja kontravarianssi: konflikti ilman syytä  //  ACM Trans. ohjelmoida. Lang. Syst.. - ACM, 1995. - Voi. 17 , ei. 3 . — s. 431-447 . — ISSN 0164-0925 . - doi : 10.1145/203095.203096 .