Klasy w C#Część NR.1 Ostatnio skończyłem kurs: podstawy C#.Jednak nie było w nim nic o klasach i obiektach. Do pisania prostych programów rzeczywiście można się obyć bez klas. Jednak nie bez powodu C# jest językiem obiektowym .Nikt też nie da nagrody za napisanie programu, który dodaje dwie cyfry. Poprzedni kurs był dobry jako wstęp ,ale nie zmienia to faktu ,że trzeba jeszcze bardziej poszerzać wiedzę.
C# nie jest trudnym językiem programowania ,więc moim zdaniem nie będziesz miał z tym kursem dużych problemów.
Platforma .NET zawiera tysiące klas i nawet jeśli jesteś początkującym programistą C# na pewno zetknąłeś się z nimi. Klasy tworzą dobry mechanizm w zarządzaniu i modelowaniu encji manipulowanymi przez aplikacje. Encja może reprezentować określony przedmiot jak produkt, bądź pracownika albo nawet transakcje.
W zależności od naszego poziomu abstrakcji encje mogą prosto obrazować rzeczywistość bądź tworzyć cały złożony system.
Obiekty klas też mają różną długość życia. Niektóre obiekty służą do przechowywania informacji przez prawie cały proces aplikacji ,a niektóre z nich są usuwana zaraz po ich użyciu.
Zrozumienie KLAS
Tworząc klasę tworzysz systematyczny sposób układania informacji oraz styl zachowania danej encji.
Różnica pomiędzy klasą a obiektem jest taka ,że obiekt jest instancją danej klasy. Czyli różnica pomiędzy nimi jest niczym jak kategoria kot ,a konkretny kot. Nie można się bawić z “pojęciem kot” tylko z jego konkretnym egzemplarzem.
Klasa Koto pisuje, jakie są koty. W klasie takiej mielibyśmy informacje, jakie można by było uzyskać o kocie jak np. kolor sierści, wiek ,waga ,wysokość ,długość ,zachowanie ,a może nawet jego imię. Klasa ta mogłaby zawierać metody jak np. miauczenie ,spanie ,jedzenie i tak dalej.
Konkretny kot miałbym . na imię Felix ,posiadałby szarą sierść, miałby 10 miesięcy ,ważyłby 3 kg i tak dalej.
Kot taki miałby też specyficzne zachowanie kotów, czyli posiadałby takie metody jak miauczenie.
W podobny sposób ludzie używają słów jak np.“samochód” do określenia obiektu z danymi atrybutami i zachowaniem. Bez klasyfikacji komunikacja między ludźmi byłaby niemożliwa, ponieważ nikt nie wiedziałby, o jaki obiekt osobie chodzi.
Nawet jeśli nie mówimy o programowaniu, to system klasyfikacji obiektów pozwala nam łatwiej zrozumieć rzeczywistość, ze względów na istnienie powielanych właściwości w przeróżnych obiektach.
Pomysł klasyfikacji w programowaniu jest jak najbardziej trafiony. Poprzez klasyfikacje i dziedziczenie możemy rozwiązać wiele złożonych problemów i do jakiegoś stopnia zmapować otaczającą nas rzeczywistość.
To jest właśnie to, co robi się w obiektowo-zorientowanym języku programowania jak C#.
Cel hermetyzacja
Hermetyzacja jest to ważna zasada, o której trzeba pamiętać tworząc własną klasę. Główną pomysł jest taki, aby program “nie martwił się” czy klasa rzeczywiście pracuje wewnętrznie. Co mam na myśli. Program np. wywołujący metodę Console.WriteLine nie powinien “zawracać sobie głowy” czy klasa Console aranżuje dane by wyświetlić tekst w konsoli. Kolejnym przykładem może być klasa kontrolki ListBox ,a raczej jej egzemplarz, w którym wystarczy wywołać odpowiednią metodę, by wszystko posortować.Sposób, w jaki to sortowanie zostanie wykonane dla programu jest bez znaczenia. Ważne jest to ,że zawartość zostanie posortowana.
Klasa musi przechowywać różne dane aby wykonywać różne metody i obrazować swój wewnętrzny stan. Ten stan informacji jest ukryty przed programem, który korzysta z klasy. Hermetyzacja spełnia więc następujące cele.
- Klasa posiada swoje dane i metody. Wspiera klasyfikacje.
- Danie kontroli nad stopniem dostępności danych i metod. Czyli określeniem gdzie i kto powinien używać danej klasy.
Hermetyzacja wraz z polimorfizmem i dziedziczeniem to najważniejsze i podstawowe cechy programowania obiektowego. Nie martw się wszystko zostanie omówione w swoim czasie.
Używanie klas
Naturalnie w C# , jak się domyślasz do deklaracji klas mam słowo kluczowe “class”. Dane i metody są zawarte wewnątrz ciała klasy, czyli pomiędzy nawiasami klamrowymi. Tutaj możesz zobaczyć przykładową klasę.
Ciało klasy jest następujące:
class Cuboid
{
int a;
int b;
int h;
int Volume()
{
return a * b * h;
}
}
Ciało tej klasy zawiera w sobie metodę: Volume, która oblicza objętość prostopadłościanu oraz zmienne, które są potrzebne aby ten prostopadłościan został określony. Prostopadłościan ma wysokość h oraz dwa boki jego podstawy, a i b. Użycie klasy Cuboid jest zbliżone do używania inny typów, z którymi się już zetknąłeś. Pojawia się jednak nowe słowo kluczowe “new”.
Cuboid cu; // tworzenie zmiennej
Cuboid cu = new Cuboid(); //Inicjalizacja
int a;
a = 121;
Jak widzisz istnieje podstawowa różnica pomiędzy inicjalizacją klasy ,a zmienną. Aby zainicjalizować zmienną wystarczyło tylko do niej przypisać wartość. W klasach nie ma takiego wyrażenia, które przypisywałoby wartości klas na zmienne. Jednym słowem taka operacja nie przejdzie.
Cuboid cu;
cu = 121;
Używając słowa new tworzymy nowy obiekt, który jest typu klasy w tym przypadku “Cuboid”. Obiekt ten jest w innym sposób zarządzany w pamięci niż zmienna np.int.
Można co prawda przypisać instancje klasy do innej klasy tego samego typu,ale…
Cuboid cu;
cu = new Cuboid();
Cuboid cu2;
cu2 = cu;
Ale istnieje pewna różnica wynikająca z tego, iż klasa nie jest typem wartościowym ,a jest typem referencyjnym. Tak naprawdę ten fragment kodu nie stworzył kopi obiektu tylko teraz ten sam obiekt ma dwie referencje cu i cu2.Czyli zmiana wartości w klasie cu zmieni też wartość cu2, ponieważ obie referują się do tego samego obiektu.
Więcej informacji o tym w trzecim wpisie kursu: typ referencyjny ,a typ wartościowy.
Dostępność pól
Na razie nasza klas “Cuboid” nie może być użyta (a raczej jej zawartość) ,ale dlaczego?
Kiedy piszesz wewnątrz swojej klasy “metody” bądź “dane”, klasa formuje granicę tych pól do świata znajdującego się poza nią.
Pola takie jak “a”,”b”,”h” oraz metody jak “Volume” mogą być widoczne dla innych klas i ich metod ,ale na razie nie są. W wypadku niepodania słowa kluczowego dostępności kompilator uznaje ,że pole jest typu “private”. Prywatne pola są widoczne tylko dla swojej klasy ,ale nie są widoczne dla świata poza nią.
Oto mała ilustracja z książki Head First C# ,która pomoże w lepszym zrozumieniu dostępności klas (przy okazji dobra książka).
Mimo iż stworzysz obiekt “Cuboid” to i tak wciąż nie masz dostępu do jego pól sprawiając ,że obecnie z tą klasą nie możesz nic zrobić.
Oczywiście możesz zmodyfikować pola dostępu wypełniając je słowami kluczowymi, które fachowo nazywają się modyfikatorami dostępu. Dla lepszej czytelności kodu lepiej aby pola prywatne też posiadały modyfikator “private”, tak by programista nie miał wątpliwości, w jakiej dostępności są te pola.
W programowaniu najczęściej są spotykane pola “private” i “public” ,ale oczywiście jest ich trochę więcej.
Tabelka ze wszystkim dostępnościami w C# znajduje się poniżej.
Modyfikator dostępu | Ograniczenia |
public | Brak ograniczeń. Publiczne klasy są dostępne dla wszystkich metod wszystkich klas. |
private | Składowe prywatne są dostępne tylko dla metod klasy, w której się znajdują. |
protected | Składowe chronione są dostępne dla klasy, w której się znajdują oraz dla klas dziedziczących po niej w danej bibliotece. |
internal | Składowe wewnętrzne są dostępne dla klasy znajdującej się w danym podzespole (biblioteka, pil *.dll) |
protected internal | Nie ma modyfikatora, który wyrażałby zachowanie wynikające z kombinacji protected lub internal. Jakakolwiek klasa, która dziedziczy po tej klasie ma dostęp do tych pól nawet jeśli znajduje się w innej bibliotece. Dostęp do pól ma też jakakolwiek klasa w danym podzespole. |
Oto nasz kod klasy “Cuboid” tym razem uzupełniony modyfikatorami dostępu. Jak widać są one pisane przed deklaracją typu.
Ogólnie złą praktyką jest dawanie wszystkim polom modyfikatora dostępu public, ponieważ łamie to zasadę hermetyzacji. Nie wszyscy muszą widzieć, co się magicznego dzieje w danej klasie.
W C# jeśli nie podasz modyfikatora przed polem to jest ono automatycznie traktowane jako private.
class Cuboid
{
int a;
int b;
int h;
int Volume()
{
return a * b * h;
}
}
Poczekaj stary.
Teraz rzeczywiście będziesz miał dostęp do metody Volume ,ale jaki jest sens obliczania objętości, jeśli nie mogę podać zmiennych do jego obliczenia. W końcu one są prywatne ,a skoro są prywatne to tylko metoda wykonująca się wewnątrz klasy ma do nich dostęp (Volume).
Oczywiście istnieje sposób na nadanie tym polom wartości bez łamania ich prywatności.
Do tego właśnie służy konstruktor.
Konstruktor
Kiedy używasz słowa kluczowego new tworzysz obiekt. Program w czasie swojego działania, gdy dojdzie do tej linijki kodu musi skonstruować ten obiekt odnosząc się do definicji klasy. Program wtedy wypełnia fragment pamięci z systemu operacyjnego polami zdefiniowanymi w klasie oraz wywołuje konstruktor do spełnienia wymagań przy inicjalizacji.
Konstruktor to specjalna metoda, która wywołuje automatycznie, kiedy tworzysz instancje klasy. Ma dokładnie taką samą nazwę jak klasa ,ale nie może niczego zwracać nawet void.
Każda klasa musi mieć konstruktor, jeśli go nie napiszesz kompilator tworzy domyślny konstruktor dla klasy. Ten domyślny konstruktor prawie nic nie robi i nie pobiera żadnych zmiennych.
Napisanie własnego konstruktora nie stanowi dużego problemu. Ważne, by konstruktor był publiczny inaczej nie może być on wywołany.
Chyba że stosujesz już wzorce projektowe jak Singleton. Czyli nawet prywatne konstruktor-y mają sens w niektórych zastosowaniach.
class Cuboid
{
public Cuboid()
{
a = 0;
b = 0;
h = 0;
}
private int a;
private int b;
private int h;
public int Volume()
{
return a * b * h;
}
}
Konstruktor, który został tu napisany wciąż jest bez parametrowy i przypisuje polom a,b,h wartość zero. Czyli obliczanie objętość w naszej klasie jest niemożliwe.
Zrobiłem to celowo by zaprezentować przeciążanie konstruktorów. Klasa może mieć kilka wersji konstruktorów.
Przeciążanie konstruktorów
Tak jak powiedziałem wcześniej konstruktor to metoda i jak każda metoda może być przeciążona.
class Cuboid
{
public Cuboid()
{
a = 0;
b = 0;
h = 0;
}
public Cuboid(int inta, int intb, int inth)
{
a = inta;
b = intb;
h = inth;
}
private int a;
private int b;
private int h;
public int Volume()
{
return a * b * h;
}
}
Kolejność konstruktorów nie ma znaczenia w klasie. Może definiować je, w jakiej kolejności ci się to podoba. Jak widzisz Intellisense w Visual Studio informuje o kilku wersjach konstruktora.
W ten sposób możesz stworzyć obiekt klasy Cuboid
Cuboid cuboid = new Cuboid(1,2,4);
Console.WriteLine(cuboid.Volume());
Warto zaznaczyć ,że domyślny konstruktor bez parametrowy pisany przez kompilator zostanie utworzony tylko wtedy, gdy dana klasa nie ma żadnego konstruktora. Czyli jeśli chcesz by klasa miała konstruktor bez parametrowy, gdy już ma inny konstruktor to musisz go sam napisać.
To nie koniec o klasach
W następnym wpisie klasa statyczna i anonimowa.