06. Dziedziczenie i polimorfizm

06. Dziedziczenie i polimorfizm

Autor: Paweł Kruczkowski

Opublikowano: 10/31/2006, 12:00 AM

Liczba odsłon: 210612

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.

Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży
Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży

Wydarzenia