Programowanie zaawansowane Kolekcje i typy generyczne
Pytania egzaminacyjne (Zestaw01) Co to jest takiego ta cała platforma .NET? Jakie są jej najważniejsze cechy? Jakie elementy składają się na platformę .NET? Co to jest CLR? Co to jest CTS? Co to jest CLS? Co to jest CIL? Co to jest CLI?
Pytania egzaminacyjne (Zestaw02) Co to są pakiety? Co to są typy i co do nich zaliczamy? Jaka jest zależność między pakietami, przestrzeniami nazw i typami? W jaki sposób wykonywane są programy na platformie .NET?
Pytania egzaminacyjne (Zestaw03) Na czym polega OOP i jakie są jego trzy podstawowe filary? Na czym polega dziedziczenie? Do czego wykorzystujemy słowa kluczowe protected, base, sealed? Na czym polega agregacja? Na czym polega zagnieżdżanie? Na czym polega delegacja?
Pytania egzaminacyjne (Zestaw04) Na czym polega polimorfizm? Co to są abstrakcyjne klasy bazowe? Co to jest interfejs polimorficzny? Na czym polega nadpisywanie i zasłanianie składowych?
Pytania egzaminacyjne (Zestaw05) Czym charakteryzuje się klasa System.Object? Na czym polega obsługa wyjątków? Jakie są bloki i słowa kluczowe związane z obsługą wyjątków? Jak realizujemy przechwytywanie wielu wyjątków?
Pytania egzaminacyjne (Zestaw06) Co to jest interfejs? Czym interfejs różni się od interfejsu polimorficznego? W jaki sposób implementujemy interfejsy? Do czego służą słowa kluczowe as i is? Czym się różni jawna i niejawna implementacja interfejsów? Co trzeba zrobić, aby po zawartości naszej klasy można było iterować za pomocą pętli foreach Czym się różnie płytkie i głębokie klonowanie? Co trzeba zrobić, aby kolekcję naszych obiektów można było posortować?
Plan wykładu Kolekcje standardowe Typy i kolekcje genryczne Tworzenie niestandardowych typów, metod i kolekcji generycznych
Kolekcje standardowe
Tablice o ustalonej wielkości Najmniej skomplikowaną konstrukcją kontenerową jest System. Array. Udostępnia ona wiele przydatnych funkcji, takich jak sortowanie, odwracanie kolejności, usuwanie i wyliczanie. Nie zmienia automatycznie swojego rozmiaru podczas dodawania i usuwania elementów. W przypadku, gdy chcemy przechowywać kolekcję elementów w bardziej elastycznej formie, możemy skorzystać z typów zdefiniowanych w przestrzeni nazw System.Collections.
Przestrzeń nazw System.Collections W przestrzeni nazw System.Collections zdefiniowanych jest wiele interfejsów, z których część już poznaliśmy. Większość klas zdefiniowanych w System.Collections implementuje te interfejsy w celu zapewnienia ujednoliconego sposobu dostępu do elementów kolekcji. Sposób dostępu do elementów kolekcji jest wspólny dla ich określonych rodzajów (lista, słownik, kolejka, stos).
Interfejsy w System.Collections ICollection – definiuje ogólne cechy (np., rozmiar, wyliczenie, bezpieczeństwo dostępu w wątkach) dla większości kolekcji niegenerycznych. IComparer – pozwala na porównywanie dwóch obiektów, IDictionary – pozwala obiektowi kolekcji niegenerycznej reprezentować zawartość za pomocą par nazwa/wartość, IDictionaryEnumerator – wylicza zawartość typu wspierającego interfejs IDictionary,
Interfejsy w System.Collections c. d. IEnumerable – zwraca interfejs IEnumerator dla danego obiektu, IEnumerator – umożliwia iterację po elementach za pomocą konstrukcji foreach, IHashCodeProvider – zwraca kod hasz typu generowany za pomocą zdefiniowanego algorytmu haszowania, IList – modeluje zachowanie kolekcji polegające na możliwości dodawania, usuwania i indeksowania jej elementów
Hierarchia interfejsów w System.Collections
Rola interfejsu ICollection Interfejs ICollection definiuje zbiór najbardziej bazowych metod, które ma wspierać każda kolekcja: Zwrócenie liczby elementów kolekcji, Metody związane z bezpieczeństwem kolekcji w wątkach, Możliwość skopiowania zawartości kolekcji do tablicy (typu System.Array).
Rola interfejsu IDictionary Interfejs IDictionary wspierany jest przez kolekcje przechowujące zbiór par nazwa/wartość. Definiuje on właściwości Keys i Values oraz metody pozwalające na operowanie na zawartości słownika.
Rola interfejsu IDictionaryEnumerator Interfejs IDictionaryEnumerator jest zwracany przez metodę GetEnumerator() zdefiniowaną w interfejsie IDictionary. Interfejs ten jest bardziej specjalizowanym enumaratorem, ponieważ rozszerza on funkcjonalność interfejsu IEnumeartor o dodatkowe właściwości.
Rola interfejsu IList Interfejs IList definiuje metody pozwalające na dodawanie, wstawianie, usuwanie i wyszukiwanie elementów kolekcji (listy).
Klasy w System.Collections ArrayList – reprezentuje tablicę obiektów o dynamicznym rozmiarze wspierane interfejsy: IList, ICollection, IEnumerable i IClonable HashTable – reprezentuje kolekcję obiektów identyfikowanych na podstawie numerycznego klucza. Niestandardowe typy przechowywane w HashTable powinny zawsze przesłaniać metodę Object.GetHashCode(). wspierane interfejsy: IDictionary, ICollection, IEnumerable i IClonable
Klasy w System.Collections c. d. SortedList – kolekcja w rodzaju słownika; do jej elementów można również odwoływać się przez indeksy. wspierane interfejsy: IDictionary, ICollection, IEnumerable i IClonable Queue – reprezentuje standardową kolejkę FIFO (ang. First In, First Out). wspierane interfejsy: ICollection, IEnumerable i IClonable Stack – reprezentuje stos LIFO (ang. Last In, First Out),
Dodatkowe klasy Dodatkowo, w przestrzeni nazw System.Collections jest zdefiniowanych wiele innych klas rzadziej używanych. Klasy: BitArray, CaseInsensitiveComparer, CaseInsensitiveHashCodeProvider. Zawiera ona również definicję abstrakcyjnych klas bazowych (CollectionBase, ReadOnlyCollectionBase, DictionaryBase), które mogą być wykorzystane podczas implementowania własnych, specjalizowanych kolekcji.
Praca z typem ArrayList Definiujemy prosty typ Car
Praca z typem ArrayList c. d.
Praca z typem ArrayList c. d. 2
Praca z typem Queue Typ Queue jest kontenerem, który zapewnia dostęp do elementów według zasady pierwszy przyszedł– jako pierwszy zostanie obsłużony. Oprócz metod wynikających z implementacji odpowiednich interfejsów, typ ten definiuje trzy główne składowe, którymi są następujące metody: Dequeue() – zwraca i jednocześnie usuwa pierwszy obiekt z kolejki Enqueue() – dodaje obiekt na końcu kolejki, Peek() – zwraca obiekt znajdujący się na początku kolejki, ale go nie usuwa (w ten sposób można podejrzeć, kto jest kolejnym czekającym na obsługę).
Praca z typem Queue c. d.
Praca z typem Queue c. d. 2
Praca z typem Stack Typ Stack jest kontenerem, który zapewnia dostęp do elementów według zasady ostatni przyszedł– jako pierwszy zostanie obsłużony. Oprócz metod wynikających z implementacji odpowiednich interfejsów, typ ten definiuje trzy główne składowe, którymi są następujące metody: Pop() – zwraca i jednocześnie usuwa obiekt znajdujący się na szczycie stosu, Push() – wrzuca obiekt na stos; dany obiekt staje się szczytem stosu. Peek() – zwraca obiekt znajdujący się na szczycie stosu, ale go nie usuwa (w ten sposób można podejrzeć, kto jest kolejnym czekającym na obsługę).
Praca z typem Stack c. d.
System.Collections.Specialized Oprócz typów zdefiniowanych w przestrzeni nazw System.Collections, biblioteka klas bazowych platformy .NET udostępnia również przestrzeń nazw System.Collections.Specialized zdefiniowaną w pakiecie System.dll. W przestrzeni tej znajdują się kolejny zbiór definicji typów reprezentujących bardziej wyspecjalizowane kolekcje.
Klasy w System.Collections.Specialized BitVector32 – prosta struktura przechowująca wartości logiczne i niewielkie liczby całkowite na 32 bitach pamięci. CollectionsUtil – tworzy kolekcje ignorujące wielkość liter w łańcuchach znaków. HybridDictionary – implementuje interfejs IDictionary za pomocą typu ListDictionary, gdy kolekcja jest mała. Przełącza się na stosowanie Hashtable, gdy kolekcja staje się większa.
Klasy w System.Collections.Specialized c. d. ListDictionary – implementuje interfejs IDictionary za pomocą listy pojedynczo powiązanej. Zalecana dla kolekcji, które posiadają poniżej 10 elementów. NameValueCollection – reprezentuje posortowaną kolekcję powiązanych kluczy i wartości typu String. Dostęp do takiej kolekcji możemy uzyskać za pomocą wartości klucza lub indeksu.
Klasy w System.Collections.Specialized c. d. 2 StringCollection – reprezentuje kolekcję obiektów typu String. StringDictionary – implementuje haszową tablicę, w której klucz nie jest ogólnego typu Object, ale konkretnego typu String. StringEnumerator – stanowi prosty sposób iterowania po elementach obiektu typu StringCollection.
Zamienię Cię na lepszy model Znamy już typy zdefiniowane w przestrzeniach nazw System.Collections i System.Collections.Specialized. Typy te uważa się obecnie za przestarzałe i w gruncie rzeczy nie powinny być wykorzystywane podczas tworzenia nowych projektów. Nie są one niebezpieczne i nie powodują błędów, ale ich stosowanie prowadzi do mniejszej wydajności i problemów z bezpieczeństwem typów. Ich odpowiednie nowe wersje zostały zdefiniowane w przestrzeni nazw System.Collections.Generic.
Typy i kolekcje generyczne
Opakowywanie i wypakowywanie Platforma .NET obsługuje dwie kategorie typów danych: Typy oparte na wartościach (ang. value types), Typy referencyjne (ang. reference types). Czasami może wystąpić koniczność reprezentacji zmiennej jednej kategorii jako zmienna drugiej kategorii. Do tego służą nam mechanizmy nazwane opakowywaniem (ang. boxing) i wypakowywanie (ang. unboxing).
Opakowywanie Opakowywanie można formalnie zdefiniować, jako jawną konwersję typu wartościowego na odpowiadający mu typ referencyjny poprzez zapisanie zmiennej w typie System.Object. Kiedy opakowujemy wartość, CLR alokuje nowy obiekt na stercie i kopiuje wartość typu wartościowego ( w tym przypadku 25) do tej instancji. Zwracana jest referencja do tego nowo zaalokowanego obiektu.
Wypakowywanie Operacją odwrotną do opakowywania jest wypakowywanie. Wypakowywanie to proces konwersji wartości przechowywanej w referencji do obiektu odpowiedniego typu wartościowego. Wypakowywanie rozpoczyna się od weryfikacji, czy „odbierający” typ danych jest równoważny z opakowanym typem.
Wypakowywanie c. d. Wypakowywanie do nieodpowiedniego typu danych powoduje wyrzucenie wyjątku InvalidCastException.
Zastosowanie (o/wy)pakowywania Mechanizm ten, mimo że wydawać się może rzadko stosowany, jest bardzo pomocny. Pozwala on na przyjęcie założenia, że wszystko może być traktowane jako obiekty klasy System.Object, podczas gdy CLR zajmuje się wszystkimi operacjami związanymi z zarządzaniem pamięcią. Jest on wykorzystywany, np., w operacjach na poznanych kolekcjach, w tym ArrayList.
Zastosowanie (o/wy)pakowywania c. d. Aby zobaczyć ten mechanizm w działaniu, załóżmy, że chcemy stworzyć obiekt typu ArrayList do przechowywania wartości numerycznych (typ wartościowy). Po przeanalizowaniu klasy ArrayList możemy zauważyć, że jej metody przyjmują jako wartości i zwracają typ System.Object.
Zastosowanie (o/wy)pakowywania c. d. 2 Jednakże, zamiast wymagać od programisty stosowania odpowiednich typów referencyjnych, CLR automatycznie zamienia typy wartościowe na referencyjne za pomocą mechanizmu opakowywania.
Zastosowanie (o/wy)pakowywania c. d. 3 Aby wydobyć dany element za pomocą indeksera, musimy go odpowiednio wypakować.
Problemy z (o/wy)pakowywaniiem Pomimo, że przedstawiony mechanizm jest bardzo wygodny z punktu widzenia programisty, ten sposób operowania na pamięci pociąga za sobą spadek efektywności (w szybkości wykonania i wielkości kodu) oraz brak kontroli typów. Na efektywność wpływa ilość kroków, które muszą zostać wykonane: Nowy obiekt musi być zaalokowany na zarządzanej stercie, Wartość ze stosu musi być przeniesiona do nowego miejsca na stercie, Podczas wypakowywania, wartość przechowywana na stercie musi zostać z powrotem przeniesiona na stos, Nieużywany obiekt na stercie musi zostać usunięty.
Problemy z (o/wy)pakowywaniiem c. d. Brak kontroli typów w przypadku wypakowywania powoduje to, że o ewentualnym błędnym rzutowaniu dowiadujemy się dopiero w czasie wykonania. Nielegalne wypakowywanie powinno być wykrywane w czasie kompilacji a nie wykonania. Idealnym rozwiązaniem byłaby możliwość przechowywania typów wartościowych w kolekcji bez konieczności opakowywania i wypakowywania. Rozwiązaniem tych problemów jest zastosowanie typów i kolekcji generycznych.
Kolekcje ze ścisłą kontrolą typów Większość typów z System.Collections może przechowywać wszystko, ponieważ ich metody opierają się na obiektach typu System.Object. W niektórych przypadkach może to się okazać użyteczne, ale w większości zastosowań lepszym rozwiązaniem byłyby kolekcje ze ścisła kontrolą typów.
Tworzenie niestandardowych kolekcji Przed wprowadzeniem typów generycznych, programiści tworzyli własne kolekcje realizujące ścisła kontrole typów. Podejście takie pociągało za sobą kilka kłopotliwych kwestii.
Tworzenie niestandardowych kolekcji c .d.
Tworzenie niestandardowych kolekcji c .d. 2 Niestandardowe kolekcje używają wewnętrznej kolekcji z biblioteki klas bazowych. Dodatkowo zapewniają metody dostępowe realizowane jedynie dla określonego typu.
Tworzenie niestandardowych kolekcji c .d. 3
Kolekcje generyczne ruszają na ratunek! W przypadku, gdy chcemy stworzyć niestandardową kolekcję dla innego typu, musimy implementować wszystkie metody od nowa. Kolekcje generyczne pozwalają na przełożenie specyfikacji typu elementów, aż do samego momentu stworzenia obiektu.
A co z mechanizmem (o/wy)pakowywania? Niestandardowe kolekcje (poprzez wewnętrzne wykorzystywanie kolekcji bazowych) realizują opakowywanie i wypakowywanie (z tą różnicą, że jest ono niewidoczne dla użytkownika).
Kolekcje generyczne znów ruszają na ratunek! Wykorzystanie generycznej kolekcji List do przechowywania liczb, całkowicie eliminuje występowanie mechanizmu opakowywania i wypakowywania podczas dodawania i pobierania elementów kolekcji.
Zalety kolekcji generycznych Podsumowując, kolekcje generyczne mają następujące zalety w porównaniu ze standardowymi: Kolekcje generyczne szybciej działają, ponieważ nie korzystają z mechanizmu opakowywania i wypakowywania, Posiadają ścisłą kontrolę typów; mogą operować tylko na wskazanym typie, W ogromnym stopniu zmniejszają potrzebę tworzenia niestandardowych kolekcji; biblioteka klas bazowych dostarcza kilka predefiniowanych typów kolekcji generycznych.
Przestrzeń nazw System.Collections.Generic Interfejsy zdefiniowane w przestrzeni nazw System.Collections.Generic odzwierciedlają te znane z System.Collections. Są to: ICollection<T>, IComparer<T>, IDictionary<TKey, TValue>, IEnumerable<T>, IEnumerator<T>, IList<T>.
Klasy w System.Collections.Generic Collection<T> (odpowiednik CollectionBase) – stanowi bazę dla kolekcji generycznych, Comparer<T> (odpowiednik Comparer) – porównuje dwa obiekty generyczne, Dictionary<TKey, TValue> (odpowiednik Hashtable – generyczna kolekcja par nazwa/wartość, List<T> (odpowiednik ArrayList) – dynamiczna dostosowująca swój rozmiar lista elementów, LinkedList<T> - generyczna implementacja podwójnie powiązanej listy,
Klasy w System.Collections.Generic c. d. Queue<T> (odpowiednik Queue) – generyczna implementacja kolejki FIFO, Stack<T> (odpowiednik Stack) – generyczna implementacja kolejki LIFO, SortedDictionary<Tkey, TValue> (odpowiednik SortedList) – dynamiczna implementacja posortowanego zbioru par nazwa/wartość, ReadOnlyCollection<T> (odpowiednik ReadOnlyCollectionBase) – generyczna implementacja zbioru elementów tylko do odczytu.
Typ List<T> pod lupą Podobnie, jak w typach niegenerycznych, obiekty typów generycznych tworzone są poprzez new i podanie odpowiednich parametrów konstruktora. Dodatkowo, specyfikujemy typ w miejsce parametru typu. W przypadku List, typ ten specyfikuje typ przechowywanych elementów.
Fragment definicji typu List<T>
Parametr typu jest odpowiednio zastępowany
Sposób zastępowania Kiedy stworzymy obiekt typu List<T>, kompilator nie tworzy jego nowej implementacji. Czyni to jedynie w przypadku składowych generycznego typu, które używamy w programie.
Tworzenie niestandardowych metod, typów i kolekcji genrycznych
Generyczne metody Większość programistów na pewno będzie korzystać ze standardowych typów i składowych genrycznych. Mogą oni również tworzyć własne typy i składowe generyczne. Przykładowo, zaimplementujemy metodę, która będzie pozwalała na zamianę dwóch zmiennych dowolnego typu.
Generyczne metody c. d. Naszą metodę możemy wywoływać w następujący sposób:
Generyczne metody c. d. 2 Jeżeli wywołujemy generyczną metodę posiadającą argumenty typu generycznego, możemy pominąć podanie parametru typu po nazwie metody. W takim przypadku, kompilator może wywnioskować nazwę typu genrycznego na podstawie odpowiednich argumentów.
Generyczne metody c. d. 3 W przypadku definiowania metod generycznych nie posiadających argumentów, podanie wartości parametru typu jest obowiązkowe.
Definiowanie generycznej klasy
Definiowanie generycznej klasy c . d.
Użycie zdefiniowanej klasy
Niestandardowe kolekcje generyczne
Niestandardowe kolekcje generyczne c. d. IEnumerable<T> rozszerza IEnumerable, dlatego musimy zaimplementować dwie wersje metody GetEnumerator(). Przykładowe użycie zdefiniowanej kolekcji generycznej
Niestandardowe kolekcje generyczne c. d. 2 Do kolekcji typów Car, możemy dodawać elementy wszystkich typów potomnych względem podanego w parametrze typu.
Problemy z niestandardowymi klasami Tak zdefiniowaną klasę możemy użyć do przechowywania dowolnego typu, który wskazujemy poprzez parametr typu.
Problemy z niestandardowymi klasami c. d. Poniższego kodu nie uda się skompilować, ponieważ nie jest znana prawdziwa tożsamość typu T. Tym bardziej nie wiemy, czy posiada on taką właściwość. Poniższego kodu również nie uda się skompilować. W tym momencie nie wiadomo, czy dany typ może być na daną klasę rzutowany.
Definiowanie ograniczeń dla parametru typu Głównym powodem definiowania niestandardowych kolekcji generyczneych jest możliwość wprowadzenia ograniczeń na parametr typu, tworząc w ten sposób bardzo ścisłą kontrolę typów. W C# służy nam do tego słowo kluczowe where. where T : struct – typ T musi mieć klasę System.ValueType jako klasę nadrzędną, where T : class – typ T nie może mieć klasy System.ValueType jako klasy nadrzędnej (czyli musi być typem referencyjnym)
Definiowanie ograniczeń dla parametru typu c. d. where T : new()– typ T musi posiadać domyślny konstruktor. Przydatne, jeżeli chemy stworzyć obiekt danego typu w naszej klasie. Ograniczenie to jest zawsze definiowane jako ostatnie. where T : nazwa klasy bazowej– typ T musi być potomkiem danej klasy where T : nazwa interfejsu – typ T musi implementować wskazany interfejs. Możemy podać kilka interfejsów po przecinkach.
Przykładowe ograniczenia
Zastosowanie ograniczeń Nie tylko kompilator nie pozwoli stworzyć kolekcji niezgodnej ze zdefiniowanymi ograniczeniami, ale również możliwe będzie wykonywanie operacji zgodnych z klasami bazowymi, interfejsami, itd., wyspecyfikowanymi w ograniczeniach.
Generyczne klasy bazowe Jeżeli niegeneryczna klasa dziedziczy po generycznej klasie, musi ona podać wartość parametru typu w swojej definicji. Jeżeli klasa bazowa definiuje składowe wirtualne lub abstrakcyjne, klasa potomna musi je nadpisać podając odpowiedni typ (zadeklarowany podczas dziedziczenia). Jeżeli klasa potomna jest również generyczna, może ona korzystać z parametru typu klasy bazowej, ale musi się stosować do wszystkich ograniczeń na niego narzuconych
Generyczne interfejsy Implementując generyczny interfejs podajemy wartość parametru typu.
Bonus round! Czym się różni string od String? Nazwy typów z małych liter są aliasami do pełnych kwalifikowanych nazw odpowiednich typów. string System.String object System.Object int System.Int32
Pytania egzaminacyjne (Zestaw07) Co to są typy generyczne? Na czym polega o/wypakowywanie i gdzie jest wykorzystywane? Wymień znane Ci kolekcje generyczne. Co to są indeksery? Na jakiej zasadzie działają metody generyczne?
Dziękuję za uwagę