Tematem
niniejszego artykułu będzie dziedziczenie oraz polimorfizm. Oba te pojęcia są
jednymi z najważniejszych z punktu widzenia programowania obiektowego. Bowiem
prędzej czy później każdy z nas w swoich programach będzie musiał zdefiniować
pewną ogólną klasę, która będzie definiować cechy wspólne dla zestawu
pozostałych elementów. Co się za tym kryje? A no to, że klasę taką będzie mogła
dziedziczyć inna klasa, która będzie bardziej specyficzna od tej odziedziczonej
(ogólnej), i która z kolei będzie dodawać następne unikatowe cechy w swojej
strukturze. Oczywiście tę nową klasę może dziedziczyć inna i proces
dziedziczenia będzie się rozbudowywał tworząc prawidłową hierarchię w naszym
kodzie. Z dziedziczeniem ściśle powiązany jest polimorfizm (poli oznacza wiele, zaś morf to forma), o którym dzisiaj będzie
również kilka słów.
Po
zdefiniowaniu powyższych nowych dla nas pojęć, postaramy się w oparciu o
przykłady wprowadzić kolejne, takie jak metody:
wirtualne i przysłaniające, czy
słowo kluczowe: base, bez których to
dziedziczenie oraz polimorfizm nie może żyć.
Zacznijmy
więc od prawidłowego zdefiniowania dziedziczenia. Jak wiemy, programowanie
obiektowe polega na rozbudowie istniejącego już kodu. Nie należy jednak nanosić
poprawek do wcześniej zdefiniowanych klas (oczywiście, jeśli są dobrze
zaprojektowane), tylko definiować kolejne klasy, które będą przejmować
właściwości oraz cechy tych już istniejących. Takie postępowanie możliwe jest
dzięki dziedziczeniu.
Załóżmy, że
mamy klasę A, która dziedziczy po klasie B. W takiej sytuacji mówi się, że
klasa B jest klasą bazową klasy A,
natomiast klasa A jest klasą pochodną
klasy B. W tym przypadku dziedziczenie polega na tym, że klasa A dziedziczy
wszystkie cechy oraz zachowania klasy B, a także zawiera wyspecjalizowane
składowe, dzięki którym można wykonać określone zadania.
W języku C#
2.0 klasę pochodną tworzy się w ten sposób, że po nazwie klasy pochodnej
umieszczamy dwukropek, a po nim nazwę klasy bazowej. Na przykład:
public class PierwszaKlasa
{
public int a;
public int b;
public void WyswietlAB()
{
System.Console.WriteLine("Wartości a i b wynoszą odpowiednio: {0} i {1}",
a, b +".");
}
}
public class DrugaKlasa : PierwszaKlasa
{
public int c;
public void WyswietlC()
{
System.Console.WriteLine("Wartość liczby c wynosi: {0}", c +".");
}
public void
SumaLiczb()
{
int suma = a + b + c;
System.Console.WriteLine("Suma liczb wynosi: {0}", suma +".");
}
}
class Glowna
{
public static void Main(string[] args)
{
PierwszaKlasa
pr = new PierwszaKlasa();
DrugaKlasa
dr = new DrugaKlasa();
//przypisanie wartosci zmiennym w obiekcie pr
typu PierwszaKlasa i wywolanie
metody pobierajacej te wartosci
pr.a = 3;
pr.b = 7;
pr.WyswietlAB();
//klasa pochodna ma dostep do wszystkich
skladowych publicznych pochodzacych z klasy, od ktorej je dziedziczy
dr.a = 2;
dr.b = 8;
dr.c = 5;
dr.WyswietlAB();
dr.WyswietlC();
dr.SumaLiczb();
}
}
W powyższym przykładzie zdefiniowaliśmy sobie dwie klasy: PierwszaKlasa, w której zadeklarowaliśmy
sobie 2 zmienne składowe (a i b), których wartości będziemy wyświetlać za
pomocą metody: WyswietlAB() oraz
klasę DrugaKlasa, w której
zdefiniowaliśmy zmienną składową c, której wartość będziemy pobierać za pomocą
metody WyswietlC(). Klasa DrugaKlasa dziedziczy wszystkie cechy
oraz właściwości od klasy PierwszaKlasa. W
ten sposób w klasie: DrugaKlasa, będziemy
mieli dostęp zarówno do zmiennych a i b
pierwszej klasy jak i do metody WyswietlAB(),
którą również zainicjalizowaliśmy w klasie: PierwszaKlasa.
Po
uruchomieniu otrzymamy następujące wyniki:

