Czym jest polimorfizm?

Metody wirtualne oraz metody abstrakcyjne są ściśle związane z mechanizmem polimorfizmu. Polimorfizm jest jednym z filarów paradygmatu programowania obie...
3 downloads 0 Views 104KB Size
Metody wirtualne oraz metody abstrakcyjne są ściśle związane z mechanizmem polimorfizmu. Polimorfizm jest jednym z filarów paradygmatu programowania obiektowego. Jak wiadomo język C# jest w całości językiem obiektowym, dlatego tak ważne jest aby zapoznać się z jego podstawowymi konstrukcjami. Dowiesz się także, kiedy lepiej wybrać metodę wirtualną a kiedy abstrakcyjną.

Czym jest polimorfizm? Na początku trochę suchej teorii. Polimorfizm jest jednym z czterech podstawowych założeń programowania obiektowego. Opisując najprościej, polimorfizm polega na zdolności obiektu do różnych zachowań zależnie od bieżącego wykonania programu. Polimorfizm to filar programowania obiektowego. Polega na różnym zachowaniu tych samych metod polimorficznych (o tych samych deklaracjach) w klasach będących w relacji dziedziczenia. Metodami polimorficznymi (czyli obsługującymi mechanizm polimorfizmu) są w C# metody wirtualne oraz metody abstrakcyjne. Do powyższej definicji polimorfizmu można się przyczepić, stricte definiuje ona polimorfizm dynamiczny. Oprócz tego w C# występuje także polimorfizm statyczny. Jednak to właśnie wiązania dynamiczne są sednem polimorfizmu, dlatego zdecydowałem się uogólnić definicję właśnie dla tego typu wielopostaciowości. Więcej szczegółów znajdziesz w kolejnych akapitach.

Komu i po co to potrzebne? Jednym przeznaczeniem polimorfizmu jest znaczne

Karol Trybulec | p-programowanie.pl | 1

skrócenie i uproszczenie kodu programu. Więcej na ten temat w kolejnych akapitach. Polimorfizm statyczny w C# Polimorfizmem statycznym w C# jest przeciążanie funkcji i operatorów. W tej samej klasie może istnieć wiele metod o tej samej nazwie, różniących się tylko parametrami. Nie jest to zaskakujące, jednak warto wiedzieć, że przeciążanie także nazywane jest polimorfizmem. W tym artykule nie będę tego opisywał, jest to dobry temat na osobny wpis. Polimorfizm statyczny zachodzi podczas kompilacji programu. Nie mamy wpływu na zachowanie metod podczas działania aplikacji. To, która metoda zostanie wybrana jest postanowione już na etapie komplikacji i zależy od ilości przekazanych do metody argumentów. Polimorfizm dynamiczny w C# Polimorfizm dynamiczny w C# to przesłanianie funkcji. Jest to aspekt, który najbardziej nas interesuje. Rzadko używane jest pojęcie ‚polimorfizmu dynamicznego’, przeważnie pisze się tylko ‚polimorfizm’ i takiej terminologii będę się dalej trzymał. Polimorfizm został opisany w akapicie wyższej. Co należy zapamiętać, aby włączyć mechanizm polimorfizmu, musimy w danej klasie utworzyć dowolną metodę polimorficzną. Na metody polimorficzne składają się metody wirtualne oraz metody abstrakcyjne (czysto wirtualne). Metody abstrakcyjne są przesłaniane, dzięki temu osiąga się wielopostaciowość interfejsu polimorficznego w obrębie klas znajdujących się w relacji dziedziczenia. Metody polimorficzne przesłania się słowem kluczowym override. Używanie terminu „przysłanianie” zamiast „przesłanianie” w terminologii paradygmatu programowania obiektowego jest błędne (a niestety często spotykane). Interfejs polimorficzny Ściśle z polimorfizmem związane jest pojęcie interfejsu polimorficznego. Dla danego związku klas będących w relacji dziedziczenia, tworzymy jeden interfejs polimorficzny i osiągamy różne zachowanie tych samych metod, narzuconych przez interfejs. Nie jest to artykuł o interfejsach występujących w C#, jednak czym one w ogóle są? Interfejsy to „umowa”, mówiąca jakie metody musi zaimplementować klasa rozszerzająca dany interfejs.

Karol Trybulec | p-programowanie.pl | 2

