BETA
Aby się zalogować, najpiew wybierz portal.
Aby się zarejestrować, najpiew wybierz portal.
Podaj słowa kluczowe
Słowa kluczowe muszą mieć co najmniej 3 sąsiadujące znaki alfanumeryczne
Pole zawiera niedozwolone znaki

Baza wiedzy











Wprowadzenie do typów generycznych

thumb
W tym artykule dowiesz się, czym są typy generyczne, dlaczego zostały stworzone i jak je stosować. Na końcu dowiesz się jak definiować własne generyczne kolekcje oraz interfejsy. W artykule nie chcę kłaść nacisku na suchą teorię, ponieważ uważam, że to, co uczy najbardziej to przykłady, dlatego też postaram się, aby tej teorii było jak najmniej.

Powinieneś znać:

- Filozofię działania platformy .NET,

- Podstawy języka C#

 

Dowiesz się:

- Jak programiści byli zmuszeni tworzyć kolekcję danych przed wydaniem platformy .NET 2.0,

- Czym są typy generyczne oraz jak zastosować wbudowane w platformę przykładowe kolekcje,

- Jak definiować własne generyczne typy danych (klasy, struktury, interfejsy)

 

Przed wydaniem platformy .NET 2.0 istniała przestrzeń nazw System.Collections, w której zawarte były różne typy działające na kolekcjach danych np. ArrayList. Kolekcja typu ArrayList jest kolekcją dynamiczną tzn. można do niej władować mnóstwo danych a ona w zależności od tego ile w nią włożymy rozszerzy się lub zmniejszy (jeśli dane usuniemy). W oparciu o kolekcje z przestrzeni nazw System.Collections powstało mnóstwo naprawdę dobrych programów jednak problem leżał tutaj gdzie indziej.

Otóż wszystkie te kolekcje działały na typie System.Object a jak wiemy Object to ostateczna klasa w C#, z której dziedziczy wszystko.

O co konkretnie chodzi? Tak wygląda deklaracja niektórych składowych typu ArrayList:

Jak widzimy metody te przyjmują, jako parametr typ Object. Przez takie właśnie działanie nasze dane, które do nich przesyłamy muszą przejść tzw. proces opakowania (ang. Boxing). Załóżmy teraz, że pracujemy na kolekcji ArrayList, gdzie dodajemy elementy typu int: tak jak wspomniałem wcześniej kiedy wkładamy je do środka naszej kolekcji ArrayList opakowane są one w typ Object jednak jeśli teraz napiszemy taką instrukcję:

To otrzymamy błąd kompilacji. Musimy jawnie wykonać proces wypakowania (ang. Unboxing) do zmiennej typu int (skoro zapakowaliśmy int):

Przez takie operacje pakowania i wypakowywania nasz program traci na wydajności z powodu całej drogi operacji, które musi przejść środowisko CLR aby wykonać dla nas nasze życzenie.

Drugi problem to bezpieczeństwo typów. Zauważyłeś już pewnie, że skoro metoda Add() w naszej kolekcji ArrayList przyjmuje, jako parametr typ Object to w zasadzie możemy tam wpakować wszystko. Ilustruje to poniższy przykład:

To są właśnie powody, dla których powstały typy generyczne zapewniające pełne bezpieczeństwo typów oraz poprawienie wydajności działania programów jak się zresztą za chwilę przekonamy.

Problem bezpieczeństwa typów można jednak rozwiązać nie używając generyczności. Pokażę teraz jak to zrobić. Otwórzmy, więc sobie Visual Studio i stwórzmy nowy projekt aplikacji konsolowej. Dla przykładu nazwijmy go: NoGenericColletction

Kiedy mamy już otwarty nasz projekt stworzymy sobie najprostszą klasę i nazwiemy ją „Car”:

W klasie nie ma nic skomplikowanego, więc myślę, że tłumaczenie, co ona robi możemy sobie darować.