Poznaliśmy
więc w ten sposób pojęcie dziedziczenia, z którym to wiążą się dwie istotne
rzeczy. Po pierwsze, dziedziczenie to po prostu wielokrotne wykorzystywanie
kodu. Bowiem w klasie: DrugaKlasa możemy
powtórnie wykorzystać elementy pochodzące z klasy bazowej: PierwszaKlasa. Po drugie – co jest na pewno istotniejszym aspektem
dziedziczenia – wiąże się z polimorfizmem, o którym w tym miejscu będzie kilka
słów.
Jak już
wyżej zostało napisane, polimorfizm to używanie danego typu w wielu formach
niezależnie od klas, jakie dostarczają te typy.
Aby lepiej
zrozumieć powyższą, książkową definicję polimorfizmu, wyobraźmy sobie nadajnik
telewizyjny, który wysyła do użytkowników sygnał. Nadajnik ten nie wie, jakiego
rodzaju jest antena użytkownika znajdująca się na końcu linii. Może to być
jakaś antena szerokopasmowa, może to być antena wielokanałowa, a może zwykła
standardowa naziemna, czy po prostu domowa. Nadajnik zna jedynie „typ bazowy”,
czyli w naszym przypadku antenę i oczekuje, że każdy „egzemplarz” tego typu
będzie potrafił odebrać jego sygnał i dostarczyć do telewizora znajdującego się
u użytkownika. W ten sposób nadajnik traktuje anteny polimorficznie.
Po
zdefiniowaniu sobie pojęcia polimorfizmu, pokażmy sobie w jaki sposób tworzy
się metody polimorficzne. Metody takie definiujemy w klasie bazowej (jak już
wiemy jest to klasa, po której dziedziczymy) i oznaczamy je jako metody wirtualne. Aby utworzyć takie
metody, należy dodać do ich deklaracji słowo kluczowe: virtual, np.:
public class A
{
public virtual void MojaWirtualna()
{ }
}
Gdy deklaracja metody zawiera modyfikator virtual, to taką metodę nazywamy wirtualną, natomiast gdy taki modyfikator nie występuje, wówczas
daną metodę nazywamy po prostu niewirtualną.
W
deklaracji metody wirtualnej nie jest możliwe umieszczenie takich modyfikatorów
jak: static, abstract oraz override. O metodach statycznych (słowo
kluczowe static w deklaracji takich
metod) była już mowa na łamach portalu CentrumXP. Przypomnę jedynie, że takie
metody działają na klasach, a nie na obiektach tych klas. Przykładem takiej
metody jest dobrze nam znana metoda Main()
z naszych przykładów. Modyfikator abstract
pojawi się za tydzień w naszym kursie, natomiast o słowie kluczowym: override będzie jeszcze mowa w
niniejszym artykule.
Po
prawidłowym zadeklarowaniu metody wirtualnej w klasie bazowej, w każdej klasie
pochodnej może znaleźć się nowa wersja tej metody. Aby utworzyć nową wersję
metody MojaWirtualna() należy ją przesłonić w klasie pochodnej,
używając do tego nowego modyfikatora: override:
public class B : A
{
public override void MojaWirtualna()
{
//wywolanie
metody klasy bazowej
base.MojaWirtualna();
//miejsce
na nowy kod w przeslonietej metodzie
…
}
}
W powyższym fragmencie kodu,
modyfikator override informuje
kompilator o tym, że dana metoda w klasie pochodnej (jak już wiemy, możemy
przesłaniać metody wirtualne z klas bazowych jedynie w klasach pochodnych)
została celowo przesłonięta. Bardzo często metody przesłaniające wywołują
metody bazowe w swojej strukturze (w końcu przesłaniamy przecież metodę, którą
dziedziczymy i często chcemy ją wywołać w nowej metodzie). Aby tak zrobić,
należy wykorzystać słowo kluczowe base.
W powyższym kodzie:
base.MojaWirtualna();
w klasie B wywołuje
metodę: MojaWirtualna(), którą
zadeklarowaliśmy w klasie A. Taki dostęp bazowy wyłącza mechanizm wywołania
wirtualnego i traktuje taką metodę jako metodę niewirtualną.
Gdybyśmy w klasie B
wywołali metodę MojaWirtualna() w
następujący sposób:
((A)this).MojaWirtualna();
to rekurencyjnie
zostałaby wywołana metoda MojaWirtualna()
zadeklarowana w klasie B, a nie w klasie A.
Napiszmy przykład,
który będzie prezentował nowo poznane przez nas pojęcie polimorfizmu.
public class PierwszaKlasa
{
public int a;
public int b;
//konstruktor
klasy A, ktory przyjmuje dwie liczby calkowite
public
PierwszaKlasa(int a, int
b)
{
this.a = a;
this.b = b;
}
//metoda
wirtualna
public virtual void
Wyswietl()
{
System.Console.WriteLine("Wartości a i b wynoszą odpowiednio: {0} i {1}",
a, b +".");
}
}
public class DrugaKlasa : PierwszaKlasa
{
public int c;
//dodatkowy
parametr w konstruktorze, wywolujemy rowniez konstruktor klasy bazowej
public DrugaKlasa(int
a, int b, int
c) : base(a, b)
{
this.c =
c;
}
//przeslaniamy
metode klasy bazowej
public override void Wyswietl()
{
base.Wyswietl();
System.Console.WriteLine("Wartość liczby c wynosi: {0}", c +".");
}
}
public class TrzeciaKlasa : DrugaKlasa
{
int d;
int suma;
//dodatkowy
parametr w konstruktorze oraz wywolanie konstruktora klasy bazowej
public TrzeciaKlasa(int a, int b, int c, int d) : base(a, b, c)
{
this.d
= d;
}
//przeslaniamy
metode klasy DrugaKlasa
public override void Wyswietl()
{
base.Wyswietl();
System.Console.WriteLine("Wartość liczby d wynosi: {0}", d + ".");
suma = a + b + c + d;
System.Console.WriteLine("Suma czterech kolejnych liczb wynosi: {0}",
suma +".");
}
}
class Glowna
{
public static void Main(string[] args)
{
//deklaracja
obiektu typu PierwszaKlasa wraz z wartosciami zmiennych
PierwszaKlasa
pr = new PierwszaKlasa(1,
2);
//deklaracja
obiektu typu DrugaKlasa wraz z wartosciami zmiennych w tym obiekcie
DrugaKlasa
dr = new DrugaKlasa(3,
4, 5);
//deklaracja
obiektu typu TrzeciaKlasa
TrzeciaKlasa
tr = new TrzeciaKlasa(6,
7, 8, 9);
//wywolanie
metody Wyswietl() z klasy PierwszaKlasa
pr.Wyswietl();
//wywolanie
metody Wyswietl() z klasy DrugaKlasa
dr.Wyswietl();
//wywolanie
metody Wyswietl() z klasy TrzeciaKlasa
tr.Wyswietl();
}
}
Powyższy przkład –
wbrew pozorom - jest bardzo łatwym programem prezentującym dziedziczenie wraz z
polimorfizmem. Przedstawia bowiem wszystkie apsekty programistyczne, o których
była mowa dzisiaj, bądź w poprzednich odcinkach kursu programowania w języku C#
2.0. Jedyną rzeczą godną uwagi oraz komentarza w powyższym kodzie jest sposób
wywoływania konstruktorów klasy bazowej.
Klasa DrugaKlasa dziedziczy po klasie: PierwszaKlasa i zawiera własny
konstruktor, który przyjmuje 3 parametry (wszystkie są liczbami całkowitymi).
Po liście parametrów, jakie ten konstruktor przyjmuje mamy dwukropek, a więc
znak, że wywołamy coś z klasy bazowej. W ten właśnie sposób wywołujemy
konstruktor klasy bazowej (a więc klasy: PierwszaKlasa).
Ważną informacją
jest fakt, że klasy nie dziedziczą konstruktorów. W takim przypadku w klasie
pochodnej musimy zdefiniować własny konstruktor, który może korzystać z
konstruktora klasy bazowej tylko poprzez jawne jego wywołanie.
A co się dzieje,
gdy w klasie bazowej znajduje się konstruktor domyślny? W takim przypadku w
konstruktorze klasy pochodnej nie jest potrzebne jawne wywoływanie konstruktora
z klasy bazowej, ponieważ za nas uczyni to kompilator. Jeśli jednak w klasie
bazowej nie ma konstruktora domyślnego, to każdy konstruktor w klasie pochodnej
musi jawnie wywoływać jakiś konstruktor z klasy bazowej wykorzystując przy tym
słówko: base.
Bardzo często o tym
zapominają programiści, dlatego warto zapamietać powyższe informacje na temat
dziedziczenia konstruktorów, aby nie popełniać podobnych błędów.
Po skompilowaniu
powyższego przykładu otrzymamy następujące wyniki:>