Interfejs polimorficzny to „umowa”, mówiąca jakie metody polimorficzne muszą implementować klasy pochodne. Dzięki temu grupa klas posiada jeden interfejs polimorficzny ale wiele tych samych metod o różnych zachowaniach (wielopostaciowość metod polimorficznych). Przykładowo, możemy utworzyć interfejs polimorficzny dla klasy Pracownik. Jedną z metod klasy będzie metoda Pracuj(). Z klasy pracownik będzie dziedziczyła klasa Szef i Sekretarka. Metoda Pracuj() będzie się zachowywać inaczej dla każdej klasy pochodnej, a to jak będzie się zachowywać będzie zależało od typu statycznego obiektu, na rzecz którego jest wywoływana. Uwaga! Nie można mylić interfejsu polimorficznego ze zwykłym interfejsem w języku C#. Interfejs to konstrukcja programistyczna deklarowana słowem interface, a interfejs polimorficzny to zbiór metod polimorficznych (wirtualnych i abstrakcyjnych) występujących w klasie bazowej, których implementacją obarczamy klasy pochodne.

Przesłanianie zwykłych metod Cała idea wielopostaciowości kręci się w okół dziedziczenia klas oraz przesłaniania metod. Przesłanianie metod wywodzi się z polimorfizmu dynamicznego. W przypadku zwykłego przesłonięcia metody, kompilator ostrzega nas i pyta, czy jest to aby na pewno efekt przez nas pożądany: 1 2 3 4 5 6 7 8 9 10