Dobra, teraz pora utworzyć nasz niegeneryczny typ kolekcji, który będzie przechowywał i operował jedynie na typie „Car”. W tym celu klikamy w menu PROJECT a następnie Add Class…

Dla naszego przykładu nazwijmy ją: NoGenericCarCollection. Kiedy już to zrobimy pora napisać kod naszej niestandardowej kolekcji:

Jak widzimy nasza klasa implementuje niegeneryczny interfejs IEnumerable, który udostępnia nam metodę GetEnumerator(), dzięki której możemy na naszym obiekcie typu NoGenericCarCollection zastosować później pętlę foreach.

Zwróćmy uwagę na metodę DisplayCar() – zwraca ona element naszej tablicy Cars, jednak jak widać jawnie rzutujemy go na typ Car a to, dlatego, że tablica ta działa przecież na typie System.Object! A to oznacza, że pomimo faktu, iż zapewniliśmy w naszej kolekcji bezpieczeństwo typów to nie uniknęliśmy operacji pakowania i rozpakowania, dzięki czemu nasza aplikacja ciągle pozostaje mniej wydajna i środowisko CLR ciągle musi przeprowadzać wszystkie transfery w pamięci.

 

Dygresja:

Może się jeszcze zastanawiasz, po co to rzutowanie. Robimy to z tego samego powodu, co we wcześniejszych przykładach. Metoda AddCar() wkłada do naszej kolekcji ArrayList obiekt typu Car – a jak wiemy tablica ta pracuje na obiektach typu System.Object i dlatego, żeby konwencji „taki typ, jaki włożysz taki musisz wyciągnąć” stało się zadość konieczne jest to rzutowanie.

W ramach ćwiczeń możesz usunąć to rzutowanie i sprawdzić, jaki błąd wyrzuci Ci kompilator.

 

Pora przejść teraz do naszej metody Main() i wykorzystać to, co napisaliśmy:

 

Dla ciekawskich geeków, którzy bardzo lubią wiedzieć jak coś działa w środku, bo ładna i przyjemna składnia ich nie zadowala powstał program ildasm.exe można go uruchomić z wiersza poleceń Visual Studio. 

Z menu file możemy wybrać projekt, który nas interesuje i podejrzeć kod CIL. Przyjrzyjmy się, więc nowej metodzie o nazwie SimpleMethod(), która prezentuje nam operację pakowania i rozpakowania:

W kodzie CIL wygląda ona następująco:

Czytelnie prawda? Pozwoliłem sobie wyróżnić interesujące nas fragmenty pakowania i odpakowania.

 

Najprościej – typy generyczne są wynalazkiem, który został wbudowany już w platformę .NET 2.0 i został pozytywnie przyjęty przez programistów tejże technologii.

Typy generyczne rozwiązują wszystkie przedstawione powyżej problemy: niebezpieczne typowanie oraz problemy z wydajnością.

Klasa ArrayList, którą przedstawiłem wcześniej została zmodyfikowana do List<T> i podobnie jak inne generyczne kolekcje znajduje się w zupełnie nowej przestrzeni nazw: System.Collections.Generic

 

 

- Zapewniają większość wydajność, ponieważ nie wymagają przeprowadzania operacji pakowania i rozpakowywania,

- Zapewniają bezpieczeństwo typów, ponieważ mogą zawierać jedynie określony przez programistę typ,

- Zmniejszają potrzebę tworzenia niestandardowych typów kolekcji, ponieważ w bibliotekach klas bazowych znajduje się mnóstwo predefiniowanych kontenerów

 

Od czasu wprowadzenia na platformy .NET typów generycznych ich stosowanie stało się zalecane, dlatego przestrzeń nazw System.Collections należy ignorować a stosować jedynie System.Collections.Generic.

Przestrzeń nazw System.Collections została na platformie .NET z powodu zachowania kompatybilności wstecznej, ponieważ jak już wspomniałem wiele programów powstało przy użyciu kolekcji w niej zdefiniowanych.

 

 

 

