Virtuaalinen menetelmä ( virtuaalifunktio ) on olioohjelmoinnin luokan menetelmä (funktio) , joka voidaan ohittaa jälkeläisluokissa siten, että kutsuttavan menetelmän erityinen toteutus määritetään ajon aikana. Ohjelmoijan ei siis tarvitse tietää objektin tarkkaa tyyppiä voidakseen työskennellä sen kanssa virtuaalisten menetelmien avulla: riittää, että tietää, että objekti kuuluu luokkaan tai luokan jälkeläiseen, jossa menetelmä on ilmoitettu. Yksi sanan virtual käännöksistä englannista voi olla "todellinen", mikä on tarkoituksenmukaisempi.
Virtuaaliset menetelmät ovat yksi tärkeimmistä tekniikoista polymorfismin toteuttamisessa . Niiden avulla voit luoda yhteistä koodia, joka voi toimia sekä perusluokan objektien että minkä tahansa sen jälkeläisluokan objektien kanssa. Tässä tapauksessa perusluokka määrittelee tavan työskennellä esineiden kanssa, ja kuka tahansa sen perillisistä voi tarjota konkreettisen toteutuksen tästä tavasta.
Jotkin ohjelmointikielet (esimerkiksi C++ , C# , Delphi ) edellyttävät sinun nimenomaisesti osoittavan, että tämä menetelmä on virtuaalinen. Muissa kielissä (esim. Java , Python ) kaikki menetelmät ovat oletuksena virtuaalisia (mutta vain ne menetelmät, joille tämä on mahdollista; esimerkiksi Javassa yksityisiä menetelmiä ei voida ohittaa näkyvyyssääntöjen vuoksi).
Perusluokka ei välttämättä tarjoa virtuaalisen menetelmän toteutuksia, vaan ilmoittaa vain sen olemassaolon. Tällaisia menetelmiä ilman toteutusta kutsutaan "puhtaasti virtuaaliseksi" (käännetty englanniksi pure virtual ) tai abstrakteiksi. Luokka, joka sisältää vähintään yhden tällaisen menetelmän, on myös abstrakti . Tällaisen luokan objektia ei voida luoda (joissakin kielissä se on sallittua, mutta abstraktin menetelmän kutsuminen johtaa virheeseen). Abstraktin luokan perijien on tarjottava [1] toteutus kaikille sen abstrakteille menetelmille, muuten ne puolestaan ovat abstrakteja luokkia. Abstraktia luokkaa, joka sisältää vain abstrakteja menetelmiä, kutsutaan käyttöliittymäksi .
Virtuaalisten menetelmien kutsumistekniikkaa kutsutaan myös "dynaamiseksi (myöhään) sitomiseksi". Tämä tarkoittaa, että ohjelmassa käytetty menetelmän nimi liittyy tietyn menetelmän syöttöosoitteeseen dynaamisesti (ohjelman suorituksen aikana), ei staattisesti (kääntämisen aikana), koska käännösaikana on yleensä mahdotonta määrittää, mikä olemassa olevia menetelmätoteutuksia kutsutaan.
Käännetyissä ohjelmointikielissä dynaaminen linkitys tehdään yleensä käyttämällä virtuaalista menetelmätaulukkoa , jonka kääntäjä luo jokaiselle luokalle, jolla on vähintään yksi virtuaalinen menetelmä. Taulukon elementit sisältävät osoittimia tätä luokkaa vastaavien virtuaalimenetelmien toteutuksiin (jos jälkeläisluokkaan lisätään uusi virtuaalinen menetelmä, sen osoite lisätään taulukkoon, jos virtuaalimenetelmän uusi toteutus luodaan jälkeläinen luokka, taulukon vastaava kenttä täytetään tämän toteutuksen osoitteella) . Siten perintöpuun jokaisen virtuaalisen menetelmän osoitteelle on yksi kiinteä siirtymä virtuaalimenetelmätaulukossa. Jokaisella oliolla on tekninen kenttä, joka objektia luotaessa alustetaan osoittimella luokkansa virtuaalimenetelmien taulukkoon. Virtuaalimetodin kutsumiseksi objektista otetaan osoitin vastaavaan virtuaalimenetelmien taulukkoon ja siitä tunnetulla kiinteällä siirtymällä osoitin tälle luokalle käytetyn menetelmän toteutukseen. Käytettäessä useampaa periytymistä tilanne muuttuu hieman monimutkaisemmaksi, koska virtuaalimenetelmätaulukosta tulee epälineaarinen.
Esimerkki C++ :sta, joka havainnollistaa eroa virtuaalisten ja ei-virtuaalisten funktioiden välillä:
Oletetaan, että perusluokalla Animal(eläimellä) voi olla virtuaalinen menetelmä eat(syö, syö, syö). Alaluokka (lapsiluokka) Fish(kala) ohittaa menetelmän eat()eri tavalla kuin alaluokka Wolf(susi) ohittaisi sen, mutta voit kutsua sitä eat()missä tahansa luokan ilmentymässä, joka perii luokasta ja saada kyseiselle alaluokalle sopivan Animalkäyttäytymisen .eat()
Tämä antaa ohjelmoijalle mahdollisuuden käsitellä luokkaobjektien luetteloa Animalkutsumalla jokaiselle objektille metodia eat()ajattelematta, mihin alaluokkaan nykyinen objekti kuuluu (eli kuinka tietty eläin syö).
Mielenkiintoinen yksityiskohta virtuaalifunktioista C++:ssa on argumenttien oletuskäyttäytyminen . Kutsuttaessa virtuaalista funktiota oletusargumentilla, funktion runko otetaan todellisesta objektista ja argumenttien arvot ovat viite- tai osoitintyyppisiä.
luokka eläin { julkinen : void /*ei-virtuaalinen*/ liikkua () { std :: cout << "Tämä eläin liikkuu jollain tavalla" << std :: endl ; } virtuaalinen void syö () { std :: cout << "Eläimet syövät jotain!" << std :: endl ; } virtuaalinen ~ Eläin (){} // tuhoaja }; luokka susi : julkinen eläin { julkinen : void move () { std :: cout << "Susi kävelee" << std :: endl ; } void eat ( void ) { // menetelmä eat on ohitettu ja myös virtuaalinen std :: cout << "Susi syö lihaa!" << std :: endl ; } }; int main () { Eläin * eläintarha [] = { uusi susi (), uusi eläin ()}; ( Eläin * a : eläintarha ) { _ a -> siirrä (); a -> syödä (); poista a ; // Koska tuhoaja on virtuaalinen, jokaisen //-objektin luokkansa tuhoajaa kutsutaan nimellä } paluu 0 ; }Johtopäätös:
Tämä eläin liikkuu jollain tavalla Susi syö lihaa! Tämä eläin liikkuu jollain tavalla Eläin syö jotain!Vastaava PHP:ssä on myöhäisen staattisen sitomisen käyttö. [2]
class Foo { julkinen staattinen funktio baz () { return 'vesi' ; } julkinen funktio __konstrukti () { echo static :: baz (); // myöhäinen staattinen sidonta } } class Bar extends Foo { public staattinen funktio baz () { return 'tuli' ; } } uusi foo (); // tulostaa 'vesi' new Bar (); // tulostaa "tuli"Delphissä käytetyn Object Pascal -kielen polymorfismi . Harkitse esimerkkiä:
Ilmoitetaan kaksi luokkaa. Esi-isä:
Tancestor = luokka yksityinen suojattu julkinen {Virtuaalinen menettely.} menettely VirtualProcedure ; virtuaalinen; menettely StaticProcedure ; loppu;ja sen jälkeläinen (Descendant):
TDescendant = luokka (TAncestor) yksityinen suojattu julkinen {Virtuaalimenettelyn ohitus.} menettely VirtualProcedure; ohittaa; menettely StaticProcedure; loppu;Kuten näet, virtuaalinen funktio on ilmoitettu esi-isäluokassa - VirtualProcedure. Jotta polymorfismia voitaisiin hyödyntää, se on ohitettava jälkeläisessä.
Toteutus näyttää tältä:
{TAancestor} menettely Tancestor.StaticProcedure; alkaa ShowMessage('Esi-staattinen menettely.'); loppu; menettely Tancestor.VirtualProcedure; alkaa ShowMessage('Esi-virtuaalimenettely.'); loppu; {TDescendant} menettely TDescendant.StaticProcedure; alkaa ShowMessage('Descendant-staattinen menettely.'); loppu; menettely TDescendant.VirtualProcedure; alkaa ShowMessage('Descendant override.'); loppu;Katsotaan kuinka se toimii:
menettely TForm2.BitBtn1Click(Lähettäjä: TObject); var OmaObject1: Tancestor; OmaObject2: Tancestor; begin MyObject1 := TAancestor .Create; MyObject2 := TDescendant .Create; yrittää MyObject1.StaticProcedure; OmaObject1.VirtualProcedure; OmaObject2.StaticProcedure; OmaObject2.VirtualProcedure; vihdoinkin OmaObject1.Free; OmaObject2.Free; loppu; loppu;Huomaa, että osiossa varolemme ilmoittaneet kaksi objektia MyObject1ja MyObject2tyyppiä TAncestor. Ja luodessaan MyObject1he loivat miten TAncestor, mutta MyObject2miten TDescendant. Näemme tämän, kun napsautamme painiketta BitBtn1:
Kaikki on MyObject1selvää, määritellyt menettelyt kutsuttiin yksinkertaisesti. Mutta MyObject2tämä ei ole niin.
Puhelu MyObject2.StaticProcedure;johti "Ancestor-staattiseen menettelyyn". Loppujen lopuksi julistimme MyObject2: TAncestor, ja siksi StaticProcedure;luokkaproseduuria kutsuttiin TAncestor.
Mutta kutsu MyObject2.VirtualProcedure;johti kutsuun , joka VirtualProcedure;toteutettiin jälkeläisessä ( TDescendant). Tämä tapahtui, koska se MyObject2ei luotu nimellä TAncestor, vaan nimellä TDescendant: . Ja virtuaalinen menetelmä ohitettiin. MyObject2 := TDescendant.Create; VirtualProcedure
Delphissa polymorfismi toteutetaan käyttämällä niin kutsuttua virtuaalista menetelmätaulukkoa (tai VMT).
Melko usein virtuaaliset menetelmät unohdetaan ohittaa . overrideTämä saa menetelmän sulkeutumaan . Tässä tapauksessa menetelmän korvaamista ei tapahdu VMT:ssä eikä vaadittua toiminnallisuutta saada.
Kääntäjä seuraa tätä virhettä ja antaa asianmukaisen varoituksen.
Esimerkki virtuaalisesta menetelmästä C#:ssa. Esimerkissä käytetään avainsanaa basepääsyn menetelmään a()yläluokan (perus) luokassa A .
class Ohjelma { static void Main ( merkkijono [] args ) { A myObj = new B (); konsoli . ReadKey (); } } // Perusluokka A public class A { public virtual string a () { return "fire" ; } } //Mielivaltainen luokka B, joka perii luokan A luokan B : A { public override string a () { return "vesi" ; } public B () { //Näytä ohitetun menetelmän palauttama tulos Console . ulos . WriteLine ( a ()); //vesi //Lähetä tulos, joka palautetaan emoluokan Console menetelmällä . ulos . WriteLine ( base.a ( ) ); //palo } }Voi olla tarpeen kutsua esi-isämenetelmää ohitetussa menetelmässä.
Ilmoitetaan kaksi luokkaa. Esi-isä:
Tancestor = luokka yksityinen suojattu julkinen {Virtuaalinen menettely.} menettely VirtualProcedure ; virtuaalinen; loppu;ja sen jälkeläinen (Descendant):
TDescendant = luokka (TAncestor) yksityinen suojattu julkinen {Virtuaalimenettelyn ohitus.} menettely VirtualProcedure; ohittaa; loppu;Kutsu esi-isä-menetelmään toteutetaan avainsanalla "peritty"
menettely TDescendant.VirtualProcedure; alkaa peritty; loppu;On syytä muistaa, että Delphissä destruktorin on välttämättä oltava päällekkäinen - "ohita" - ja sisältää kutsun esivanhemmille.
TDescendant = luokka (TAncestor) yksityinen suojattu julkinen tuhoaja Tuhoa; ohittaa; loppu; destructor TDescendant. Tuhota; alkaa peritty; loppu;C++:ssa sinun ei tarvitse kutsua esi-isän konstruktoria ja tuhoajaa, tuhoajan on oltava virtuaalinen. Esivanhempien tuhoajia kutsutaan automaattisesti. Jotta voit kutsua esi-isämenetelmää, sinun on kutsuttava menetelmä erikseen:
luokan esi-isä { julkinen : virtual void function1 () { printf ( "Ancessor::function1" ); } }; luokka Jälkeläinen : julkinen esi-isä { julkinen : virtuaalinen void function1 () { printf ( "Jälkeläinen::funktio1" ); Esi-isä :: toiminto1 (); // "Ancestor::function1" tulostetaan tähän } };Jotta voit kutsua esi-isäkonstruktorin, sinun on määritettävä konstruktori:
luokka Jälkeläinen : julkinen esi-isä { julkinen : Jälkeläinen () : Esi-isä (){} };
Tässä esimerkissä luokka Ancestormäärittelee kaksi funktiota, joista toinen on virtuaalinen ja toinen ei. Luokka Descendantohittaa molemmat funktiot. Vaikuttaa kuitenkin siltä, että sama kutsu funktioille antaa erilaisia tuloksia. Ohjelman tulos on seuraava:
Jälkeläinen::function1() Jälkeläinen::function2() Jälkeläinen::function1() Esi-isä::funktio2()Eli tietoa objektin tyypistä käytetään määrittämään virtuaalifunktion toteutus, ja "oikeaa" toteutusta kutsutaan osoittimen tyypistä riippumatta. Kun ei-virtuaalista funktiota kutsutaan, kääntäjää ohjaa osoitin tai viittaustyyppi, joten kahta eri toteutusta kutsutaan function2(), vaikka käytetään samaa objektia.
On huomattava, että C++:ssa on mahdollista tarvittaessa määrittää tietty virtuaalisen funktion toteutus, itse asiassa kutsumalla sitä ei-virtuaalisesti:
osoitin -> Esi -isä :: toiminto1 ();esimerkissämme tulostaa Ancestor::function1() , ohittaen objektin tyypin.
Toinen esimerkki luokka A { julkinen : virtuaalinen int- funktio () { paluu 1 ; } int get () { palauta tämä -> funktio (); } }; luokka B : julkinen A { julkinen : int funktio () { paluu 2 ; } }; #include <iostream> int main () { Bb ; _ std :: cout << b . get () << std :: endl ; // 2 paluu 0 ; }Vaikka luokassa B ei ole get() - metodia , se voidaan lainata luokasta A ja tämän menetelmän tulos palauttaa B::function() -laskelmat !
Kolmas esimerkki #include <iostream> käyttäen nimiavaruutta std ; struct IBase { virtuaalinen void foo ( int n = 1 ) const = 0 ; virtuaalinen ~ IBase () = 0 ; }; void IBase::foo ( int n ) const { cout << n << "foo \n " ; } IBase ::~ IBase () { cout << " Perustuhoaja \n " ; } struct Johdettu lopullinen : IBase { virtuaalinen void foo ( int n = 2 ) const override final { IBase :: foo ( n ); } }; void bar ( const IBase & arg ) { arg . foo (); } int main () { baari ( johdettu ()); paluu 0 ; }Tässä esimerkissä on esimerkki IBase-liittymän luomisesta. Käyttöliittymän esimerkin avulla on esitetty mahdollisuus luoda abstrakti luokka, jossa ei ole virtuaalisia menetelmiä: kun destructor julistetaan puhtaaksi virtuaaliseksi ja sen määritelmä tehdään luokan rungosta, kyky luoda tällaisen luokan objekteja katoaa. , mutta kyky luoda tämän esi-isän jälkeläisiä säilyy.
Ohjelman tulos on: 1 foo\nBase destructor\n . Kuten näemme, argumentin oletusarvo on otettu linkin tyypistä, ei objektin todellisesta tyypistä. Aivan kuten tuhoaja.
Viimeinen avainsana osoittaa, että luokkaa tai menetelmää ei voi ohittaa, kun taas ohitus osoittaa, että virtuaalinen menetelmä on nimenomaisesti ohitettu.