Prześledźmy
następujący przykład:
public class A
{
public virtual void
MojaWirtualna()
{
System.Console.WriteLine("To jest metoda wirtualna w klasie A");
}
}
public class B : A
{
public virtual void MojaWirtualna()
{
base.MojaWirtualna();
System.Console.WriteLine("To jest metoda wirtualna w klasie B");
}
}
class Glowna
{
public static void Main()
{
B b = new B();
b.MojaWirtualna();
}
}
W klasie A
zadeklarowaliśmy wirtualną metodę o nazwie: MojaMetoda().
Klasa B, która dziedziczy po klasie
bazowej A, definiuje również
wirtualną metodę MojaMetoda() o
takiej samej sygnaturze jak jej poprzedniczka. Czy taka sytuacja jest
dozwolona? Skompilujmy i spójrzmy na wyniki:

Okazuje
się, że nasz przykład skompilował się i otrzymaliśmy satysfakcjonujące nas
wyniki. Wszystko byłoby dobrze, gdyby nie komunikat naszego kompilatora w
momencie uruchomienia powyższego kodu:

Otrzymaliśmy
więc ostrzeżenie, które informuje nas, że w klasie B ukryto dziedziczoną z klasy A
metodę MojaWirtualna(). Klasa B nie
zawiera bowiem modyfikatora override,
dlatego też metoda MojaWirtualna()
klasy A nie została przesłonięta.
Aby uniknąć
powyższego ostrzeżenia, musimy użyć słowa kluczowego: new przy deklaracji metody wirtualnej w klasie B:
public class A
{
public virtual void MojaWirtualna()
{
System.Console.WriteLine("To jest metoda wirtualna w klasie A");
}
}
public class B : A
{
new public virtual void MojaWirtualna()
{
base.MojaWirtualna();
System.Console.WriteLine("To jest metoda wirtualna w klasie B");
}
}
class Glowna
{
public static void Main()
{
B b = new B();
b.MojaWirtualna();
}
}
W ten sposób kompilator wie, że nie przesłaniamy
dziedziczonej wirtualnej metody, a jedynie ją ukrywamy.
W dzisiejszym artykule
wprowadzliśmy sobie nowe pojęcia takie jak: dziedziczenie i polimorfizm. Są to
mechanizmy, bez których nasz program nie istniałby. Poznaliśmy też takie
pojęcia jak: klasa bazowa i pochodna, oraz metoda wirtualna i przesłaniająca.
Nauczyliśmy się również używać nowego słowa kluczowego: base.
Jednak to nie wszystko, jeśli chodzi o to zagadnienie
programistyczne. Musimy opowiedzieć sobie jeszcze o klasach i metodach
abstrakcyjnych, które są niejawnymi metodami wirtualnymi.