9. Programowanie obiektowe – Dziedziczenie, klasa bazowa, modyfikatory dostępu (hermetyzacja), klasa abstrakcyjna (abstract), klasa zapieczętowana (sealed), wielokrotne dziedziczenie (Interface)

9. Programowanie obiektowe – Dziedziczenie, klasa bazowa, modyfikatory dostępu (hermetyzacja), klasa abstrakcyjna (abstract), klasa zapieczętowana (sealed), wielokrotne dziedziczenie (Interface)
18 lipca 2022 Brak komentarzy C#, Poradnik Tajko

Kolejny punkt przy programowaniu obiektowym, tym razem omówię dziedziczenie i pokażę na przykładach, kiedy może się przydać, wspomnę o klasie bazowej, pokażę klasę abstrakcyjną jako ‘lepszą’ wersję klasy bazowej, wspomnę trochę o modyfikatorach dostępu, klasie zapieczętowanej i paru innych rzeczach. Na koniec przy przykładzie z poziomu kodu nawet wpadnie przykład właściwości i geterów seterów😊

Punkty w nagraniu:
  1. Czym jest dziedziczenie?
  2. Klasa bazowa
  3. Modyfikatory dostępu (hermetyzacja)
  4. Klasa i metoda abstrakcyjna (abstract)
  5. Klasa zapieczętowana (sealed)
  6. Wielokrotne dziedziczenie (Interface)
  7. Przykłady z poziomu kodu

Projekt do pobrania: Link


1. Czym jest dziedziczenie?

Dziedziczenie jest jednym z najważniejszych pojęć w programowaniu obiektowym, pozwala na zdefiniowanie uogólnionej klasy po której inne klasy mogą dziedziczyć, czyli mieć dostęp do takich samych pól, właściwości, metod itp.

Podczas tworzenia klasy zamiast pisać wszystko od nowa, można skorzystać z klasy bazowej. Dzięki czemu nie musimy kopiować kodu w paru miejscach tylko po to żeby mieć np. parę takich samych właściwości w paru innych miejscach. Pomyśl sobie, że masz klasę Player, która ma właściwość name i teraz chcesz stworzyć trzy inne klasy, które także miałyby właściwość name, więc musisz na nowo je utworzyć, później dojdziesz do wniosku, że chcesz zmienić wielkość litery z name na Name, musisz zrobić to w trzech klasach, a tak dzięki dziedziczeniu i klasie bazowej, zmiany dokonasz tylko w jednej klasie.

Prosty przykład tworzenia klasy bazowej, czyli zwyczajnej klasy.
Tworzenie klasy pochodnej, która dziedziczy po klasie bazowej ‘KlasaBazowa’

Żeby dziedziczyć po danej klasie (tzw. klasie bazowej), po nazwie klasy dodajemy dwukropek i wpisujemy nazwę klasy po której chcemy dziedziczyć.

Ważne jest żeby wiedzieć, że w C# dziedziczyć można tylko po jednej klasie, więc poniższy zapis niestety nie przejdzie

Jeśli mimo wszystko chcielibyśmy dziedziczyć po wielu klasach to możemy to uzyskać po przez wykorzystanie interfejsów. W C# można dziedziczyć po wielu interfejsach o czym wspomnę w późniejszym punkcie.


2. Klasa bazowa

Klasa bazowa to taka, klasa która zazwyczaj ma elementy wspólne, do których dostęp mają klasy pochodne, czyli takie, które po niej dziedziczą.

public class Player
{
	public string Name;
	protected int _level;
	protected string _description;
	protected Professions _profession;

	public Player (string name, int level, string description)
	{
		Name = name;
		_level = level;
		_description = description;
	}

 	public void ShowInformation() => Console.WriteLine($"{Name} - {_profession}");
}

Dla przykładu mamy klasę Player i na jej podstawie chcemy utworzyć jeszcze klasy Archer, Mage, Warrior, więc nie ma sensu kopiować całej zawartości klasy Player, zmieniając jedynie nazwę klasy i konstruktora, prościej będzie dziedziczyć po klasie Player

public class Archer: Player
{
	public float Agility;
	public Archer (string name, int level, string description) 
	: base(name, level, description)
	{
		_profession = Professions.Archer;
	}

	public new void ShowInformation()
	{		
		Console.WriteLine("Profesja Łucznika: ");
		base.ShowInformation();
	}
}

public class Mage: Player
{
	public float Mana;
	public Mage (string name, int level, string description)
	: base (name, level, description) 
	{ 
		_profession = Professions.Mage;	
	}

 	public void ShowInformation()
	{		
		Console.WriteLine("Profesja Maga: ");
		base.ShowInformation();
	}
}

