DziedziczenieCzęść NR.11Jest to kurs obiektowości , a jeszcze nie było mowy o dziedziczeniu w C#. Chociaż dziedziczenie i polimorfizm pojawiały się w niektórych miejscach w innych wpisach.

Dziedziczenie to kluczowy mechanizm obiektowości. Dziedziczenie pozwala na powielanie funkcjonalności wobec różnych klas w ten sposób nie musimy pisać ciągle samego kodu. .



Zalety nie kończą się jednak na tym. Przejdź do rzeczy.

Dziedziczenie

Czasami w kursie, jak i w innych wpisach mówiłem , że klasy dziedziczące są jak synowie klas bazowych. Ale takie rodzinne porównanie może nie być takie trafne. Każdy programista rozumie dziedziczenie na swój sposób .Wiadomo dobra metafora pozwala lepiej zrozumieć dane zjawisko. Jednak dziedziczenie w programowaniu jest zupełnie czymś innym niż terminy znane nam z życia jak: dziedziczenie genów czy dziedziczenie spadku.

Dziedziczenie w programowaniu jest bardziej o związkach pomiędzy klasami np. jedna może zawierać się w drugiej. Aby to uprościć powiedzmy , że klasy są jak kategorie . Ssaki to duża kategoria zwierząt, w której zawiera się też człowiek. Wszystkie ssaki mają pewne cechy wspólne jak: faza snu Rem, czy sposób karmienia potomstwa. Jednak każdy odrębny ssak ma swoje cechy specjalne.

Drugi przykład. Klasa figury opisuje cechy wspólne klas jak prostokąt, czy trójkąt, ponieważ są one też figurami. Prostokąt i trójkąt jednak mają swoje cechy specjalne. Ustalanie cech wspólnych poszczególnych klas daje sens tworzenia klas bazowych. Nie chciałbyś dla każdej figury, czy ssaka powielać tych samych cech, które występują. Jest to esencja modelowania klasy. Zarządzenie taką aplikacją jest sporo łatwiejsze, ponieważ w wypadku dodania nowej metody dla wszystkich ssaków, czy figury nie trzeba byłoby robić kopiuj/wklej dla każdej klasy po kolei.

Użycie dziedziczenia w C#

Oto przykład jak dziedziczenie jest pisane w C#.

class KlasaPochodna : KlasaBazowa
{

}

Klasa pochodna dziedziczy po klasie bazowej . W wyniku tej operacji wszystkie pola, metody, właściwości nieoznaczone modyfikatorem private są dostępne dla klasy pochodnej. Klasa nie może dziedziczyć po wielu klasach bazowych, czyli w C# dziedziczenie może zajść tylko do jednej klasy bazowej. C# nie obsługuje wielodziedziczenia. Nic nie stoi na przeszkodzie, oprócz słowa kluczowego sealed , aby dziedziczenie mogło zachodzić do dalszych klas.

class KlasaPodPochodna : KlasaPochodna
{
    
}

Wracając do poprzednich przykładów. Oto jak dziedziczenie zachodziłoby pomiędzy klasą figury  a klasą prostokąt.

class Figury
{
    public string kolorWypelnienia;
    public string kolorObramowania;
    public int wielkoscObramowania;
}

class Prostokąt :Figury
{

}

Teraz klasa Prostokąt będzie posiadała pola jak kolor i obramowanie. Każda figura powinna mieć informacje o tym, w jakim kolorze jest wypełniona i obramowana.

Oto przykład z ssakami. Każdy ssak powinien posiadać metodę sen REN.

class Ssaki
{
    public void SenRem()
    {}
}

class Kot : Ssaki{}

class Pies :Ssaki{}

W którymś wpisie kursu mówiłem , że każda klasa w System.Object jest źródłem wszystkich klas. Tworząc swoją klasę nie musimy pisać , że ona dziedziczy po Object, ponieważ ta zasada jest przekazywana wszystkim klasom.

Łatwo to udowodnić, ponieważ wszystkie klasy mają metody, jak np. ToString().

Dziedziczenie a Struktury

Alternate Text Struktury nie mogą dziedziczyć po żadnej klasie czy strukturze. Wydaje się to trochę oczywiste. Istnieje jednak pewien wyjątek wszystkie struktury dziedziczą po klasie abstrakcyjnej System.ValueType, która wprowadza do wszystkich struktur te same zachowania.

Struktury mogą dziedziczyć po interfejsach.

Dla programistów C++ i Java

Alternate Text W C++ istnieje możliwość określenia czy dziedziczenie jest prywatne, publiczne, czy chronione. W C# dziedziczenie zawsze jest publiczne. W przeciwieństwie do Javy w C# nie stosuje się słowa kluczowego extends tylko znak dwukropka “:”.

Wywoływanie konstruktorów klas bazowych

