AbstrakcjaCzęść NR.14Klasy abstrakcyjne działają w podobny sposób jak interfejsy jednak ich metody mogą posiadać ciała i informacje.
W klasie abstrakcyjnej możesz też zadeklarować metody wirtualne tak, aby klasa dziedzicząca po niej mogła podać swoją własną implementacje tej właśnie metody.
Kiedy klasy abstrakcyjne wchodzą do gry
Interfejsy to wspaniały wynalazek uzupełniający pewne braki klas. Klasa może dziedziczyć po wielu interfejsach , a tylko po jednej klasie. Jednak interfejsy nie są czasami ostateczną odpowiedzią na wszystko.
Zdarzają się pewne sytuacje, w których kod będzie się powielał. Interfejs daje możliwość ustalenia programiście kontraktu czy spisu metod , które klasa dziedzicząca musi implementować . Jednak sam interfejs został stworzony tak, aby w nim nie można było deklarować ciał metod więc mogą pojawiać się sytuacje gdzie nasz kod będzie się powielać. A gdy coś trzeba pisać dwa razy to znaczy ,że coś jest nie tak.
interface IZucieTrway
{
void ZucieTrawy();
}
class Krowa : Ssak,IZucieTrway
{
public void ZucieTrawy()
{
Console.WriteLine("żucie trawy");
}
}
class Owca :Ssak,IZucieTrway
{
public void ZucieTrawy()
{
Console.WriteLine("żucie trawy");
}
}
Jak widzisz musiałem napisać dwa razy te same ciało metody ,a z drugiej strony te klasy już dziedziczą po jednej klasie Ssaki więc następna klasa nie może się pojawić.
Ale zaraz, co prawda klasa może dziedziczyć po jednej klasie bazowej ,ale przecież mogę stworzyć całą hierarchię dziedziczenia w taki sposób aby rozwiązać ten problem.
Mogę stworzyć klasę "SsakZujacyTrawe" , która będzie dziedziczyć po Ssakach ,a potem użyć tej klasy na Krowie i na Owcy.
class SsakZujacyTrawe : Ssak
{
public void ZucieTrawy()
{
Console.WriteLine("zucie trawy");
}
}
class Krowa : SsakZujacyTrawe
{
}
class Owca : SsakZujacyTrawe
{
}
Problem rozwiązał się tak. Ale pozostał jeszcze mały szczegół otóż mogę stworzyć obiekt czyli instancje klasy SsakZujacyTrawe i nawet mogę stworzyć instancje klasy Ssak. Z punktu widzenia obiektywności to nie do końca jest logiczne. Co to za obiekt "Ssak" czy "SsakZującyTrawe" możesz to sobie wyobrazić.
Zapytam jeszcze inaczej, jeśli nasz program ma operować na konkretnych zwierzętach (i ludziach xD), to po co program ma mieć możliwość tworzenia instancji tych klas.
Celem tych klas jest tylko przekazanie pewnych cech i funkcji do swoich kolegów i tyle. Klasy te nie powinny spełniać funkcji własnych encji.
W takim wypadku do gry wchodzi słowo kluczowe “abstract".
Dzięki niemu możesz stworzyć klasę abstrakcyjną. Co robi klasa abstrakcyjna.
Nie możesz stworzyć instancji klasy abstrakcyjnej, czyli oznaczasz ,że ta klasa spełnia swój cel wyłącznie w hierarchii dziedziczenia.
abstract class SsakZujacyTrawe : Ssak
{
public void ZucieTrawy()
{
Console.WriteLine("zucie trawy");
}
}
Jak widzisz słowo kluczowe abstract zablokowało możliwość utworzenia instancji tej klasy.
Jeśli zapomniałem napisać o tym wcześniej to napiszę teraz. Nie można też tworzyć instancji Interfejsów jest to całkowicie logicznie ponieważ one przecież nie zawierają żadnej implementacji i nawet konstruktora, więc z czego miałby powstać obiekt.
Metody abstrakcyjna
Klasa abstrakcyjna może zawierać metody abstrakcyjne. Co to jest metoda abstrakcyjna?
Metoda abstrakcyjna działa w podobny sposób jak metoda wirtualna poza tym ,że nie posiada swojego ciała.
Klasa dziedzicząca musi implementować tę metodę jest to zasada zbliżona do interfejsów. W innym przypadku zobaczysz błąd w czasie kompilacji.
Metoda abstrakcyjna nie może być prywatna. Skoro klasa dziedzicząca musi implementować tę metodę to logicznie myśląc metoda nie może być prywatna ponieważ wtedy klasa dziedzicząca nie będzie miał do niej dostępu.
abstract class SsakZujacyTrawe : Ssak
{
public abstract void ZucieTrawy();
}
Jest to tylko przykład, ponieważ trochę wyżej udowodniłem ,że ta metoda powinna mieć swoją jedną uniwersalną implementacje.
Używając metod abstrakcyjnych twoja klasa abstrakcyjna może symulować działanie interfejsów.
Klasa nieabstrakcyjna nie może zawierać w sobie metod abstrakcyjnych.
Klasa zamknięta : Sealed class
Hierarchia dziedziczenia może być niekończącą się opowieścią. Klasa “B” może dziedziczyć po klasie “A” , a dalsza klasa “C” może dziedziczyć po klasie “B”.
Dla bezpieczeństwa swojej aplikacji może chcesz, aby twoja klasa nie mogła już stać się klasą bazową dla kolejnej klasy. Może już wiesz , że niektóre klasy są ostatnim wierzchołkiem w hierarchii i nie mogą i nie powinny być używane dalej.
A może nie chcesz aby twoja klasa, nad którą tak długo pracowałeś mogła być użyta jako klasa bazowa przez osoby trzecie. Tego modyfikator dostępu nie mogą zagwarantować.
Do takiej gry wchodzi słowo kluczowe “sealed”.
sealed class Krowa : SsakZujacyTrawe
{
}
Jeśli ktoś spróbuje dziedziczyć po klasie Krowa otrzyma błąd. Nawet Intellisense automatycznie nie pokazuje tej klasy w trakcie dziedziczenia więc ciężko popełnić ten błąd. Jak widzisz na poniższej ilustracji klasa Krowa nie jest nawet koloryzowana na niebiesko.
Z logicznego punktu widzenia klasy zamknięte nie mogą deklarować metod wirtualnych i abstrakcyjnych, ponieważ jak one miałby być użyte, skoro dziedziczenie jest zablokowane.
Struktury są automatycznie zamknięte dlatego dziedziczenie nie jest możliwe.
Zamknięte metody : Sealed Methods
Istnieje możliwość użycia słowa kluczowego sealed do metod ze słowem kluczowym override. Każda metoda override domyślnie nie jest zamknięta, czyli może być nadpisana przez kolejną klasę dziedziczącą. Zamykając tę metodę blokujemy tą możliwość.
Zamkniętą metodą może stać się tylko metoda nadpisująca (override). Czyli aby ją zadeklarować musisz użyć słów kluczowych “sealed override”. Nie możesz więc zamknąć metody pochodzącej z interfejsu . Możesz zamknąć tylko metody pochodzące od klas i są to oczywiście metody nadpisujące (nie mogą to być metody abstrakcyjne, ponieważ one zachowują się jak metody pochodzące od interfejsów)
Aby wszystko było jasne najlepiej zapamiętać słowa kluczowe jak: interface , virtual , override i sealed w takiej kolejności.
- Interface daje możliwość zadeklarowania samej nazwy metody i nic więcej.
- Metoda wirtualna pokazuje domyślną implementacje metody, która może być nadpisana.
- Słowo kluczowe override pozwala na nadpisanie metody wirtualnej.
- Zamknięcie metody spowoduje zablokowanie dalszych modyfikacji metody dla klas dziedziczących.
Poniższy kod obrazuje tą kolejność.
interface IZucieTrway
{
void ZucieTrawy();
}
abstract class SsakZujacyTrawe : Ssak,IZucieTrway
{
virtual public void ZucieTrawy(){}
}
class Krowa : SsakZujacyTrawe
{
sealed override public void ZucieTrawy() { }
}
class KrowaAfrykańska : Krowa
{
}
Poniżej próbowałem złamać zasadę zamknięcia dla "KrowyAfrykańskiej" jak widać kompilator wykazał mi błąd.
Co dalej
Myślę , że najwyższy czas aby coś powiedzieć o właściwościach klas , struktur i interfejsów. Słowo właściwości padało dość często w tym kursie ,a jest to bardzo istotna rzecz, której nie możemy już dłużej pomijać.