public class Warrior: Player
{
	public float Strength;
	public Warrior (string name, int level, string description) 
	: base(name, level, description)
	{
		_profession = Professions.Warrior;
	}

 	public void ShowInformation()
	{		
		Console.WriteLine("Profesja Wojownika: ");
		base.ShowInformation();
	}
}

Jeśli w klasie bazowej dodamy inny konstruktor niż domyślny to w klasach pochodnych musimy odwołać się do takiego konstruktora. W powyższym przykładzie w klasie bazowej mamy konstruktor przeciążony z trzema parametrami, więc w klasach pochodnych też wypadałoby / musimy mieć konstruktor z trzema parametrami. Dochodzi tutaj nowe słowo kluczowe base, które jest dodane po konstruktorze po dwukropku. Oznacza ono nic innego jak odwołanie się do klasy bazowej i w tym przypadku odwołanie się do konstruktora z klasy bazowej. Dla przykładu tworząc obiekt klasy Mage, podajemy trzy parametry, które jednocześnie są przekazane do konstruktora znajdującego się w klasie bazowej i tam przypisane są do poszczególnych pól, dzięki temu nie musimy tego robić w klasie Mage. W powyższym przykładzie w poszczególnych klasach w konstruktorach ustawiamy jedynie profesje.

Można by się przyczepić metody ShowInformation, która występuje w klasie bazowej jak i w klasach pochodnych, no ale o tym później.

Jeśli pozostaniemy przy powyższym przykładzie do w Visualce wyskoczy nam warning przy metodzie ShowInformation, który raczej nas pyta oto czy jesteśmy pewni, że chcemy stworzyć nową metodę ukrywając tą, która znajduje się w klasie bazowej. Jedną z opcji ukrycia tego komunikatu jest dodanie słowa kluczowego new do tej metody, wtedy będzie wiadomo, że chcemy korzystać z metody w tej klasie, a nie z klasy bazowej

Warning, gdy posiadamy taką samą metodę w takiej samej postaci w klasie bazowej i w klasie pochodnej
public new void ShowInformation()
{		
	Console.WriteLine("Profesja Łucznika: ");
	base.ShowInformation();
}

Ukrycie/rozwiązanie problemu z warningiem poprzez dodanie new do metodki.


3. Modyfikatory dostępu (hermetyzacja)

W C# jak i w wielu językach programowaniu mamy takie cudeńka jak modyfikatory dostępu, pozwalają one nam na ukrycie niektórych elementów w klasach przed widokiem z zewnątrz, można to też nazwać hermetyzacją albo enkapsulacją, a hermetyzacja jest jedną z metodologii programowania obiektowego. Jedną z głównych idei w programowaniu obiektowym jest pisanie kodu, który jest jak najbardziej zamknięty, innymi słowy większość rzeczy dzieje się wewnątrz klasy.

W C# wyróżnić możemy modyfikatory dostępu takie jak:

  • public – elementy oznaczone tym modyfikatorem są normalnie widoczne wewnątrz jak i poza klasą, jeden z najczęstszych modyfikatorów
  • private – elementy z tym modyfikatorem widoczne są tylko wewnątrz np. klasy w której się znajdują
  • protected – ten modyfikator jest podobny do private z taką różnicą, że dostęp do elementów z tym modyfikatorem mają jeszcze klasy pochodne. Dla przykładu masz klasę Player i w niej pole Name oznaczony jako protected, tworzysz klasę Archer i z automatu masz dostęp do tego pola, ALE nie będzie ono widoczne po kropce po utworzeniu obiektu np. klasy Archer
  • internal – modyfikator internal jest podobny do modyfikatora public z taką różnicą, że elementy są widoczne tylko wewnątrz projektu/biblioteki w której zostały utworzone, więc jeśli stworzysz klasę oznaczoną jako internal to będzie ona dostępna tylko w tym konkretnym projekcie, jeśli jest to biblioteka, którą chcesz wykorzystać w innym projekcie to przy próbie utworzenia klasy z tym modyfikatorem uzyskasz komunikat o tym, że nie masz do niej (tej klasy) dostępu

No i teraz trochę przykładów do każdego z nich

Modyfikator dostępu – public

public class Player
{
	public string Name;
}

Klasa publiczna z polem publicznym Name. Dostęp do elementu i klasy ogólny, więc po utworzeniu obiektu tej klasy, po kropce wyświetli nam dostęp do pola Name

Modyfikator dostępu – private

public class Player
{
	private string _name;
}