Klasa dziedzicząca posiada wszystkie pola i metody klasy bazowej. Jednak jak te pola są inicjowane i jak działa konstruktor w wypadku dziedziczenia. Każda klasa ma konstruktor napisany przez kompilator. Domyślam się , że konstruktor pisany przez kompilator uzupełnia wszystkie pola automatycznie w klasie dziedziczącej i nie korzysta z dziedziczenia. Ale to tylko moja teoria, by to sprawdzić trzeba byłoby pogrzebać w kodzie IL. Zresztą nawet nie wiem jak dziedziczenie jest oznaczone w IL.

Pisząc swoje konstruktory domyślne, jak i inne dobrą praktyką byłoby wywoływanie konstruktora z klasy bazowej w czasie inicjacji.

class Figury
{
    public Figury(string kolowyp,string kolobr,
        int wielOb)
    {
        kolorWypelnienia = kolowyp;
        kolorObramowania = kolobr;
        wielkoscObramowania = wielOb;
    }

    public string kolorWypelnienia;
    public string kolorObramowania;
    public int wielkoscObramowania;
}

class Prostokąt :Figury
{
    public Prostokąt(string kolowyp, string kolobr, int wielOb)
        : base(kolowyp,kolobr,wielOb)
    {

    }
}

Wywołanie konstruktora klasy bazowej w klasie pochodnej odbywa się za pomocą słowa kluczowego “base” , a w nim trzeba podać odpowiednie argumenty.

W tym przykładzie konstruktor klasy bazowej (Figury) wykona się najpierw potem wykona się konstruktor klasy pochodnej (Prostokąt)

Ta sztuczka działa pod warunkiem , że konstruktor w klasie bazowej nie jest prywatny.

Przypisywanie klasy

Korzystając z mechanizmu dziedziczenia jest możliwe przeobrażenie obiektu w inny pod warunkiem , że typ, w który obiekt się przeobraża jest w hierarchii dziedziczenia pod nim.

Czyli mogę przeobrazić obiekt prostokąt w obiekt figura, jak i obiekt kot na obiekt ssak.

Oto przykład:

Prostokąt pros = new Prostokąt("Biały", "Czarny", 4);
Figura fi = pros;

Z oczywistych powodów obiekt prostokąt nie może stać się obiektem trójkąt, ponieważ są to dwa różne obiekty. Jeden i drugi nie mają tych samych cech wspólnych , a raczej jeden nie zawiera się w drugim, więc transformacja nie może zajść, mimo iż dziedziczą po tej samej klasie Figury.

Polimorfwizm fail

Zwróć uwagę na inną ważną sprawę.

Wszystkie koty to ssaki ,wszystkie prostokąty to figury więc nic dziwnego , że możemy je bezpiecznie przypisywać. Hierarchia dziedziczenia mówi ,że możesz myśleć o prostokącie jako o figurze i o kocie jako ssaku. Kot zawiera wszystkie właściwości ssaka , a prostokąt figury.

Jednak taka transformacja ma pewne ograniczenie.

class Ssaki
{
    public void SenRem()
    { }
}

class Kot : Ssaki
{
    public void Miauczenie()
    {}    
}

Kiedy? np. jeśli przypiszemy obiekt z Kota do Ssaka poprzez inne zmienne możemy używać tylko metod, pól i właściwości ssaka. Mimo iż obiekt wciąż w pewnym sensie jest kotem o czym później jego własne metody nie są widoczne jak np. miauczenie.

Tracenie cech

Takie działanie najlepiej porównać z przypisywaniem zmiennych liczbowych na typ Object. Efekt będzie taki sam. Object nie posiada pola statycznego MaxValue.

float f = float.MaxValue;
object ob = f;
ob = ob.MaxValue;

Ponieważ nie możemy później przypisać ssaka do kota bezpośrednio musimy zastosować rzutowanie.

Wymagane rzutowanie

Nie wszystkie ssaki są kotami tak samo nie wszystkie obiekty są typem liczbowym float. Dlatego dla bezpiecznego rzutowania zastosujemy operator dwuargumentowy “as”.

W ten sposób możemy odzyskać naszego kota z powrotem.

Kot kot = new Kot();

Ssak ssak = kot; 
//ssak referuje się do kota 

Kot kotPowrot = ssak as Kot; 
//ssak był kotem 

Pies pies = new Pies();
Ssak ssak2 = pies;
kotPowrot = ssak2 as Kot; 
//null rzutowanie nieudane

Jak dobrze pamiętasz w wypadku błędnego rzutowania operator as zwróci null. Jak widzisz jest to przydatne ponieważ nie możemy rzutować z ssaka, który jest psem do kota.

Wraz z dziedziczeniem przychodzi...

W następnym wpisie o tym, jak przysłaniać metody i jak je nadpisywać oraz jak metody zachowują się w trakcie polimorfizmu.