Załóżmy, że mamy napisać metodę, która dokonuje zamiany liczb całkowitych

A - > B

B - > A

Taka zamiana jest bardzo prosta i może przydać się np. podczas obliczania NWD przy pomocy Algorytmu Euklidesa.

Tak, więc zaimplementujmy ją:

To zadziała. Ale wyobraźmy sobie teraz, że w trakcie pisania naszego programu przyjdzie potrzeba napisania metody do zamiany stringów… no dobra, zaimplementujmy ją korzystając po prostu z przeciążonej metody swap:

I znowu piszemy program i ponownie zachodzi potrzeba zamiany zmiennych innego typu… i co? Napiszemy kolejną przeciążoną metodę? No niezbyt mądry pomysł, bo takim sposobem myślenia możemy narobić sobie dużo pracy a zysku z tego będzie niewiele.

Nie znając generyczności też możemy wpaść na prawidłowy pomysł rozwiązania tego problemu i zaimplementować naszą metodę swap tak:

No i to już jest w pewnym sensie rozwiązanie naszego problemu, ponieważ ta metoda zadziała na praktycznie każdym typie danych.  Ale ta metoda jest problematyczna, bo nie rozwiązuje problemów z pakowaniem, odpakowaniem i spadkiem wydajności. 

Tutaj przychodzą nam z pomocą typy generyczne i prawidłowa implementacja tej metody powinna wyglądać tak:

Teraz możemy wywołać ją z poziomu Main() z przeróżnymi typami danych nie martwiąc się o bezpieczeństwo typów jak i wydajność.

Weźmy na początku pod lufę naszą generyczną metodę Swap<T>, jak widzimy jej pierwszy człon jest jak najbardziej nam znany, nowości natomiast zaczynają się po nazwie metody – widzimy tam parę ostrych nawiasów a między nimi literkę „T” – oznacza ona Typ, na jakim metoda ma wykonywać swoje operacje. Nie jest powiedziane, że to koniecznie musi być „T” jednak tak się już wśród programistów przyjęło.

 

Następnie widzimy parę standardowych nawiasów i bardzo podobnie jak w zwykłych metodach parametry z tą różnicą, że zamiast prawidłowego typu danych np. int musimy podać tam taki, jaki wsadziliśmy w parę ostrych nawiasów – w naszym przypadku jest to T.

 

Oficjalnie T nazywamy parametrem typu można też jednak używać łatwiejszej nazwy, czyli po prostu wypełniacz.

 

Jak wspomniałem wcześniej – nie ma znaczenia jak nazwiemy nasz wypełniasz jednak z przyzwyczajenia programiści nazywają go według następującej konwencji:

 

T – typ

K – klucz

V – wartość

 

Kiedy tworzymy generyczny obiekt lub implementujemy generyczny interfejs to musimy dostarczyć właściwą wartość dla naszego parametru typu.

 

Zajmiemy się teraz omówieniem i zastosowaniem kilku ciekawych kolekcji generycznych. Będą to: List<T>, Stack<T> oraz SortedSet<T>. Później stworzymy kilka typów, które będą implementowały również predefiniowane generyczne interfejsy.

Pierwsze, co powinniśmy zrobić to utworzyć nowy projekt w Visual Studio – nazwijmy go GenericTypes i niech jest to projekt aplikacji konsolowej.

Pierwszą generyczną kolekcją, którą się zajmiemy jest List<T>. Filozofia jej działania polega mniej więcej na tym samym, co omawiany przeze mnie wcześniej ArrayList – można nawet powiedzieć, że jest to jej generyczny odpowiednik.

Wybrane metody generycznej kolekcji List<T>:

Stwórzmy sobie teraz na potrzeby ćwiczeń przykładową listę zawierającą imiona:

Jak widać, aby dodać element do listy wykorzystujemy metodę Add(). Myślę, że nie ma w tym nic skomplikowanego.

