Kolekcje c.d. - Lista - Kolejka - Stos - Słownik Tworzenie i użycie klas "własnych" - Modyfikatory dostępu - Argumenty metod - Tworzenie obiektów - Hermetyzacja danych akcesory - Składowe statyczne - Usuwanie obiektów - Przeciążanie metod i konstruktorów - Specjalizacja i uogólnianie - Dziedziczenie - Polimorfizm - Klasy abstrakcyjne - Klasy zamknięte - Klasa główna - Object 1
Klasa List Poważną niedogodnością tablic jako kolekcji danych jest fakt, że muszą one mieć stały rozmiar. Programista musi wiedzieć z góry ile miejsca powinien zarezerwować na pomieszczenie wszystkich przetwarzanych danych. Problem ten nie występuje w klasie List. Jest to zbiór (kolekcja) danych, których liczba może się dynamicznie zmieniać. Klasa ta udostępnia szereg wygodnych w użyciu metod i właściwości zebrano je w tabeli 2.1. 2
Tabela 2.1. Metody i właściwo ści klasy List Metoda lub właściwość Capacity Count Item() Add() AddRange() BinarySearch() Clear() Contains() CopyTo() Exists() Find() FindAll() GetEnumerator() GetRange() IndexOf() Insert() InsertRange() LastIndexOf() Remove() RemoveAt() RemoveRange() Reverse() Sort() ToArray() TrimToSize() Działanie Właściwość do pobierania i ustawiania liczby elementów, które może zawierać lista Zwraca liczbę aktualnie przechowywanych elementów Pobiera element o określonym indeksie lub przypisuje do niego wartość Metoda publiczna dodaje element do listy Publiczna metoda, która dodaje na koniec listy elementy z ICollection Wyszukuje binarnie określony element w posortowanej liście Usuwa z listy wszystkie elementy Określa czy dany element znajduje się na liście Kopiuje listę do jednowymiarowej tablicy Sprawdza czy element znajduje się na liście Zwraca pierwsze wystąpienie elementu na liście Zwraca wszystkie dane elementy występujące na liście Zwraca licznik, który umożliwia przechodzenie po elementach listy Kopiuje zakres elementów do nowej listy Zwraca indeks pierwszego wystąpienia danej wartości na liście Dodaje element do listy Dodaje zakres elementów kolekcji do listy Zwraca indeks ostatniego wystąpienia wartości na liście Usuwa z listy pierwsze wystąpienie określonego elementu z listy Usuwa element o podanym indeksie Usuwa zakres elementów Odwraca kolejność elementów listy Sortuje listę Kopiuje elementy listy do nowej tablicy Ustawia pojemność (właściwość Capacity) na aktualną liczbę elementów listy
4
5
6
Kolejka Kolejka jest kolekcją typu FIFO (First-In, First-Out ) Metody i właściwości klasy Queue zebrano w tabeli 2.2. Metoda lub właściwość Count Clear() Contains() CopyTo() Dequeue() Enqueue() GetEnumerator() Peek() ToArray() Działanie Właściwość publiczna, która zwraca liczbę elementów kolejki Usuwa z kolejki wszystkie obiekty Sprawdza, czy dany element znajduje się w kolejce Kopiuje elementy kolejki do istniejącej tablicy jednowymiarowej Usuwa i zwraca element znajdujący się na początku kolejki Dodaje obiekt na koniec kolejki Zwraca licznik kolejki Zwraca element znajdujący się na początku kolejki, ale go nie usuwa Kopiuje elementy do nowej tablicy Listing 2.2 zawiera przykład prostej aplikacji ilustrującej operacje na kolejce. Umieszczono tam specjalnie oznaczone komentarze, które w czasie kompilacji pozwolą automatyczne utworzyć plik XML stanowiący "dokumentację" programu 7
8
9
10
11
Stos Stos jest kolekcją typu LIFO (Last-In, First-Out ) Metody i właściwości klasy Stack zebrano w tabeli 2.3. Metoda lub właściwość Count Clear() Clone() Contains() CopyTo() GetEnumerator() Peek() Pop() Push() ToArray() Działanie Właściwość publiczna, która zwraca liczbę elementów stosu Usuwa ze stosu wszystkie obiekty Tworzy kopię stosu Sprawdza, czy dany element znajduje się na stosie Kopiuje elementy stosu do istniejącej tablicy jednowymiarowej Zwraca licznik stosu Zwraca element znajdujący się na szczycie stosu, ale go nie usuwa Usuwa i zwraca obiekt ze szczytu stosu Wstawia obiekt na szczyt stosu Kopiuje elementy do nowej tablicy 12
13
14
15
Słownik Słownik jest kolekcją, która zawiera wartości powiązane z kluczami. Klasa słownika udostępniana przez.net pozwala łączyć dowolny typ klucza (łańcuchy znaków, liczby całkowite, obiekty itp.) z wartościami dowolnego typu. Właściwości i metody klasy Dictionary pokazano w tabeli 2.4 Metoda lub właściwość Count Clear() Item() Keys Values Add() ContainsKey() ContainsValue() GetEnumerator() GetObjectData() Remove() Działanie Właściwość publiczna, która zwraca liczbę elementów słownika Usuwa ze słownika wszystkie obiekty Mechanizm indeksowania słownika Publiczna właściwość, która zwraca kolekcję zawierającą klucze słownika Publiczna właściwość, która zwraca kolekcję zawierającą wartości słownika Dodaje element o określonym kluczu i wartości Sprawdza, czy dany klucz znajduje się w słowniku Sprawdza, czy dana wartość znajduje się w słowniku Zwraca licznik słownika Obsługuje intefejs Iserializable i zwraca dane potrzebne do serializacji słownika Usuwa element o podanym kluczu 16
17
Tworzenie i użycie klas "własnych" Języki obiektowe charakteryzują się możliwością tworzenia nowych typów danych. Trzy najważniejsze cechy klas to: hermetyzacja właściwości i zachowań, polimorfizm i dziedziczenie. Elementy klasy zachowanie i właściwości to składowe klasy. Utworzenie klasy to kolejno jej deklaracja a następnie definicja jej metod i pól. Pełna składnia deklaracji klasy ma postać: [atrybuty][modyfikatory dostępu] class identyfikator [:klasa bazowa[.interfejsy]]{ciało klasy} W deklaracji klasy modyfikatorem dostępu jest zwykle słowo kluczowe public Obiekt to egzemplarz klasy. Różnica pomiędzy klasą a obiektem jest taka jak pomiędzy np. typem int a zmienną typu int. Przykład deklaracji klasy, definicji i jej użycia przedstawia listing 2.5 18
19
Tabela 2.5. Modyfikatory dostępu Modyfikator dostępu public private protected internal protected internal Ograniczenia Brak ograniczeń. Składowe dostępne dla wszystkich metod wszystkich klas Składowe prywatne klasy A są dostępne tylko dla metod tej klasy Składowe chronione klasy A są dostępne dla metod klasy A i dla metod klas potomnych A Składowe wewnętrzne klasy A są dostępne dla metod wszystkich klas z podzespołu klasy A Składowe chronione wewnętrzne klasy A są dostępne dla metod tej klasy, dla metod klas potomnych A oraz dla wszystkich metod wszystkich klas z podzespołu A 20
Domyślnym poziomem dostępu do składowych klasy jest poziom prywatny. Listing 2.5 jest zatem poprawny. Wszystkie pola składowe klasy Czas są prywatne. Dla zwiększenia czytelności źródła należy wyraźnie zaznaczyć nawet domyślny prywatny tryb dostępu. Deklaracje pól składowych klasy Czas powinny zatem wyglądać tak:... private int Rok; private int Miesiąc; private int Dzień; private int Godzina; private int Minuta; private int Sekunda; // Zmienne prywatne... Klasa Tester i metoda WyświetlCzas są publiczne, mogą z nich korzystać inne klasy. 21
Argumenty metod Metody mogą przyjmować dowolną liczbę parametrów. Przykład przekazywania wartości do metody za pośrednictwem parametrów pokazuje listing 2.6 22
Tworzenie obiektów Obiekty są typami referencyjnymi, przechowywane są na stercie, a tworzy się je za pomocą operatora new, np. Czas t = new Czas(); W ten sposób utworzono obiekt klasy Czas znalazł się on na stercie, a wartość zmiennej t jest jego adresem - referencją na pewien anonimowy obiekt w obszarze sterty. Zmienna t nie zawiera "wartości" obiektu Czas, a tylko jego adres. Zawsze, kiedy powstaje egzemplarz klasy następuje wywołanie specjalnej metody konstruktora. Definicja konstruktora jest częścią klasy lub domyślnie udostępnia ją CLR. Zadaniem konstruktora jest utworzenie obiektu danej klasy i nadanie mu prawidłowego stanu. Jeżeli w deklaracji klasy (np. klasy Czas) nie zdefiniowano konstruktora, to kompilator automatycznie tworzy tzw. domyślny konstruktor klasy, który jedynie tworzy obiekt nie wykonując żadnych innych czynności. Często jednak potrzebny jest specyficzny konstruktor klasy, który przyjmuje argumenty, co pozwala określić początkowy stan obiektu. Konstruktor jest metodą o takiej samej nazwie jak klasa; nie zwraca żadnej wartości i na ogół jest publiczny. Listing 2.7 przedstawia modyfikację klasy Czas dodano do niej konstruktor. 23
24
Hermetyzacja danych akcesory Bezpośredni dostęp spoza klasy do prywatnych danych składowych pól klasy opatrzonych atrybutem private. Warto jednak pozostawić dane składowe jako prywatne co zapobiega przypadkowym zmianom ich wartości lub próbie odczytu danych przez obiekt zewnętrzny do tego nieuprawniony. Hermetyzacja danych polega na oddzieleniu stanu klasy od metod zmieniających ten stan. Niektóre pola prywatne warto jednak udostępnić dla wykonywania na nich w sposób świadomy i kontrolowany określonych operacji. Do tego celu służą elementy klasy zwane akcesorami. Udostępniają one klientom prosty interfejs, który wygląda jak zwykła zmienna składowa. Jednocześnie są metodami, które pozwalają ukryć dane, zabezpieczyć je przed niekontrolowanym, przypadkowym odczytem, bądź co gorsze nieporządanymi zmianami wartości. Takie zabezpieczenie danych zwane hermetyzacją jest cechą dobrego programowania obiektowego. Tworzenie i zastosowanie właściwości pełniących rolę akcesorów ilustruje listing 2.8. 25
26
27
Składowe statyczne Składowe klasy (zmienne, metody, zdarzenia itp.) mogą być: - Składowymi egzemplarza (obiektu), - Składowymi statycznymi (klasy). Składowe egzemplarza są związane z konkretnym obiektem (egzemplarzem klasy), a składowe statyczne są elementem klasy. - Dostęp do składowych statycznych uzyskuje się przez nazwę klasy, - dostęp do składowych egzemplarza przez nazwę obiektu. W deklaracji składowej statycznej słowem kluczowym static określa się składowe statyczne. Metody statyczne działają na klasach, a nie na ich egzemplarzach (obiektach). Przykłady metod statycznych: WriteLine, ReadKey w klasie Console, Parse w strukturze np. Double Metoda Main jest statyczna. Metody statyczne nie mają referencji this bo nie istnieje obiekt, na który ta referencja mogłaby wskazywać. Metody statyczne nie mają bezpośredniego dostępu do składowych niestatycznych. Metoda Main może wywołać metodę niestatyczną tylko wtedy, gdy wcześniej utworzy obiekt (patrz listing 2.9). 28
29
30
Usuwanie obiektów Język C# obsługuje automatyczne odzyskiwanie pamięci na ogół nie jest konieczne jawne usuwanie obiektów (tzw. garbage collection). Jeżeli obiekt zawiera tzw. niezarządzane zasoby (są nimi np. pliki), to trzeba je zwolnić kiedy nie są już potrzebne. Do niejawnej kontroli niezarządzanych zasobów służą destruktory. Wywołuje je mechanizm odzyskiwania pamięci w momencie usuwania obiektu. Destruktor powinien zwalniać zasoby wykorzystywane przez dany obiekt, ale nie może zmieniać stanu innych obiektów. Zmniejsza wydajność programu. Destruktora nie można wywoływać samodzielnie za jego wywołanie odpowiada mechanizm odzyskiwania pamięci. Mechanizm odzyskiwania pamięci przechowuje listę obiektów zawierających destruktor. Aktualizacja tej lista następuje w momencie powstania lub usunięcia obiektu. Po zwróceniu do systemu pamięci zajmowanej przez obiekt system umieszcza go w kolejce obiektów czekających na usunięcie i dopiero na takim obiekcie system wykonuje operacje destruktora. 31
Przykładowy kod destruktora w klasie np. MojaKlasa ma postać: ~MojaKlasa() { // potrzebne operacje } Kompilator języka C# przekształci ten kod na: protected override void Finalize() { try { // potrzebne operacje } finally { base.finalize(); } } 32
Destruktora nie można wywoływać samodzielnie w przypadku obsługi "cennych" niezarządzanych zasobów (np. plików) trzeba użyć interfejsu Idisposable. Interfejs ten wymaga, aby klasy dziedziczące go zawierały definicję metody Dispose() Jeżeli jest dostępna ta metoda, to system nie czeka na wywołanie destruktora, a wywołuje metodę Dispose(). Jeżeli udostępniono metodę Dispose(), to należy zablokować mechanizm odzyskiwania pamięci tak aby nie wywoływał destruktora. W tym celu należy wywołać statyczną metodę GC.SuppressFinalize() z argumentem this (referencja na usuwany obiekt). Przykład ilustruje listing 2.11. 33
34
35
Przeciążanie metod i konstruktorów W przypadku konstruktorów, ale również innych metod w klasie wygodnym rozwiązaniem jest definiowanie i używanie kilku funkcji o tej samej nazwie. Sygnatura metody to jej nazwa i lista parametrów. Dwie metody mają różne sygnatury, jeżeli mają różne nazwy lub różne listy parametrów. Klasa może zawierać dowolną liczbę metod jeżeli każda z nich ma niepowtarzalną sygnaturę. Przykład klasy z dwoma konstruktorami (przeciążonymi) przedstawia listing 2.13. Jeden z tych konstruktorów przyjmuje obiekt typu DateTime a drugi 6 liczb całkowitych. 36
37
38
Specjalizacja i uogólnianie Klasy i obiekty (egzemplarze klas) istnieją w sieci zależności i związków (relacji). Specjalizacja to relacja jest-czymś np. pies jest ssakiem. Wg tego stwierdzenia pies jest specjalnym rodzajem ssaka ma wszystkie cechy ssaka, ale oprócz tego jeszcze parę innych właściwych tylko dla psów (canine domesticus). Kot jest też ssakiem i ma wiele cech wspólnych z psem, ale też kilka właściwych tylko dla kotów. Do graficznej ilustracji relacji specjalizacji i uogólniania, a ogólnie do opisu systemów często wykorzystuje się tzw. zunifikowany język modelowania (UML Unified Modeling Language ). Przykładową relację specjalizacji zapisaną w notacji UML pokazano na rysunku 2.1. 39
Rys.2.1. Relacja jest-czymś 40
Dziedziczenie W C# relacja specjalizacji jest realizowana za pomocą dziedziczenia. Stwierdzenie, że klasa ListBox dziedziczy po klasie Control oznacza, że klasa ListBox jest wyspecjalizowaną wersją klasy Control. Klasę Control nazywa się klasą bazową a klasę ListBox klasą pochodną lub potomną. Klasa ListBox dziedziczy cechy i zachowania klasy Control, ale oprócz tego może posiadać też wyspecjalizowane składowe. Przykład klasy bazowej i klasy potomnej pokazuje listing 2.14. 41
42
43
Polimorfizm Najważniejszym aspektem dziedziczenia jest polimorfizm. Oznacza on możliwość używania typu w wielu formach, niezależnie od szczegółów klas. Aby utworzyć metodę polimorficzną, należy ją oznaczyć jako wirtualną w klasie bazowej. W przykładzie 2.14 taką metodą polimorficzną jest metoda Wyświetl, której nazwa w klasie bazowej PUNKT_2D została poprzedzona słowem kluczowym virtual. Dzięki temu w każdej klasie potomnej można umieścić nową wersję tej metody. W klasie PUNKT_3D znalazła się metoda Wyświetl przeznaczona do wyświetlania współrzędnych punktu w przestrzeni trójwymiarowej. W deklaracji tej metody pojawiło się słowo kluczowe override, które powoduje, że metoda w klasie potomnej o takiej samej nazwie jak metoda w klasie bazowej "przesłania" metodę bazową. Słowo base w definicji konstruktora klasy potomnej oznacza wykorzystanie konstruktora bazowego jako części konstruktora obiektu klasy potomnej. Uwaga! Słowo base odwołujące się do konstruktora klasy bazowej umieszczono po znaku dwukropka, co oznacza, że konstruktor klasy potomnej dziedziczy metodę konstruktora bazowego. 44
45
46
Przykład polimorfizmu przedstawia listing 2.15 W tablicy zbiór zadeklarowanej jako kolekcja obiektów klasy bazowej Kontrolka umieszczono zarówno obiekt klasy bazowej jak i obiekty obydwu klas potomnych. System prawidłowo rozpoznał i wybrał odpowiednią metodę wirtualną do wyświetlenia informacji o konkretnym obiekcie. W metodzie Wyświetl, która w klasie potomnej Lista przesłania metodę Wyświetl klasy bazowej, użyto słowa kluczowego base do odwołania się do metody z klasy bazowej, a ta jest tutaj częścią definicji metody wirtualnej. Klasy potomne nie dziedziczą konstruktorów. W konstruktorze klasy pochodnej konieczne jest jawne wywołanie konstruktora klasy bazowej. W przypadku konstruktora domyślnego robi to automatycznie kompilator. Każdy konstruktor jawny w klasie potomnej musi wywoływać jeden z konstruktorów klasy bazowej. Do tego celu wykorzystuje się słowo kluczowe base. 47
Klasy abstrakcyjne W przykładzie pokazanym na listingu 2.15 każda klasa potomna klasy Kontrolka powinna zawierać własną wersję metody Wyświetl. W pokazanym przykładzie nie istnie mechanizm, który zmusiłby programistę do takiego postępowania. Aby wymusić tworzenie nowych wersji metod klasy bazowej, należy oznaczyć taką metodę jako abstrakcyjną za pomocą słowa kluczowego abstract. Umieszczenie w klasie przynajmniej jednej metody abstrakcyjnej powoduje, że cała klasa staje się abstrakcyjna. Klasy abstrakcyjne stanowią bazę dla klas pochodnych. W programie nie można tworzyć obiektów klas abstrakcyjnych. Przykład definicji i użycia klasy abstrakcyjnej przedstawia listing 2.16. 48
49
50
51
Klasy zamknięte Klasy zamknięte stanowią przeciwieństwo klas abstrakcyjnych. Od klas zamkniętych nie można tworzyć klas pochodnych. Tworzeniu klas pochodnych zapobiega słowo kluczowe sealed umieszczone przed deklaracją klasy. Stosuje się ten sposób, aby zapobiec przypadkowemu dziedziczeniu. Próba utworzenia klasy potomnej w stosunku do klasy oznaczonej jako "zapieczętowana" powoduje przerwanie kompilacji z komunikatem informującym o niemożności dziedziczenia. 52
Klasa główna - Object Wszystkie klasy języka C# są klasami pochodnymi od klasy System.Object (dotyczy to także typów skalarnych). Klasa Object udostępnia wiele metod wirtualnych, które mogą być przesłaniane w klasach potomnych. Przykładami są metody Equals(), GetType(), ToString(). W tabeli 2.6 zestawiono metody klasy Object. Metoda Equals() GetHashCode() GetType() ToString() Finalize() MemberwiseClone() ReferenceEquals() Działanie Sprawdza czy dwa obiekty są sobie równe Pozwala obiektom udostępnić własną funkcję haszującą, używaną w kolekcjach Pozwala sprawdzić typ obiektu Zwraca łańcuch znaków reprezentujący obiekt Porządkuje zasoby inne niż pamięć. Implementowana za pomocą destruktora. Tworzy kopie obiektu. Nie można jej przesłonić w klasach definiowanych przez użytkownika Sprawdza, czy dwa obiekty wskazują na ten sam egzemplarz 53