Ponownie klasa publiczna, ale tym razem z polem prywatnym _name. Dostęp do tego pola będzie możliwy tylko wewnątrz tej klasy, dlatego przy próbie wyświetlenia elementów tej klasy po utworzeniu obiektu nie będzie on wyświetlany.

Modyfikator dostępu – protected

public class Player
{
	protected string Name;
}

public class Archer: Player
{
	public Archer()
	{
		
	}
}

W powyższym przykładzie mamy proste dziedziczenie i jak widać po screenach, dostęp do pola Name jest możliwy tylko w klasie bazowej i w klasie Archer, ale po utworzeniu obiektu po kropce już nie wyświetli, więc jeśli chcielibyśmy uzyskać do niej dostęp to jedną z opcji byłoby utworzenie dodatkowej metody, która umożliwiłaby do niej dostęp, np. w klasie bazowej metodka GetName

public class Player
{
	protected string Name;
	public string GetName() => Name;
}

Modyfikator dostępu – internal

Klasy utworzone w projekcie „Dziedziczenie”
public class Player
{
	internal string Name;
}

public class Archer: Player
{
	public Archer()
	{
		Name = "Łucznik";
	}
}

internal class Mage: Player
{
	public Mage()
	{
		 Name = "Mag";
	}
}

W powyższym przykładzie mamy klasę bazową Player, która posiada pole Name oznaczone jako internal, czyli dostęp do niej będzie w tej klasie, w klasach dziedziczących oraz ogólnie w projekcie/bibliotece w której została utworzona klasa Player, dodatkowo będzie widoczny do niej dostęp po kropce po utworzeniu obiektu. Na screenshotach z prawej strony widzimy utworzonego Playera w drugim projekcie i jak widać, nie mamy dostępu do tego pola, podobnie przy próbie utworzenia obiektu klasy Mage, otrzymaliśmy komunikat o braku dostępu, ponieważ klasa była oznaczona jako internal.


4. Klasa i metoda abstrakcyjna (abstract)

Klasa abstrakcyjna jest czymś podobnym do interfejsów (o których jeszcze nie wspominałem) z taką różnicą, że może posiadać ciała metod jak i metody wirtualne (o tym też w późniejszych wpisach) itd. Z takich ogólnych informacji, które warto wiedzieć o klasie abstrakcyjnej to to, że nie można utworzyć instancji takiej klasy, można po niej dziedziczyć, ale już utworzyć takiego obiektu nie można, dodatkowo tylko w takiej klasie można utworzyć metody abstrakcyjne. Metoda abstrakcyjna to taka metoda, która posiada tylko swoją definicję bez ciała, ciało metody jest zaimplementowane dopiero w klasach pochodnych.

Patrząc na wcześniejszy przykład kodu i klasę Player, na jej podstawie tworzyliśmy klasę Archer, Mage i Warrior, nie chcielibyśmy tworzyć Playera, on miał tylko służyć nam za rodzica po którym będziemy dziedziczyć, więc przydałoby się uniemożliwić możliwość tworzenia.

public abstract class Player
{
	public abstract void Attack (float value); 
	public void GetInformations() { }
}

Teraz utworzymy klasę Archer, która będzie dziedziczyła po klasie abstrakcyjnej Player

public class Archer : Player
{
    public Archer()
    {
        
    }
}

Lecz, gdy zostawimy to w takim stanie jakim jest to otrzymamy komunikat o tym, że klasa Archer nie ma implementacji metody Attack z klasy bazowej, jak łatwo zauważyć, metoda Attack została oznaczona jako abstrakcyjna, więc musi mieć swoją implementację w klasach, które dziedziczą po klasie Player

Komunikat o tym, że klasa Archer nie posiada implementacji metody Archer z klasy bazowej Player
public class Archer : Player
{
	public Archer()
	{
		   
	}

	public override void Attack (float value)
	{
		throw new NotImplementedException();	
	}
}

I teraz już wszystko piko belo, dodaliśmy implementację metodki Attack i już wszystko ok. Żeby wygenerować automatycznie tzn. bez potrzeby ręcznego pisania, wystarczy najechać kursorem na klasę Archer i poczekać aż wyświetli się żarówka, kliknąć na nią i wybrać opcję (zazwyczaj pierwszą) odpowiedzialną za generowanie metody, gdy nie wyświetli nam żarówy to kliknij na klasę Archer i przytrzymaj lewy ALT + Enter aż wyświetli menu kontekstowe i wybierz odpowiednią opcję.


5. Klasa zapieczętowana (sealed)