Możemy też dodać element w określonym miejscu listy. Załóżmy, że chcemy go dodać zaraz po imieniu „Janek” –

names.Insert(2, "Lolek");

Przed wyświetleniem zawartości listy na ekran wypadałoby posortować ją, zróbmy to więc korzystając z metody Sort() a następnie wypiszmy ją na ekran korzystając przy okazji z funkcji Count(), która zwraca ilość elementów w tej liście:

Bezpieczeństwo typów

W ramach zadania domowego spróbuj do powyższej listy dodać element typu float.

Tym przepięknym akcentem wypisana listy na ekran zakończymy jej temat. Mógłbym się, co prawda rozpisywać o jej wszystkich metodach, ale moim zamiarem nie jest i nie było napisanie książki o typach generycznych a jedynie artykułu.

  

Kolekcja Stack<T> reprezentuje stos. Można się więc domyślić, że będzie ona miała metody push() oraz pop().

Stos reprezentuje bufor typu LIFO, czyli ostatni na wejściu, pierwszy na wyjściu. Można to rozumieć w taki sposób, że element, który do stosu włożymy, jako pierwszy to otrzymamy go, jako ostatni. Najlepiej wyobrazić to sobie na przykładzie książek – książka, którą jako pierwszą położymy na półce zostanie zaraz przykryta innymi książkami i tym samym, żeby nie zwalić ich sobie na głowę brutalnie wyciągając tą pierwszą, którą włożyliśmy musimy wyciągnąć nową pierwszą, czyli tą, którą włożyliśmy, jako ostatnią.

Innymi słowy: pierwszy element (książka na samym dole) staje się ostatnim a ostatni (książka na samej górze) pierwszym.

Przełóżmy ten zawiły przykład na język programowania. Przedtem jednak w naszym projekcie utwórzmy sobie klasę „Book”, która reprezentować będzie książkę. Niech posiada ona właściwości: „BookName” oraz „AuthorName”.

 

Przydatne metody Stack<T>

Myślę, że powyższego kodu nie trzeba tłumaczyć. Zaczynamy od zadeklarowania stosu, przyjmującego, jako parametr typ Book, następnie wkładamy do niego kilka książek, określamy ile ich się tam znajduje, wypisujemy na ekran, usuwamy jedną książkę i zaktualizowaną, zawartość naszej biblioteki ponownie wypisujemy na ekran.

Klasa Stack<T> zawiera mnóstwo metod i zachęcam oczywiście do poeksperymentowania z nimi.

Kolejną klasą, której omawianiem się zajmiemy jest SortedSet<T>. Umożliwia nam ona automatyczne sortowanie elementów wstawionych do zbioru. Klasę należy poinformować, z jaką dokładnością chcemy, aby nasze elementy były sortowane.

Do naszego projektu GenericTypes dodamy sobie teraz zupełnie nową klasę: SortedCarByMaxSpeed. Klasa ta będzie implementowała interfejs IComparer, który udostępnia metodę Compare(), w której można zdefiniować o jakie porównanie nam chodzi. 

Jak widzimy, nasza klasa używa już generycznego interfejsu IComparer<T>, dzięki czemu nie musimy już nigdzie tworzyć obiektu typu Car w naszej klasie i sprawdzać, czy obiekt, na którym pracuje klasa faktycznie jest typu Car.

Oto implementacja naszej kolekcji:

Jak widzimy, jako parametr klasa przyjmuje obiekt typu Car a następnie informowana jest, z jaką dokładnością mają być sortowane obiekty się w niej znajdujące dzięki przesłaniu do niej instancji klasy SortedCarByMaxSpeed.

  

To już wszystko w tym artykule, jeśli chodzi o zastosowanie wbudowanych typów generycznych. Warto jednak poświęcić trochę czasu i przejrzeć dokumentację języka C# gdzie znajduje się wiele innych ciekawych przykładów zastosowania powyższych jak i zupełnie innych kolekcji.

Pora teraz przejść do tworzenia własnych, niestandardowych typów generycznych.

 

