Virtuální metoda ( virtual function ) je metoda (funkce) třídy v objektově orientovaném programování , kterou lze v podřízených třídách přepsat , takže konkrétní implementace volané metody bude určena za běhu. Programátor tedy nepotřebuje znát přesný typ objektu , aby s ním mohl pracovat prostřednictvím virtuálních metod: stačí vědět, že objekt patří do třídy nebo potomka třídy, ve které je metoda deklarována. Jeden z překladů slova virtuální z angličtiny může být „aktuální“, což je významově vhodnější.
Virtuální metody jsou jednou z nejdůležitějších technik pro implementaci polymorfismu . Umožňují vám vytvořit společný kód, který může pracovat jak s objekty základní třídy, tak s objekty kterékoli z jejích podřízených tříd. V tomto případě základní třída definuje způsob práce s objekty a kterýkoli z jejích dědiců může poskytnout konkrétní implementaci tohoto způsobu.
Některé programovací jazyky (například C++ , C# , Delphi ) vyžadují, abyste výslovně uvedli, že tato metoda je virtuální. V jiných jazycích (např. Java , Python ) jsou všechny metody ve výchozím nastavení virtuální (ale pouze ty metody, u kterých je to možné; například v Javě nelze metody se soukromým přístupem přepsat kvůli pravidlům viditelnosti).
Základní třída nemusí poskytovat implementace virtuální metody, ale pouze deklarovat její existenci. Takové metody bez implementace se nazývají „pure virtual“ (přeloženo z angličtiny pure virtual ) nebo abstraktní. Třída obsahující alespoň jednu takovou metodu bude také abstraktní . Objekt takové třídy nelze vytvořit (v některých jazycích je to povoleno, ale volání abstraktní metody bude mít za následek chybu). Dědicové abstraktní třídy musí poskytnout [1] implementaci pro všechny její abstraktní metody, jinak se naopak stanou abstraktními třídami. Abstraktní třída, která obsahuje pouze abstraktní metody, se nazývá rozhraní .
Technika volání virtuálních metod se také nazývá "dynamická (pozdní) vazba". To znamená, že název metody použitý v programu je spojen se vstupní adresou konkrétní metody dynamicky (během provádění programu), a nikoli staticky (během kompilace), protože v době kompilace je obecně nemožné určit, která z budou volány stávající implementace metod.
V kompilovaných programovacích jazycích se dynamické propojení obvykle provádí pomocí tabulky virtuálních metod , kterou kompilátor vytváří pro každou třídu, která má alespoň jednu virtuální metodu. Prvky tabulky obsahují ukazatele na implementace virtuálních metod odpovídající této třídě (pokud je v podřízené třídě přidána nová virtuální metoda, její adresa je přidána do tabulky, pokud je nová implementace virtuální metody vytvořena v potomek, odpovídající pole v tabulce je vyplněno adresou této implementace) . Pro adresu každé virtuální metody ve stromu dědičnosti tedy existuje jeden pevný posun v tabulce virtuálních metod. Každý objekt má technické pole, které se při vytvoření objektu inicializuje ukazatelem na tabulku virtuálních metod své třídy. Pro volání virtuální metody se z objektu převezme ukazatel na odpovídající tabulku virtuálních metod a z ní se známým pevným posunem ukazatel na implementaci metody použité pro tuto třídu. Při použití vícenásobné dědičnosti se situace poněkud zkomplikuje tím, že se tabulka virtuálních metod stává nelineární.
Příklad v C++ ilustrující rozdíl mezi virtuálními a nevirtuálními funkcemi:
Předpokládejme, že základní třída Animal(zvíře) může mít virtuální metodu eat(jíst, jíst, jíst). Podtřída (třída potomka) Fish(ryba) přepíše metodu eat()jinak, než Wolfby ji přepsala podtřída (vlk), ale můžete ji zavolat eat()na jakoukoli instanci třídy, která dědí z třídy, Animala získat chování eat()vhodné pro tuto podtřídu.
To umožňuje programátorovi zpracovat seznam objektů třídy Animalvoláním metody na každý objekt eat(), aniž by přemýšlel o tom, do které podtřídy aktuální objekt patří (tedy jak se konkrétní zvíře stravuje).
Zajímavým detailem virtuálních funkcí v C++ je výchozí chování argumentů . Při volání virtuální funkce s výchozím argumentem je tělo funkce převzato ze skutečného objektu a hodnoty argumentů jsou typu odkazu nebo ukazatele.
třída zvíře { veřejnost : void /*nevirtuální*/ pohyb () { std :: cout << "Toto zvíře se nějakým způsobem pohybuje" << std :: endl ; } virtuální void jíst () { std :: cout << "Zvíře něco sněz!" << std :: endl ; } virtuální ~ Animal (){} // destruktor }; třída Vlk : public Zvíře { veřejnost : void move () { std :: cout << "Vlk chodí" << std :: endl ; } void eat ( void ) { // metoda eat je přepsána a také virtuální std :: cout << "Vlk jí maso!" << std :: endl ; } }; int main () { Zvíře * zoo [] = { nový vlk (), nové zvíře ()}; pro ( zvíře * a : zoo ) { a -> přesun (); a -> jíst (); smazat a ; // Protože je destruktor virtuální, pro každý // objekt se bude volat destruktor jeho třídy } návrat 0 ; }Závěr:
Toto zvíře se nějakým způsobem pohybuje Vlk jez maso! Toto zvíře se nějakým způsobem pohybuje Zvíře něco snězte!Ekvivalentem v PHP je použití pozdní statické vazby. [2]
class Foo { public static function baz () { return 'water' ; } public function __construct () { echo static :: baz (); // pozdní statická vazba } } class Bar extends Foo { public static function baz () { return 'fire' ; } } nové foo (); // vypíše 'voda' new Bar (); // vypíše 'fire'polymorfismus jazyka Object Pascal používaného v Delphi. Zvažte příklad:
Vyhlásíme dvě třídy. Předek:
TAncestor = třída private protected public {Virtuální postup.} procedura VirtualProcedure ; virtuální; procedura StaticProcedure ; konec;a jeho potomek (Descendant):
TDescendant = třída (TAncestor) soukromý chráněný veřejný {Přepsání virtuální procedury.} procedura VirtualProcedure; přepsat; procedura StaticProcedure; konec;Jak vidíte, virtuální funkce je deklarována ve třídě předka - VirtualProcedure. Aby bylo možné využít polymorfismus, musí být v potomku přepsán .
Implementace vypadá takto:
{TAncestor} procedura Tancestor.StaticProcedure; začít ShowMessage('Statická procedura předka.'); konec; procedura Tancestor.VirtualProcedure; začít ShowMessage('Virtuální procedura předka.'); konec; {TDescendant} postup TDescendant.StaticProcedure; začít ShowMessage('Postupný statický postup.'); konec; postup TDescendant.VirtualProcedure; začít ShowMessage('Procedura přepsání potomka.'); konec;Podívejme se, jak to funguje:
procedure TForm2.BitBtn1Click(Sender: TObject); var MůjObjekt1: Tancestor; MyObject2: Tancestor; begin MyObject1 := Tancestor .Create; MůjObjekt2 := TDescendant .Create; Snaž se MyObject1.StaticProcedure; MyObject1.VirtualProcedure; MyObject2.StaticProcedure; MyObject2.VirtualProcedure; Konečně MyObject1.Free; MyObject2.Free; konec; konec;Všimněte si, že v sekci varjsme deklarovali dva objekty MyObject1a MyObject2typy TAncestor. A při tvorbě MyObject1tvořili jak TAncestor, ale MyObject2jak TDescendant. Toto vidíme, když klikneme na tlačítko BitBtn1:
Jak MyObject1je jasné, specifikované postupy byly jednoduše nazývány. Ale pro MyObject2toto tomu tak není.
Výsledkem volání byla MyObject2.StaticProcedure;"Statická procedura předka.". Koneckonců jsme deklarovali MyObject2: TAncestor, a proto se procedura StaticProcedure;třídy nazývala TAncestor.
Ale volání MyObject2.VirtualProcedure;vedlo k volání VirtualProcedure;implementovanému v potomkovi ( TDescendant). Stalo se to proto, že MyObject2nebyl vytvořen jako TAncestor, ale jako TDescendant: . A virtuální metoda byla přepsána. MyObject2 := TDescendant.Create; VirtualProcedure
V Delphi je polymorfismus implementován pomocí toho, co je známé jako virtuální tabulka metod (nebo VMT).
Docela často se zapomíná, že virtuální metody jsou přepsányoverride . To způsobí uzavření metody . V tomto případě nedojde k nahrazení metody ve VMT a nebude získána požadovaná funkčnost.
Tato chyba je sledována kompilátorem, který vydá příslušné varování.
Příklad virtuální metody v C#. Příklad používá klíčové slovo k baseposkytnutí přístupu k metodě v a()nadřazené (základní) třídě A .
class Program { static void Main ( string [] args ) { A myObj = new B (); Konzole . ReadKey (); } } // Základní třída A public class A { public virtual string a () { return "fire" ; } } //Libovolná třída B, která zdědí třídu A třídu B : A { public override string a () { return "water" ; } public B () { //Zobrazení výsledku vráceného přepsanou metodou Console . ven . WriteLine ( a ()); //water //Vypíše výsledek vrácený metodou nadřazené třídy Console . ven . WriteLine ( base.a ( ) ); //fire } }Může být nutné volat metodu předka v přepsané metodě.
Vyhlásíme dvě třídy. Předek:
TAncestor = třída private protected public {Virtuální postup.} procedura VirtualProcedure ; virtuální; konec;a jeho potomek (Descendant):
TDescendant = třída (TAncestor) soukromý chráněný veřejný {Přepsání virtuální procedury.} procedura VirtualProcedure; přepsat; konec;Volání metody předka je implementováno pomocí klíčového slova "zděděno"
postup TDescendant.VirtualProcedure; začít zděděno; konec;Stojí za to připomenout, že v Delphi musí být destruktor nutně překryt - "přepsat" - a obsahovat volání destruktoru předka
TDescendant = třída (TAncestor) soukromý chráněný veřejný destruktor Destroy; přepsat; konec; destruktor TDescendant. Zničit; začít zděděno; konec;V C++ nemusíte volat konstruktor a destruktor předka, destruktor musí být virtuální. Destruktory předků budou volány automaticky. Chcete-li zavolat metodu předka, musíte metodu explicitně zavolat:
třída Předek { veřejnost : virtual void function1 () { printf ( "Předchůdce::funkce1" ); } }; třída Potomek : veřejný Předek { veřejnost : virtual void function1 () { printf ( "Potomek::funkce1" ); Předek :: funkce1 (); // Zde se vytiskne "Ancestor::function1" } };Chcete-li zavolat konstruktor předka, musíte zadat konstruktor:
třída Potomek : veřejný Předek { veřejnost : Potomek () : Předek (){} };
V tomto příkladu třída Ancestordefinuje dvě funkce, jedna je virtuální a druhá ne. Třída Descendantpřepíše obě funkce. Zdá se však, že stejné volání funkcí dává různé výsledky. Výstup programu bude následující:
Potomek::function1() Potomek::function2() Potomek::function1() Předek::function2()To znamená, že informace o typu objektu se používají k určení implementace virtuální funkce a je volána "správná" implementace bez ohledu na typ ukazatele. Když je volána nevirtuální funkce, kompilátor se řídí typem ukazatele nebo reference, takže jsou volány dvě různé implementace function2(), i když je použit stejný objekt.
Je třeba poznamenat, že v C++ je možné v případě potřeby specifikovat konkrétní implementaci virtuální funkce, ve skutečnosti ji volat nevirtuálně:
ukazatel -> Předek :: funkce1 ();pro náš příklad vypíše Ancestor::function1() , ignoruje typ objektu.
Druhý příklad třída A { veřejnost : virtuální int funkce () { návrat 1 ; } int get () { return this -> function (); } }; třída B : veřejná A { veřejnost : int funkce () { návrat 2 ; } }; #include <iostream> int main () { Bb ; _ std :: cout << b . get () << std :: endl ; // 2 return 0 ; }I když třída B nemá metodu get() , lze si ji vypůjčit od třídy A a výsledek této metody vrátí výpočty pro B::function() !
Třetí příklad #include <iostream> pomocí jmenného prostoru std ; struct IBase { virtuální void foo ( int n = 1 ) const = 0 ; virtuální ~ IBase () = 0 ; }; void IBase::foo ( int n ) const { cout << n << "foo \n " ; } IBase ::~ IBase () { cout << "Základní destruktor \n " ; } struct Odvozené konečné : IBase { virtuální void foo ( int n = 2 ) const override final { IBase :: foo ( n ); } }; void bar ( const IBase & arg ) { arg . foo (); } int main () { pruh ( Odvozeno ()); návrat 0 ; }Tento příklad ukazuje příklad vytvoření rozhraní IBase. Na příkladu rozhraní je ukázána možnost vytvoření abstraktní třídy, která nemá virtuální metody: když je destruktor deklarován jako čistý virtuální a jeho definice je vytvořena z těla třídy, schopnost vytvářet objekty takové třídy zmizí. , ale schopnost vytvářet potomky tohoto předka zůstává.
Výstup programu bude: 1 foo\nBase destructor\n . Jak vidíme, výchozí hodnota argumentu byla převzata z typu odkazu, nikoli ze skutečného typu objektu. Stejně jako destruktor.
Klíčové slovo final označuje, že třídu nebo metodu nelze přepsat, zatímco přepsání označuje, že virtuální metoda je explicitně přepsána.