Klasa zapieczętowana to taka klasa po której nie chcemy dziedziczyć. Dla przykładu mamy klasę bazową Race, czyli rasa, później tworzymy klasę Elf, NightElf i nie chcemy żeby ktoś inny dziedziczył po klasie NightElf. Żeby dokonać takiej blokady, trzeba dodać słowo kluczowe sealed obok słowa kluczowego class przy tej konkretnej klasie

public abstract class Race
{
	public string Name;
}

public class Elf: Race
{

}

public sealed class NightElf: Elf
{

}

I teraz, gdy ktoś będzie próbował utworzyć klasę, która dziedziczyłaby po klasie NightElf, dostanie komunikat o braku takiej możliwości


6. Wielokrotne dziedziczenie (Interface)

Jak wspomniałem wcześniej, w C# niestety nie można dziedziczyć po wielu klasach bazowych, możemy dziedziczyć tylko po jednej klasie, ale za to możemy dziedziczyć po wielu interfejsach (czym one są to o tym będzie pewnie w przyszłości).

Dla przykładu weźmy taką sytuację, że mamy klasę Race utworzoną wcześniej, tworzymy tego Elfa, który dziedziczy po klasie Race i chcemy dodać jeszcze dziedziczenie po jeszcze innej klasie tylko po to żeby akurat ten Elf posiadał jeszcze parę dodatkowych pól i metod, no ale kurcze lipa, nie możemy dziedziczyć po dwóch klasach -.-.. na upartego można by przenieść elementy z tej drugiej klasy do tej pierwszej no ale wtedy wszystkie inne klasy, które dziedziczyły po Race, miały by do nich dostęp, a tego nie chcemy i dodatkowo powstałby burdel.. W takiej sytuacji na pomoc przychodzą właśnie interfejsy, są one podobne do klasy abstrakcyjnej jak i metod abstrakcyjnych, czyli jeśli będziemy dziedziczyli po danym interfejsie to taka klasa będzie musiała mieć implementację wszystkich elementów, które ten interfejs posiada, coś w stylu do dziedziczenia z klasy abstrakcyjnej, która ma ileś tam elementów abstrakcyjnych, wtedy wszystkie klasy pochodne też będą musiały mieć implementacje tych elementów abstrakcyjnych z klasy bazowej.

Prościej będzie pokazać na przykładzie, poniżej na początek prosty przykład klasy abstrakcyjnej i klasy, która po niej dziedziczy.

public abstract class Race
{
    
}

public class Elf: Race
{
    
}

oraz komunikat jaki uzyskamy, gdy będziemy chcieli dziedziczyć po jeszcze innej klasie

Informacja o tym, że nie można dziedziczyć po wielu klasach bazowych

No i teraz przykład wykorzystania interfejsów, najpierw kodzik

public interface IShowInfo
{
	void ShowInfo();
}

public interface IShowRaceDetail
{
	void ShowRaceDetail();
}

public class NightElf: Elf, IShowInfo, IShowRaceDetail
{

}

Jak widać, utworzone mamy dwa interfejsy, które posiadają po jednej metodzie. Na samym dole jest klasa NightElf, która dziedziczy po klasie Elf oraz po dwóch interfejsach. W C# dziedziczyć można po wielu interfejsach no i tak z grubsza trochę informacji o co kaman z tymi interfejsami żebyście wiedzieli chociaż podstawowo o co się rozchodzi.

Więc interfejsy tworzymy podobnie jak klasy, z taką różnicą, że zamiast słowa kluczowego class, korzystamy ze słowa kluczowego interface. Dodatkowo nazwa takiego interfejsu zazwyczaj zaczyna się od dużej litery ‘i’ np. IShowInfo, IShowRaceDetail itp., wtedy przy odwołaniach z automatu będziemy wiedzieli co jest klasa, a co interfejsem. Wszystko co znajduje się w interfejsie z automatu oznaczone jest jako publiczne, więc nie musimy dopisywać modyfikatora dostępu public. Dodatkowo w interfejsach nie podajemy definicji metod.

Klasa, która dziedziczy po interfejsie, musi mieć implementacje elementów, które w niej (w interfejsie) się znajdują

Na początku, gdy dodamy interfejsy do dziedziczenia, otrzymamy komunikaty o błędzie, rozchodzi się o brak implementacji elementów, które znajdują się w interfejsie, wystarczy ręcznie je dodać lub po prostu najechać kursorem na interfejs i poczekać na żarówę (klik na interfejs i lewy ALT + enter jeśli żarówa się nie wyświetla) i wybrać generowanie metod/elementów z tego interfejsu, coś podobnego co robiliśmy przy metodach abstrakcyjnych. W powyższym przykładzie dziedziczymy po dwóch interfejsach, więc powtarzamy czynność podwójnie. Poniżej pokażę jak wygląda klasa domyślnie po dodaniu brakujących implementacji metod