Wcześniej już zapoznaliśmy się jak tworzyć generyczne metody. W ramach przypomnienia stworzymy sobie teraz bardzo klasę, która taką generyczną metodę będzie posiadała. Dla przykładu, niech to ciągle będzie metoda Swap.

Wywołanie metody:

Teraz pora na stworzenie pełnej generycznej klasy. Dodajmy, więc do projektu nową klasę i nazwijmy ją GenericPoint.

Implementację takiej klasy zaczynamy w ten sposób:

Czyli tak samo jak w przypadku metod i tutaj po nazwie musimy dodać parę ostrych nawiasów i generyczny parametr reprezentujący typ. Zaraz po tym tak samo jak w standardowych klasach możemy dodać klasy bazowe oraz interfejsy, które klasa ma dziedziczyć. O dziedziczeniu generycznych klas bazowych porozmawiamy za chwilę. Implementacja struktur wygląda tak samo, z tym wyjątkiem, że nie można korzystać z dziedziczenia.

Jak widzimy, klasa różni się od standardowej jedynie tym, że zamiast operować na zwyczajnych typach wartościowych/referencynych – operuje na typach T, dzięki czemu nasz punkt mogą opisywać nawet łańcuchy znakowe.

Pewnie zastanawiasz się czemu służy słowo default(T). Ustawia ono domyślną wartość dla parametru. W przypadku typów wartościowych jest to 0 a null w przypadku typów referencyjnych.

Wcześniej w przykładzie widzieliśmy implementowany w klasie interfejs ICompare<T>. Przyjrzyjmy się teraz bliżej tworzeniu generycznych interfejsów.

Myślę, że już wszyscy się przyzwyczaili do takiego zapisu, ponieważ jest on identyczny w metodach jak i klasach tak więc chyba nie trzeba tego tłumaczyć.

Klasa, która implementuje ten interfejs po wybraniu „automatycznej implementacji” w Visual Studio „T” w interfejsie zamieni się na „Book”, kiedy klasa zostanie zdefiniowana w taki oto sposób:

Kiedy teraz w metodzie Main() wywołamy naszą składową w taki sposób:

gpi.SampleMethod(new Book { BookName = "Punkty na płaszczyźnie kartezjańskiej", AuthorName = "Jonasz Kowalski" });

Na ekranie wyświetli się nam tytuł fikcyjnego dzieła i autora publikacji o punktach na płaszczyźnie kartezjańskiej.

Teraz, kiedy już wiemy jak tworzyć niestandardowe klasy i interfejsy generyczne pora przyjrzeć się bliżej łańcuchowi dziedziczenia, kiedy na szczycie tej całej piramidy jest jakaś klasa generyczna. Następnie dowiemy się, w jaki sposób nałożyć na nasz typ większe ograniczenia, co stwarza możliwość do jeszcze większego bezpieczeństwa typów.

1) Kiedy z generycznej klasy wywodzi się niegeneryczna klasa to musi ona określać parametr typu

2) Jeśli w generycznej klasie bazowe stworzono generyczne metody wirtualne lub abstrakcyjne, w klasie, która po niej dziedziczy należy przysłonić te metody z określeniem parametru typu.

3) Jeśli dziedzicząca klasa również jest generyczna to w swej definicji może pominąć parametr typu klasy bazowej, jednak musi zachowywać również wszystkie ograniczenia klasy – matki. Więcej o ograniczeniach porozmawiamy za chwilę.

 

Słowo kluczowe, które umożliwia nam tworzenie ograniczeń to where. Składnia takiego ograniczenia jest stosunkowo prosta:

class NazwaKlasy<Typ> where Typ : ograniczenie

Zobaczmy to na przykładzie:

Klasa ta, co prawda nie robi nic pożytecznego, bo ma za zadanie jedynie pokazać zastosowanie ograniczeń typu.