public class Pracownik { public void Pracuj() { Console.WriteLine("Pracownik.Pracuj()"); } } public class Sekretarka : Pracownik { //przesłonięcie metody public void Pracuj() { Console.WriteLine("Sekretarka.Pracuj()"); } }

Kod się kompiluje jednak występuje ostrzeżenie: Warning ‚Sekretarka.Pracuj()’ hides inherited member ‚Pracownik.Pracuj()’. Use the new keyword if hiding was intended. Aby pozbyć się ostrzeżenia, wystarczy w linijce nr. 9 dodać słowo kluczowe new. Jedynym zadaniem new jest powiadomienie kompilatora, że przesłaniamy funkcję niepolimorficzną świadomie. Takie przesłanianie niepolimorficznych funkcji nie jest poprawne, przeważnie oznacza błędnie

Karol Trybulec | p-programowanie.pl | 3

zaprojektowany program. Jakie są skutki takiego przesłonięcia funkcji? W przypadku rzutowania klasy pochodnej na typ klasy bazowej, zostanie wywołana metoda typu statycznego: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

public class Pracownik { public void Pracuj() { Console.WriteLine("Pracownik.Pracuj()"); } } public class Sekretarka : Pracownik { // dodanie new usuwa ostrzeżenie kompilatora // jednak new nie jest wymagane - nic nie zmienia new public void Pracuj() { Console.WriteLine("Sekretarka.Pracuj()"); } } static void Main(string[] args) { Pracownik p = new Sekretarka(); p.Pracuj(); // zwraca: Pracownik.Pracuj(); Console.ReadKey(); }

Od teraz, trzymaj się takich konstrukcji programistycznych z daleka. Na początku zapewne nie dostrzegasz wad tego rozwiązania, jednak zrozumiesz je w dalszych akapitach. Podstawową z takich wad jest znaczące powielanie kodu. W tym momencie wkracza polimorfizm. Aby korzystać z jego zalet, będziemy przesłaniać metody polimorficzne, co jest opisane w następnym akapicie.

Metody polimorficzne i przesłanianie metod W C# istnieją dwa typy metod polimorficznych, które uruchamiają mechanizm polimorfizmu. Są to metody wirtualne oraz metody abstrakcyjne. Ich przedrostki dopisujemy do dowolnej metody danej klasy. Niżej są one omówione bardziej szczegółowo: Metody wirtualne Metoda wirtualna w C# jest metodą polimorficzną. Jest to funkcja składowa dowolnej klasy oznaczona słowem kluczowym virtual. Metoda wirtualna (funkcja wirtualna) jest metodą składową klasy, której wywołanie zależy

Karol Trybulec | p-programowanie.pl | 4

od typu dynamicznego obiektu. Jej użycie włącza mechanizm polimorfizmu dynamicznego. Metodę wirtualną tworzy się w klasie bazowej. We wszystkich klasach pochodnych można polimorficznie przesłonić jej nazwę i zapewnić jej inną implementację. 1 public class Pracownik 2{ 3 // metoda wirtualna 4 virtual public void Pracuj() { Console.WriteLine("Pracownik.Pracuj()"); } 5} Jaka jest różnica między metodą wirtualną a zwykłą? Różnica polega na zachowaniu wywoływania przesłoniętych metod podczas dziedziczenia klas. Rozważmy przykład znany z akapitu o przesłanianiu metod, wzbogacony o funkcję wirtualną: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

public class Pracownik { // funkcja wirtualna virtual public void Pracuj() { Console.WriteLine("Pracownik.Pracuj()"); } } public class Sekretarka : Pracownik { // przesłaniamy override public void Pracuj() { Console.WriteLine("Sekretarka.Pracuj()"); } } static void Main(string[] args) { Pracownik p = new Sekretarka(); p.Pracuj(); // UWAGA! zwraca: Sekretarka.Pracuj(); Console.ReadKey(); }

W przypadku przesłonięcia metody niewirtualnej, zostanie wywołana metoda typu statycznego wskazywanego przez referencję, czyli metoda z klasy Pracownik. Ponieważ użyliśmy metody wirtualnej (polimorficznej), mimo rzutowania instancji klasy Sekretarka na typ bazowy Pracownik, wywołana zostaje metoda wirtualna klasy Sekretarka. Występuje tutaj zależność od typu dynamicznego, a nie statycznego jak w przypadku zwykłego przesłaniania. Zauważ! Metoda klasy bazowej jest wirtualna. Nie musimy jej przesłaniać, ale jeżeli

Karol Trybulec | p-programowanie.pl | 5

chcemy to musimy to zrobić za pomocą słowa kluczowego override. Kilka wniosków na temat metod wirtualnych

metody, która nie jest wirtualna, nie można przesłonić poprzez użycie override metody statyczne ani prywatne nie mogą być wirtualne metoda wirtualna może być przesłonięta w klasie pochodnej – ale nie musi. W przypadku braku przesłonięcia zostanie wywołana metoda klasy bazowej Najważniejszą cechą metod wirtualnych, jest ta opisana wyższej. Nie jesteśmy zmuszeni do przesłonięcia metody wirtualnej w klasie bazowej. Jest to główna różnica w stosunku do metod abstrakcyjnych. Każda klasa w C# dziedziczy niejawnie z klasy Object, która z kolei udostępnia kilka metod wirtualnych. 1 public class Object 2{ 3 /* składowe wirtualne */ 4 virtual public bool Equals(object o); 5 virtual protected void Finalize(); 6 virtual public string ToString(); 7 /* itp.. */ 8} Dzięki temu, możesz zawsze je nadpisać za pomocą override, nawet jeżeli jawnie nie rozszerzasz Object. Ponieważ to metody wirtualne, nie musisz ich przesłonić. W przypadku braku przesłonięcia zostanie wywołana implementacja domyślna z kasy bazowej Object. Metody abstrakcyjne Metoda abstrakcyjna w C# jest metodą polimorficzną. Musi być zadeklarowana jako funkcja składowa abstrakcyjnej klasy, poprzedzona przedrostkiem abstract. Metoda abstrakcyjna zachowuje się dokładnie tak samo jak metoda wirtualna, jedyna różnica leży w braku definicji ciała funkcji. Tworząc metodę abstrakcyjną deklarujesz funkcję (deklaracja to typ zwracany, nazwa i argumenty) ale nie definiujesz ciała funkcji. Metodę abstrakcyjną można zadeklarować tylko w klasie abstrakcyjnej (także poprzedzonej słowem abstract). Dlaczego tak jest? Klasa abstrakcyjna jest klasą, której instancji nie da się stworzyć. Można po niej tylko dziedziczyć, rozszerzając ją o inne klasy. Klasy dziedziczące muszą implementować metody abstrakcyjne klasy abstrakcyjnej. Gdyby

Karol Trybulec | p-programowanie.pl | 6

dało się utworzyć instancję klasy abstrakcyjnej, doszło by do sytuacji, że istnieje w niej abstrakcyjna funkcja bez definicji ciała. Za zdefiniowanie ciała funkcji abstrakcyjnej odpowiedzialne są klasy pochodne. Odbywa się to poprzez przesłanianie słowem override tak samo jak w funkcjach wirtualnych. Krótki przykład: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

abstract public class Pracownik { // funkcja abstrakcyjna, bez ciala funkcji(!) abstract public void Pracuj(); } public class Sekretarka : Pracownik { // przesłaniamy override public void Pracuj() { Console.WriteLine("Sekretarka.Pracuj()"); } } static void Main(string[] args) { Pracownik p = new Sekretarka(); p.Pracuj(); // UWAGA! zwraca: Sekretarka.Pracuj(); Console.ReadKey(); }

Warto dodać, że klasa abstrakcyjna może posiadać zwykłe metody, posiadające ciało funkcji mimo posiadania metod abstrakcyjnych. Cała reszta działa tak samo jak w funkcjach wirtualnych. Wywoływana jest przesłonięta metoda, biorąc pod uwagę typ dynamiczny. Podsumowanie metod abstrakcyjnych

działają tak samo jak funkcje wirtualne, oprócz tego że nie podaje się definicji ciała funkcji w klasie bazowej muszą być zadeklarowane w klasie abstrakcyjnej nie mogą być prywatne ani statyczne klasa pochodna musi przesłonić wszystkie metody abstrakcyjne

Kiedy używać metod wirtualnych a kiedy abstrakcyjnych? Zarówno metody abstrakcyjne jak i metody wirtualne są polimorficzne i prawie niczym się nie różnią. Nasuwa się więc pytanie: kiedy używać jednych a kiedy drugich? Odpowiedź

Karol Trybulec | p-programowanie.pl | 7

na to pytanie jest prosta. Metod wirtualnych używamy wszędzie tam, gdzie może wystąpić powielanie kodu w przesłanianych funkcjach. Dlaczego? Używając polimorfizmu i metod wirtualnych, możemy wywołać po typie dynamicznym metodę wirtualną klasy pochodnej (domyślnie), a jeśli zechcemy także metodę klasy pochodnej i bazowej. Odbywa się to dzięki słowu kluczowemu base odnoszącemu się do klasy bazowej. Prosty przykład: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

public class Pracownik { // funkcja wirtualna virtual public void Pracuj() { Console.WriteLine("Autoryzacja"); Console.WriteLine("Sprawdź zlecenia"); } } public class Sekretarka : Pracownik { // przesłaniamy override public void Pracuj() { base.Pracuj(); Console.WriteLine("Obowiazki sekretarki"); } } static void Main(string[] args) { Pracownik p = new Sekretarka(); p.Pracuj(); //Autoryzacja //Sprawdz zlecenia //Obowiazki sekretarki Console.ReadKey(); }

W przykładzie wyżej, każdy pracownik przed rozpoczęciem pracy musi przejść proces autoryzacji a następnie sprawca zlecenia na dziś. Dopiero po tym etapie zaczyna właściwe dla swojego stanowiska obowiązki. Jeżeli stworzylibyśmy reprezentację klas dla dużej ilości pracowników, błędem byłoby użycie metody abstrakcyjnej. Doskonale za to naddaje się tutaj metoda wirtualna, ponieważ zapobiegamy powielaniu kodu. Obowiązki wspólne dla każdego pracownika są

Karol Trybulec | p-programowanie.pl | 8

wywoływane z metody wirtualnej klasy bazowej, a specjalistyczne obowiązki poszczególnych stanowisk dopiero później przesłaniając funkcję wirtualną. Odwrotnie wygląda sprawa z metodami abstrakcyjnymi. Jeżeli masz pewność, że nie zajdzie zjawisko powielania kodu, możesz ich użyć. Przykładowy program dotyczący figur geometrycznych: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

abstract public class Figura { protected int a,b,c; abstract public int Pole(); } public class Kwadrat : Figura { override public int Pole() { return a*a; } } public class Trojkat : Figura { override public int Pole() { return 1/2*a*b; } }

W tym przykładzie użycie metody wirtualnej nie miałoby sensu. Pole powierzchni dla każdej figury liczy się zupełnie inaczej, więc wygodnie nam było użyć metody abstrakcyjnej. Dzięki temu mamy pewność, że klasa pochodna musi dostarczyć własną definicję ciała metody Pole(). W przypadku metod wirtualnych klasa pochodna nie musi przesłonić metody klasy bazowej.

Polimorfizm w C++, C# i Javie Polimorfizm występuje we wszystkich obiektowych językach programowania. Generalnie zawsze polega on na tym samym – wielopostaciowości przesłanianych metod wirtualnych. Małe różnice występują natomiast w specyficznych elementach danego języka. C++ vs C# W języku C++ uruchamiamy polimorfizm dodając do klasy dowolną funkcję wirtualną. W klasach pochodnych przesłaniamy funkcję, jednak nie poprzedzamy jej już żadnym słowem kluczowym. W C# konieczne jest dodanie override. W C++ dopuszczalne jest dopisanie

Karol Trybulec | p-programowanie.pl | 9

virtual jednak nie jest to wymagane, a skoro nie jest to wiadomo – nie dopisujemy. Kod w C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

class Pracownik { public: virtual void Pracuj() { cout