Dziedziczenie
VarrComodoo
##1. Czym jest dziedziczenie
Dziedziczeniem jest przejmowanie pewnych cech/właściwości/ lub zachowań /metod/ z klas bazowych do klas potomnych. Klasą bazową jest ta z której dziedziczymy, klasą potomną jest ta która dziedziczy. Relacją opisującą dziedziczenie pomiędzy obiektem pochodnym a bazowym, jest relacja „jest”/„jest odmianą”. Klasa pochodna jest zawsze bardziej konkretnym przypadkiem klasy bazowej. Przykład: Brzoza dziedziczy z Drzewa. Brzoza jest bardziej skonkretyzowanym przypadkiem Drzewa. Na klasę bazową należy patrzeć jako bardziej ogólny przypadek typu pochodnego. Przykład: Samo określenie Drzewo jest bardziej ogólnym określeniem dla Brzozy i nie zawsze Drzewo musi być Brzozą, są przecież inne typy drzew
##2. Praktyczny przykład dziedziczenia.
Na samym dole w pkt. 12, znajduje się pełen przykład z drzewem genealogicznym obiektów po sobie dziedziczących. Na podstawie tego drzewa będą omawiane zagadnienia związane z dziedziczeniem. W niektórych przypadkach dla pokazania pewnych rzeczy będą w treści artykułu podane zmiany jakie należy nanieść w modelu aby pewne rzeczy zaobserwować lub krótkie wtrącenia z zupełnie innego modelu dziedziczenia.
##3. Zastosowanie i stosowanie dziedziczenia
Dziedziczenie generalnie stosuje się wszędzie tam gdzie potrzebujemy rozszerzyć dany typ lub dodać do niego nowe elementy np. właściwości/metody itp. Dziedziczenie sprzyja również grupowaniu cech wspólnych i wspólnych zachowań w klasie nadrzędnej np. podczas refaktoryzacji, dzięki temu wyodrębniamy wspólny kod z wielu klas do klasy dla nich bazowej. Są to główne założenia i zalety dziedziczenia.
W przykładzie z pkt.12 widać, że właściwości ‘KolorWykonczenia’ i ‘MaterialWykonczenia’ są dostępne w każdej klasie całego łańcuszka dziedziczącego po ‘Mebel’. Sprawdzając klase ‘Kolyska’ widać ze zawiera metodę ‘Odnow()’ również dziedziczoną z ‘Lozka’, które dziedziczy ją po ‘Meblu’.
W C# można dziedziczyć tylko po jednej klasie bazowej, ponadto wszystkie nowo utworzone klasy dziedziczą niejawnie po klasie ‘object’. Przykład: coś zdefiniowane jako:
public class MojaNowaKlasa
{
}
Po utworzeniu:
MojaNowaKlasa nowa = new MojaNowaKlasa();
nowa. //w tym miejscu Intellisense podpowiada 4 metody z ‘object’
obiekt nowa ma dostępne metody: Equals(), GetHashCode(), GetType() oraz ToString(). Są to metody odziedziczone po typie ‘object’. W Przykłądzie na samym końcy niejawnie klasa bazowa ‘Mebel’ również dziedziczy po ‘object’ a co za tym idzie cechy ‘object’ są kopiowane do wszystkich obiektów dziedziczących po ‘Mebel’ i dalej.
##4. Klasa abstrakcyjna i dziedziczenie po klasie abstrakcyjnej
Wyciąganie elementów wspólnych dla typów i definiowanie ich w klasie bazowej może doprowadzić do powstania klasy bazowej która sama w sobie „nie ma sensu”. Przykładem tego jest w niższym drzewku klasa Mebel. Co by było gdybyśmy utworzyli klasę ‘Mebel’ /moglibyśmy utworzyć taki typ gdy usuniemy z definicji tej klasy słowo kluczowe ‘abstract’/? Z obecnej definicji powstałoby coś, co ma jakiś kolor i jakiś materiał wykończenia, ale kształtu tego obiektu trudno się nawet domyślić, czy to ma mieć siedzisko czy blat czy materac a może drzwiczki? Dlatego klasa ‘Medel’ z przykładu, została oznaczona w definicji słowem ‘abstract’ co znaczy ze nie można utworzyć tego typu obiektu poprzez operator ‘new’. Korzystanie z typu abstrakcyjnego jest możliwe po zrzutowaniu podtypu na typ bazowy, zabronione jest tylko tworzenie typu abstrakcyjnego poprzez operator ‘new’, przykład:
Mebel mebel;
FotelBujany fotelBujany = new FotelBujany(ConsoleColor.Cyan, Material.Drewno, 40, 5);
mebel = fotelBujany; //niejawne rzutowanie
Pomimo tego że ‘fotelBujany’ został utworzony poprzez ‘new FotelBujany()’ po zrzutowaniu go do mebel typu ‘Mebel’, mebel ma ograniczony zakres dostępnych właściwości i metod tylko do typu ‘Mebel’.
Mebel mebel = new Mebel() //przy zdefiniowaniu ‘Mebel’ jako abstract, w tej linijce kompilator zgłosi blad.
##5. Rzutowanie jawne i niejawne
Aby uzyskać ponownie dostęp do właściwości i metod typu ‘FotelBujany’ należy zrzutować ‘mebel’ z powrotem na typ ‘FotelBujany’ jak niżej.
FotelBujany fotel2 = (FotelBujany)mebel; //rzutowanie jawne
int test = fotel22.ZakresBujania; //do test przypisana zostaje wartość 5,
//ponieważ taka wartość przekazaliśmy podczas tworzenia obiektu FotelBujany w poprzednim punkcie.
Rzutowanie, czyli inaczej konwersja, jest operacją możliwą do wykonania dla typów w jednego łańcucha dziedziczenia. Możliwe jest to pomiędzy obiektami dzięki temu, że dziedziczenie tworzy relację pomiędzy obiektami „jest”/”jest odmianą”. Typ pochodny ‘Krzeslo’ jest też typem ‘Mebel’. Dlatego wartość typu ‘Krzeslo’ można przypisać do zmiennej typu ‘Mebel’. Wykorzystano to w przykładzie wyżej:
Mebel mebel = fotelBujany;
Jest to przykład rzutowania niejawnego. Kompilator sam wie jak zrzutować typ szczegółowy na bardziej ogólny ponieważ poruszając się po drzewku dziedziczenia od podtypu do typu ogólnego jest tylko jedna droga, wiec nie należy tego dodatkowo oznaczać sładnią rzutowania jawnego np. tak:
Mebel mebel = (Mebel)fotelBujany; //choć to również jest poprawne, jest jednak nadmiarowe i nie musi być stosowane
Przy rzutowaniu typu bardziej ogólnego na typ bardziej szczegółowy jest już wymagane pokazanie kompilatorowi na co chcemy zrzutować typ ogólny. Poruszając się na drzewku dziedziczenia od typu ogólnego do bardziej szczegółowego mamy rozgałęzione drogi – ta droga nie jest już jednoznaczna dlatego musimy wskazać czego kompilator może się spodziewać.
fotel = (FotelBujany)mebel;
Takie rzutowanie może jednak nie udać się i rzucić wyjątek w przypadku jak niżej:
Krzeslo krzeslo = new Krzeslo(....);
Lozko lozko = new Lozko(....);
Mebel mebel = krzeslo;
Lozko lozkoDrugie = (Lozko)mebel; //tu będzie błąd, nie można rzutować typu ‘Krzeslo’ na ‘Lozko’,
//pochodzą one z dwóch różnych gałęzi drzewka dziedziczenia
##6. Konwersja niestandardowa (implicite i explicit)
Konwersja jawna i niejawna działa tylko na typach z jednego łańcucha dziedziczenia. Można jednak zdefiniować konwersję pomiędzy typami zupełnie ze sobą niepowiązanymi. Służą do tego operatory ‘implicit’ i explicit’ /odpowiednio operator konwersji niejawnej i jawnej/. Jednak ta operacja przeważnie obarczona jest bezpowrotną utratą części danych. Na poniższym przykładzie widać dlaczego:
Definiujemy klasę Kiwon
(def. Kiwon – osoba bezrefleksyjnie przytakująca wszystkim i wszystkiemu
https://lubimyczytac.pl/ksiazka/100446/kiwony) będziemy rzutować ‘Kolyskę’ na ‘Kiwona’, a następnie z powrotem na ‘Kolyske’.
public class Kiwon
{
public int ZakresBujania { get; set; }
public Kiwon(int przytakiwanie)
{
this.ZakresBujania = przytakiwanie;
}
public static implicit operator Kiwon(Kolyska kolyska)
{
return new Kiwon(kolyska.ZakresBujania);
}
public string BujajSie()
{
return "Tak proszę Pana, oczywiscie proszę Pana";
}
}
Do klasy ‘Kolyska’ należy dodać definicje:
public static implicit operator Kolyska(Kiwon kiwon)
{
return new Kolyska(null, null, 0, 0, kiwon.SzybkoscPrzytakiwania);
}
Sposób użycia:
Kolyska kolyska = new Kolyska(ConsoleColor.Red, Material.Skora ,40, 120, 5);
Kiwon kiwon = kolyska; //gdybyśmy użyli słowa explicit w definicjach operatorów
//należałoby jawnie rzutować Kiwon kiwon = (Kiwon)kolyska
Kolyska kolyska2 = kiwon; //kolyska2 utraciła dane: kolor, typ wykończenia, szerokość i długość
Teraz widać ze konwertując z ‘Kiwona’ na ‘Kolyske’ nie utracimy żadnych danych i potem konwertując z powrotem z ‘Kolyski’ na ‘Kiwona’ uzyskamy ‘Kiwona’ jak na początku. Konwertujac w drugą stronę tracimy bezpowrotnie informacje o 4 z 5ciu cechach ‘Kolyski’.
##7. Modyfikatory dostępu dla składowych klas
Modyfikatory dostępu:
private, protected, public, internal, protected internal
opisane są w artykule opisujących te modyfikatory
Modyfikatory dostępu:
virtual, override, new, sealed, abstract, static, extern
Opisane zostały w innym artykule opisującym metody
Różnice pomiędzy modyfikatorem ‘new’ oraz ‘override’ pokazuje przykład metod ‘Odnow()’ dla typów ‘Krzeslo’ i ‘Stol’. Warto posprawdzać jakie wartości będą zwracały te typy w zależności od tego na co będą rzutowane i z którego typu będą wywoływane metody ‘Odnow()’. Po każdym wywołaniu metody ‘Odnow()’ należy sprawdzać właściwości które są modyfikowane w tej metodzie.
##8. Skladowe bazowe : base()
Nawet gdy składowa typu przeslania składową jego podtypu, można wywołać składową podtypu używając składni base(). Przykład takiego wywołania widać w typie ‘Krzeslo’ w jego metodzie ‘Odnow()’. ‘Krzeslo’ przeslania metode ‘Odnow()’ z ‘Mebla’ ale wewnątrz siebie, wywołuje metodę bazowa przekazując jej dwa swoje parametry a w swoim ciele dopisuje jeszcze obsługę trzeciego parametru. Ponadto metoda ta jest opatrzona modyfikatorem ‘new' co oznacza że dany typ szukając najbardziej pochodnej metody ‘Odnow()’ nie dotrze do metody w ‘Krzesle’, potraktuje ją jako nową metodę i zastosuję metodę’Odnow()’ z obiektu i jeden poziom niżej niż ‘Krzeslo’.
Przy konstruktorach wygląda to trochę inaczej. Podczas tworzenia podtypu np. ‘Stol’, kompilator zaczyna tworzenie typu od konstruktora jego klasy bazowej. Najpierw szuka konstruktora w klasie bazowej ‘Mebel’ o takich samych parametrach jak wywołany konstruktor podtypu ale takiego nie znajduje, wtedy szuka konstruktora klasy bazowej bezparametrowego jeżeli i jego nie znajdzie – zgłasza błąd. Przykład:
Obecnie konstruktor typu ‘Stol’ wygląda tak:
public class Stol
{
(…)
public Stol(ConsoleColor kolor, Material material, int Wysokosc)
: base(kolor, material)
{
WysokoscBlatu = Wysokosc;
}
}
A konstruktor typu ‘Mebel’ wyglada tak:
public abstract class Mebel
{
//(…)
public Mebel(ConsoleColor kolor, Material material)
{
KolorWykonczenia = kolor;
MaterialWykonczenia = material;
}
}
Gdyby wyciąć z konstruktora ‘Stol’ fragment
: base(kolor, material)
W klasie ‘Mebel’ musi zostać zdefiniowany nawet pusty konstruktor bezparametrowy:
public Mebel()
{ }
Wtedy możliwe będzie tworzenie ‘Stolow’ ale tracimy kontrole nad przypisaniem wartości dla właściwości ‘KolorWykonczenia’ oraz ‘MaterialWykonczenia’, dla wywołania:
Stol stol = new Stol(ConsoleColor.Red, Material.Drewno, 23);
Utworzony zostanie stol, czarny i z wykończeniem z materiału, ponieważ to są wartości domyślne /pierwsze w tablicy/ dla ‘ConsoleColor’ oraz ‘Material’.
##9. Polimorfizm czyli wielopostaciowość (Artykul - opisujący szerzej to zagadnienie)
Przeglądając przykład poniżej widać ze klasy ‘Krzeslo’ oraz ‘Stol’ implementują metodę o identycznej sygnaturze ‘Odnow()’, różnice są w ich implementacjach w jednym przypadku podwyższamy blat stołu w drugim siedzisko. Innym przykładem jest metoda ‘BujajSie()’ jest identycznej sygnatury w klasie ‘Kolyska’ i ‘FotelBujany’ ale rożni się działaniem.
Dzięki temu że różne obiekty mają metody o identycznych sygnaturach ale te różne obiekty mają wspólnego na którymś poziomie przodka można te różne obiekty zrzutować na typ bazowy i tak samo wywoływać ich metody ‘Odnow()’ a każdy obiekt zrobi to po swojemu, przykład:
Mebel[] meble = new Mebel[2];
meble[0] = new Krzeslo(ConsoleColor.Cyan, Material.Skora, 45);
meble[1] = new Stol(ConsoleColor.DarkBlue, Material.Drewno,85);
foreach (var item in meble)
{
item.Odnow(ConsoleColor.Red, Material.Drewno);
}
W pętli dla każdego mebla wywołujemy jego metodę ‘Odnow()’ i każdy mebel robi to po swojemu.
Z polimorfizmem silnie związane są interfejsy. Klasa dziedziczyć może tylko po jednej klasie ale może implementować nieograniczoną liczbę interfejsów, warunkiem jest jedynie to aby w definicji dziedziczenia najpierw zapisany były typ klasy bazowej a za nią dopiero interfejsy w dowolnej już kolejności.
W związku z powyższych gdybyśmy zdefiniowali taki interfejs - (Artykuł - opisujący szerzej to zagadnienie)):
public interface IBujajacy
{
int ZakresBujania { get; set; }
string BujajSie();
}
Oraz dopisali go w definicjach takich trzech klas:
public class Kolyska : Lozko, IBujajacy
public class FotelBujany : Krzeslo, IBujajacy
public class Kiwon : IBujajacy
można byłoby każdy z tych typów zrzutować na IBujajacy i dla każdego takiego obiektu wywoływać metodę ‘BujajSie()’ – uzyskamy to samo co wyżej. Będziemy każdy obiekt prosić o to samo a z każdego z nich uzyskamy inny wynik.
IBujajacy [] meble = new IBujajacy [3];
IBujajacy [0] = new Krzeslo(ConsoleColor.Cyan, Material.Skora, 45);
IBujajacy [1] = new Stol(ConsoleColor.DarkBlue, Material.Drewno,85);
IBujajacy [2] = new Kiwon(45);
foreach (var item in IBujajacy)
{
item.BujajSie(); //dla każdego z elementów tablicy będzie wywołana metoda ‘BujajSie()’
// z konkretnego typu w jakim został dany element tablicy utworzony.
}
##10. Operator is (sprawdzanie "czy jesteś" typu)
Operator is służy do ustalenia konkretnego typu sprawdzanego obiektu. Jeżeli mamy tablicę pełną mebli, a chcemy wywołać tylko metodę ‘ZascielSie()’ zdefiniowaną tylko dla typu ‘Lozko’ można zrobić to w ten sposób.
Mebel[] meble = new Mebel[4];
meble[0] = new Krzeslo(ConsoleColor.Cyan, Material.Skora, 45);
meble[1] = new Stol(ConsoleColor.DarkBlue, Material.Drewno,85);
meble[3] = new Lozko(ConsoleColor.Green, Material.Material, 220, 220);
foreach (var item in meble)
{
if (item is Lozko lozko)
{
Console.WriteLine(lozko.ZascielSie());
}
}
Console.ReadLine();
Warto zauważyć że operator is sprawdzając wartość null /element meble[2] jest null/ zwraca false, także nie będzie tu rzuconego żadnego wyjątku, program będzie działał stabilnie.
##11. Operator as (traktuj ten typ jako podajTyp)
Operator as służy do rzutowania obiektu na dany typ z tym że w przypadku gdy taka konwersja nie jest możliwa ustawia w elemencie docelowym wartość null. Czyli robi to samo co operator is z tym ze w przypadku niepowodzenia konwersji ustawia wartość null.
Ponadto operator as jest mniej dokładny w porównaniu do is, może często wprowadzić w błąd . O ile operatorem is jesteśmy w stanie precyzyjnie ustalić typ obiektu, operator as może ustawić typ obiektu na ten wyżej lub niżej hierarchii dziedziczenia lub na typy które udostępniają swoje własne operatory rzutowania /implicite explicit/.
##12. Przykład omawiany w artykule.
public enum Material
{
Material,
Skora,
Drewno,
Metal
}
public abstract class Mebel
{
public ConsoleColor KolorWykonczenia { get; protected set; }
public Material MaterialWykonczenia { get; protected set; }
public Mebel(ConsoleColor kolor, Material material)
{
KolorWykonczenia = kolor;
MaterialWykonczenia = material;
}
public virtual void Odnow(ConsoleColor nowyKolor, Material nowyMaterial)
{
this.MaterialWykonczenia = nowyMaterial;
this.KolorWykonczenia = nowyKolor;
}
}
public class Stol : Mebel
{
public int WysokoscBlatu { get; protected set; }
public Stol(ConsoleColor kolor, Material material, int Wysokosc)
: base(kolor, material)
{
WysokoscBlatu = Wysokosc;
}
public string StoliczkuNakryjSie()
{
return "Nakryto do stolu na full wypas.";
}
public override void Odnow(ConsoleColor nowyKolor, Material nowyMaterial)
{
base.Odnow(nowyKolor, nowyMaterial);
WysokoscBlatu = WysokoscBlatu + 5;
}
}
public class Krzeslo : Mebel
{
public int WysokoscSiedziska { get; protected set; }
public Krzeslo(ConsoleColor kolor, Material material, int wysokosc)
: base(kolor, material)
{
WysokoscSiedziska = wysokosc;
}
public new void Odnow(ConsoleColor nowyKolor, Material nowyMaterial)
{
base.Odnow(nowyKolor, nowyMaterial);
WysokoscSiedziska = WysokoscSiedziska + 5;
}
}
public class Lozko : Mebel
{
public int SzerokoscMateraca { get; protected set; }
public int DlugoscMateraza { get; protected set; }
public Lozko(ConsoleColor kolor, Material material, int szerokosc, int dlugosc)
: base(kolor, material)
{
this.SzerokoscMateraca = szerokosc;
this.DlugoscMateraza = dlugosc;
}
public string ZascielSie()
{
return "Cud! Lozko samo sie sciele.";
}
}
public class Kolyska : Lozko
{
public int ZakresBujania { get; set; }
public Kolyska(ConsoleColor kolor, Material material, int szerokosc, int dlugosc, int zakres)
: base(kolor, material, szerokosc, dlugosc)
{
ZakresBujania = zakres;
}
public string BujajSie()
{
return "Buja z lewa do prawa w zakresie " + ZakresBujania;
}
}
public class FotelBujany : Krzeslo
{
public int ZakresBujania { get; set; }
public FotelBujany(ConsoleColor kolor, Material material, int wysokosc, int zakres)
: base(kolor, material, wysokosc)
{
ZakresBujania = zakres;
}
public string BujajSie()
{
return "Bujaj do prozdu i do tylu w zakresie " + ZakresBujania;
}
}