Powinieneś już teraz wiedzieć jak i kiedy stosować typy generyczne a także znać zalety ich stosowania. Mam też nadzieję, że wyrobiłeś sobie świadomość dlaczego warto stosować generyczne kolekcje zamiast tych zwykłych – obecnych od platformy .NET w wersji 1.0. Na koniec chciałbym Cię zachęcić do zgłębienia tej tematyki bo to, co ja przedstawiłem to jedynie podstawy podstaw a jest ona naprawdę bardzo ciekawa. Wszystkie potrzebne Ci informacje znajdziesz na msdn.microsoft.com a jeżeli masz jakiś problem to zawsze możesz pisać na forum społeczności GeekClub J.

Serdecznie pozdrawiam i życzę powodzenia w zgłębianiu tajników typów generycznych.

 



tagi: C#

Komentarze 8

Przemysław Marcinkiewicz
Przemysław Marcinkiewicz
151 pkt.
Junior
08-03-2013
oceń pozytywnie 3
Ciekawy artykuł :)
Użytkownik usunięty VIP
Użytkownik usunięty
40 pkt.
Poczatkujacy
Właściciel projektów
Uczestnik projektów
Uczestnik projektów
Właściciel projektów
Uczestnik projektów
Uczestnik projektów
Uczestnik projektów
09-03-2013
oceń pozytywnie 0
Dzięki :)
Paulkk
Paulkk
1 pkt.
Nowicjusz
10-03-2013
oceń pozytywnie 0
Fajny artykuł , w sumie pierwszy który przeczytałem na stronie :D Jedna propozycja. Wyłączenie czerwonych podkreśleń w kodzie ułatwiłoby jego estetyke i czytelność
Użytkownik usunięty VIP
Użytkownik usunięty
40 pkt.
Poczatkujacy
Właściciel projektów
Uczestnik projektów
Uczestnik projektów
Właściciel projektów
Uczestnik projektów
Uczestnik projektów
Uczestnik projektów
10-03-2013
oceń pozytywnie 0
Wiem, te podkreślenia są spowodowane tym, że kod był przekopiowany do worda. Jak zapisałem to w .pdf (można pobrać: http://sdrv.ms/Y44VMU) to podkreśleń nie było. Każdy z kodów to po prostu zdjęcie wycięte z Word'a (niektóre z Visuala tam nie ma podkreśleń) a zauważyłem to tak naprawdę dopiero po publikacji tutaj. Przy następnych artykułach będę bardziej uważał. ;)
Smsik
Smsik
6 pkt.
Nowicjusz
16-03-2013
oceń pozytywnie 0
W przypadku klasy NonGenericCarCollection i metody DisplayCar() napisaleś że zachodzi tam proces boxing'u i unboxing'u. Nie zgodzę się z tym, ponieważ w wyżej wymienionej metodzie zachodzi proces jawnego rzutowania. Nie alokujemy miejsca na stercie dla typu wartosciowego w tym przypadku.
Użytkownik usunięty VIP
Użytkownik usunięty
40 pkt.
Poczatkujacy
Właściciel projektów
Uczestnik projektów
Uczestnik projektów
Właściciel projektów
Uczestnik projektów
Uczestnik projektów
Uczestnik projektów
16-03-2013
oceń pozytywnie 0
Zachodzi tam rozpakowanie dla procedury wywołującej. Chyba, że się pomyliłem - zdarza się.
Krzysztof Szklarzewicz
Krzysztof Szklarzewicz
2385 pkt.
Guru
12-03-2014
oceń pozytywnie 0
Dobrze że to napisałeś - rozjaśniło mi trochę sprawę. Podoba mi się także luźny styl! :)
lubieklockilego
lubieklockilego
0 pkt.
Nowicjusz
20-02-2015
oceń pozytywnie 0
Nie rozumiem tej końcówki : gpi.SampleMethod(new Book { BookName = (..) obiektem czego jest gpi ?
pkt.

Zaloguj się lub Zarejestruj się aby wykonać tę czynność.