public class NightElf: Elf, IShowInfo, IShowRaceDetail
{
	public void ShowInfo()
	{
		throw new NotImplementedException();
	}

	public void ShowRaceDetail()
	{
		throw new NotImplementedException();
	}
}

No ale ale, może być taka sytuacja w której będziemy mieli interfejsy z takimi samymi nazwami metod i co wtedy? skąd będzie wiadomo, która metoda jest z jakiego interfejsu skoro przy implementacji doda nam tylko jedną metodę.. To mi kurczę nie przyszło do głowy w trakcie tworzenia prezentacji i nagrywania hehe, dopiero teraz przy pisaniu wpisu na bloga.. pewnie dorzucę to jeszcze tam do materiałów tak na zaś i może dogram krótki materiał w którym to omówię, no ale to się jeszcze zobaczy, więc do rzeczy.

Na początek przykład takich dwóch interfejsów oraz klasy, która po nich dziedziczy, dla przykładu weźmiemy interfejs IBook i IFile oraz klasę Manager, będą one miały metodę Read i najpierw pokaże jak będzie wyglądała domyślna implementacja oraz wywołanie

public interface IBook
{
	void Read();
}

public interface IFile
{
	void Read();
}

public class Manager : IBook, IFile
{
	public void Read()
	{
		Console.WriteLine("Odczyt ??");
	}
}
Manager m = new();
m.Read();

Powyżej proste utworzenie obiektu Manager i wywołanie metody Read, która wyświetla komunikat w konsoli. W tym momencie nie wiemy czy metoda Read jest z interfejsu IBook czy IFile.. Dokonajmy więc małej modyfikacji klasy Manager

public class Manager : IBook, IFile
{
	void IBook.Read()
	{
		Console.WriteLine("Odczyt książki");
	}

	void IFile.Read()
	{
		Console.WriteLine("Odczyt pliku");
	}
}

Jak można łatwo zauważyć, zastąpiliśmy jedną metodę Read, dwiema metodami, dodatkowo zniknął modyfikator dostępu public oraz przed nazwą metody dodałem nazwę interfejsu. Teraz będzie wiadomo z jakiego interfejsu pochodzi dana metoda, no ale ale.. teraz przy próbie odwołania się do metody Read po utworzonym obiekcie klasy Manager, nie wyświetli nam tych metod..

Nie wyświetla metod Read, ponieważ są one prywatne

I teraz żeby dostać się do tych metod, można zrobić to na parę sposobów, pierwszy to normalne utworzenie obiektu klasy Manager oraz pól tych interfejsów z których chcemy wywołać metodę Read albo po prostu machnąć rzutowanie przy odczycie, pokażę to w jednym przykładzie.

// Opcja pierwsza z pojedynczymi interfejsami oraz wywołanie
IBook ism = m;
IFile ism2 = m;
ism.Read();
ism2.Read();

// Opcja druga bez tworzenia zmiennych pod wywołanie
(m as IBook).Read();
(m as IFile).Read();

// Można jeszcze tak
((IBook)m).Read();
((IFile)m).Read();

Jak widać wyżej, możemy wywołać metodki na trzy sposoby. Warto jeszcze dodać, że minus takiego czegoś (interfejsy posiadają metodę z taką samą nazwą i dziedziczą w tej samej klasie) jest taki, że wewnątrz klasy nie można odwołać się do takiej implementacji metody, czyli jak mam IBook.Read to np. wewnątrz konstruktora już nie wywołam Read() jak i nie zadziała IBook.Read().. przynajmniej na chwilę obecną nie wiem jak to ogarnąć, jeśli macie jakieś pomysły no to napiszcie w komentarzu i z edytuję i dodam.

No i dobra, to by było na tyle jeśli chodzi o dziedziczenie, postarałem się omówić czym ono jest, czym jest klasa bazowa, z jakich i z ilu elementów (klas/interfejsów) możemy dziedziczyć. Wspomniałem o tym czym jest klasa jak i metoda abstrakcyjna, klasa zapieczętowana, omówiliśmy sobie modyfikatory dostępu. Jeśli będziecie chcieli to mogę rozbić tematykę klasy zapieczętowanej, zabezpieczonej, interfejsu na osobne wpisy/nagrania co być może i tak uczynię w przyszłości:)

Tagi
O Autorze
Tajko
Tajko Tajko z tej strony:) Obecnie pracuję we Wrocławiu przy projektach desktopowych. Skoro sam się czegoś nauczyłem to i inni mogliby nauczyć się tego co ja w łopatologiczny sposób:)

ZOSTAW ODPOWIEDŹ