Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00669 010964 7494885 na godz. na dobę w sumie
C# 6.0. Kompletny przewodnik dla praktyków. Wydanie V - ebook/pdf
C# 6.0. Kompletny przewodnik dla praktyków. Wydanie V - ebook/pdf
Autor: , Liczba stron: 856
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-2519-7 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> c# - programowanie
Porównaj ceny (książka, ebook (-50%), audiobook).
C# jest prostym, nowoczesnym, bezpiecznym językiem programowania, który powstał na bazie języków C i C++, jednak otrzymał też najlepsze cechy takich języków jak Visual Basic, Object Pascal, Delphi czy Java. Został od podstaw zaprojektowany jako obiektowy. C# stanowi część platformy Microsoft .NET Framework. Ta dojrzała technologia pozwala na efektywne tworzenie kodu bezpiecznego, przejrzystego, wydajnego i prostego w konserwacji.

Niniejsza książka to bardzo praktyczne kompendium wiedzy o języku C#. Została oparta na podstawowej specyfikacji C# Language 6.0. Zawiera kompletne omówienie języka. Książkę pomyślano jako podręcznik, dzięki któremu szybko można rozpocząć praktyczną pracę nad projektami programistycznymi. Osoby znające C# będą mogły zapoznać się ze skomplikowanymi paradygmatami programowania, a także przejrzeć szczegółowe omówienie funkcji wprowadzonych w najnowszej wersji języka, C# 6.0, oraz w platformie .NET Framework 4.6. Ponadto każdy, kto pracuje w C#, znajdzie tu doskonale zorganizowane źródło wiedzy o tym potężnym języku.

W tej książce przedstawiono:

Sprawdź, jak wygodnym i niezawodnym językiem jest C#!


Mark Michaelis jest założycielem firmy IntelliTect. Laureat nagrody Microsoft MVP, dyrektor regionalny Microsoftu. Prowadzi prelekcje na konferencjach dla programistów i jest autorem wielu książek.

Eric Lippert jest programistą w zespole odpowiedzialnym za analizę języka C# w firmie Coverity/Synopsys. Wcześniej był głównym programistą w zespole pracującym nad kompilatorem języka C# w Microsofcie i członkiem zespołu projektującego ten język. Brał też udział w projektowaniu i implementowaniu wielu innych technologii w Microsofcie.
Znajdź podobne książki

Darmowy fragment publikacji:

Tytuł oryginału: Essential C# 6.0 (5th Edition) Tłumaczenie: Tomasz Walczak ISBN: 978-83-283-2518-0 Authorized translation from the English language edition, entitled: ESSENTIAL C# 6.0, Fifth Edition; ISBN 0134141040; by Mark Michaelis; and Eric Lippert; published by Pearson Education, Inc, publishing as Addison-Wesley Professional. Copyright © 2016 Pearson Education, Inc. All rights reserved. No part of this book may by reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc. Polish language edition published by HELION SA. Copyright © 2016. Microsoft, Windows, Visual Basic, Visual C#, and Visual C++ are either registered trademarks or trademarks of Microsoft Corporation in the U.S.A. and/or other countries/regions. Wszelkie prawa zastrzeżone. Nieautoryzowane rozpowszechnianie całości lub fragmentu niniejszej publikacji w jakiejkolwiek postaci jest zabronione. Wykonywanie kopii metodą kserograficzną, fotograficzną, a także kopiowanie książki na nośniku filmowym, magnetycznym lub innym powoduje naruszenie praw autorskich niniejszej publikacji. Wszystkie znaki występujące w tekście są zastrzeżonymi znakami firmowymi bądź towarowymi ich właścicieli. Autor oraz Wydawnictwo HELION dołożyli wszelkich starań, by zawarte w tej książce informacje były kompletne i rzetelne. Nie biorą jednak żadnej odpowiedzialności ani za ich wykorzystanie, ani za związane z tym ewentualne naruszenie praw patentowych lub autorskich. Autor oraz Wydawnictwo HELION nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Wydawnictwo HELION ul. Kościuszki 1c, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: helion@helion.pl WWW: http://helion.pl (księgarnia internetowa, katalog książek) Drogi Czytelniku! Jeżeli chcesz ocenić tę książkę, zajrzyj pod adres http://helion.pl/user/opinie/ch6kpp Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję. Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/ch6kpp.zip Printed in Poland. • Kup książkę • Poleć książkę • Oceń książkę • Księgarnia internetowa • Lubię to! » Nasza społeczność X Spis treści Spis rysunków 11 Spis tabel 13 Przedmowa 15 Wprowadzenie 17 Podziękowania 27 O autorach 29 1. Wprowadzenie do języka C# 31 Witaj, świecie 32 Podstawy składni języka C# 33 Dane wejściowe i wyjściowe w konsoli 44 2. Typy danych 59 Podstawowe typy liczbowe 59 Inne podstawowe typy 67 Wartości null i void 78 Kategorie typów 81 Modyfikator umożliwiający stosowanie wartości null 83 Konwersje typów danych 84 Tablice 89 3. Operatory i przepływ sterowania 105 Operatory 106 Zarządzanie przepływem sterowania 119 Bloki kodu ({}) 124 Bloki kodu, zasięgi i przestrzenie deklaracji 126 Wyrażenia logiczne 127 Operatory bitowe ( , , |, , ^, ~) 135 Instrukcje związane z przepływem sterowania — ciąg dalszy 140 Instrukcje skoku 150 Dyrektywy preprocesora języka C# 156 Kup książkęPoleć książkę 6 Spis treści 4. Metody i parametry 165 Wywoływanie metody 166 Deklarowanie metody 172 Dyrektywa using 176 Zwracane wartości i parametry metody Main() 181 Zaawansowane parametry metod 183 Rekurencja 190 Przeciążanie metod 193 Parametry opcjonalne 195 Podstawowa obsługa błędów z wykorzystaniem wyjątków 199 5. Klasy 213 Deklarowanie klasy i tworzenie jej instancji 216 Pola instancji 218 Metody instancji 221 Stosowanie słowa kluczowego this 222 Modyfikatory dostępu 228 Właściwości 230 Konstruktory 244 Składowe statyczne 253 Metody rozszerzające 262 Hermetyzacja danych 263 Klasy zagnieżdżone 266 Klasy częściowe 268 6. Dziedziczenie 273 Tworzenie klas pochodnych 274 Przesłanianie składowych z klas bazowych 284 Klasy abstrakcyjne 294 Wszystkie klasy są pochodne od System.Object 299 Sprawdzanie typu za pomocą operatora is 301 Konwersja z wykorzystaniem operatora as 301 7. Interfejsy 303 Wprowadzenie do interfejsów 304 Polimorfizm oparty na interfejsach 305 Implementacja interfejsu 309 Przekształcanie między klasą z implementacją i interfejsami 314 Dziedziczenie interfejsów 315 Dziedziczenie po wielu interfejsach 317 Metody rozszerzające i interfejsy 317 Implementowanie wielodziedziczenia za pomocą interfejsów 319 Zarządzanie wersjami 321 Interfejsy a klasy 323 Interfejsy a atrybuty 324 Kup książkęPoleć książkę Spis treści 7 8. Typy bezpośrednie 327 Struktury 331 Opakowywanie 336 Wyliczenia 343 9. Dobrze uformowane typy 355 Przesłanianie składowych z klasy object 355 Przeciążanie operatorów 365 Wskazywanie innych podzespołów 373 Definiowanie przestrzeni nazw 377 Komentarze XML-owe 381 Odzyskiwanie pamięci 385 Porządkowanie zasobów 387 Leniwe inicjowanie 394 10. Obsługa wyjątków 397 Wiele typów wyjątków 397 Przechwytywanie wyjątków 400 Ogólny blok catch 403 Wskazówki związane z obsługą wyjątków 405 Definiowanie niestandardowych wyjątków 407 Ponowne zgłaszanie opakowanego wyjątku 411 11. Typy generyczne 415 Język C# bez typów generycznych 416 Wprowadzenie do typów generycznych 420 Ograniczenia 430 Metody generyczne 442 Kowariancja i kontrawariancja 446 Wewnętrzne mechanizmy typów generycznych 452 12. Delegaty i wyrażenia lambda 457 Wprowadzenie do delegatów 458 Wyrażenia lambda 466 Metody anonimowe 471 Delegaty ogólnego przeznaczenia — System.Func i System.Action 473 13. Zdarzenia 489 Implementacja wzorca „obserwator” za pomocą delegatów typu multicast 490 Zdarzenia 503 14. Interfejsy kolekcji ze standardowymi operatorami kwerend 513 Typy anonimowe i zmienne lokalne o niejawnie określanym typie 514 Inicjatory kolekcji 519 Interfejs IEnumerable T sprawia, że klasa staje się kolekcją 522 Standardowe operatory kwerend 527 Kup książkęPoleć książkę 8 Spis treści 15. Technologia LINQ i wyrażenia z kwerendami 557 Wprowadzenie do wyrażeń z kwerendami 558 Wyrażenia z kwerendą to tylko wywołania metod 573 16. Tworzenie niestandardowych kolekcji 577 Inne interfejsy implementowane w kolekcjach 578 Podstawowe klasy kolekcji 580 Udostępnianie indeksera 594 Zwracanie wartości null lub pustej kolekcji 598 Iteratory 598 17. Refleksja, atrybuty i programowanie dynamiczne 613 Mechanizm refleksji 614 Operator nameof 623 Atrybuty 624 Programowanie z wykorzystaniem obiektów dynamicznych 644 18. Wielowątkowość 655 Podstawy wielowątkowości 657 Używanie klasy System.Threading 663 Zadania asynchroniczne 670 Anulowanie zadania 686 Wzorzec obsługi asynchroniczności za pomocą zadań 692 Równoległe wykonywanie iteracji pętli 713 Równoległe wykonywanie kwerend LINQ 721 19. Synchronizowanie wątków 727 Po co stosować synchronizację? 728 Zegary 752 20. Współdziałanie między platformami i niezabezpieczony kod 755 Mechanizm P/Invoke 756 Wskaźniki i adresy 766 Wykonywanie niezabezpieczonego kodu za pomocą delegata 775 Używanie bibliotek Windows Runtime w języku C# 776 21. Standard CLI 781 Definiowanie standardu CLI 782 Implementacje standardu CLI 783 Kompilacja kodu w języku C# na kod maszynowy 784 Środowisko uruchomieniowe 786 Domeny aplikacji 790 Podzespoły, manifesty i moduły 790 Język Common Intermediate Language 792 Common Type System 793 Common Language Specification 794 Base Class Library 794 Metadane 794 Kup książkęPoleć książkę Spis treści 9 A Pobieranie i instalowanie kompilatora języka C# oraz platformy CLI 799 Platforma .NET dla systemu Windows 799 Platforma .NET w systemach OS X i Linux 801 B Kod źródłowy programu do gry w kółko i krzyżyk 803 C Wielowątkowość bez biblioteki TPL i przed wersją C# 6.0 809 Wzorzec APM 810 Asynchroniczne wywoływanie delegatów 821 Wzorzec EAP — asynchroniczność oparta na zdarzeniach 824 Wzorzec wykorzystujący roboczy wątek działający w tle 827 Kierowanie wywołań do interfejsu użytkownika w systemie Windows 830 D Zegary przed wprowadzeniem w wersji C# 5.0 słów kluczowych async i await 835 Skorowidz 841 Kup książkęPoleć książkę 10 Spis treści Kup książkęPoleć książkę 11 Typy generyczne Początek 2.0 G DY ZACZNIESZ TWORZYĆ BARDZIEJ złożone projekty, będziesz potrzebował lep- szego sposobu na ponowne wykorzystywanie i dostosowywanie istniejącego oprogramo- wania. Aby ułatwić wielokrotne wykorzystanie kodu (a zwłaszcza algorytmów), w języku C# udostępniono mechanizm typów generycznych. Podobnie jak metody są bardziej wartościowe, ponieważ mogą przyjmować argumenty, tak typy i metody przyjmujące argumenty określa- jące typ dają dodatkowe możliwości. Typy generyczne w języku C# są składniowo podobne do typów generycznych z Javy i szablonów z języka C++. We wszystkich trzech wymienionych językach wspomniane mecha- nizmy umożliwiają jednokrotne zaimplementowanie algorytmów i wzorców. Nie są potrzebne odrębne implementacje dla każdego typu, dla którego dany algorytm lub wzorzec działa. Jed- nak typy generyczne w języku C# znacznie różnią się od typów generycznych z Javy i szablo- nów z języka C++, jeśli chodzi o szczegóły implementacji oraz wpływ tych mechanizmów na system typów. Typy generyczne zostały dodane do środowiska uruchomieniowego i języka C# w wersji 2.0. Kup książkęPoleć książkę 416 Rozdział 11. Typy generyczne Język C# bez typów generycznych W ramach omawiania typów generycznych najpierw przeanalizujemy klasę, w której takie typy nie są używane. Ta klasa, System.Collections.Stack, reprezentuje stos, czyli kolekcję obiektów, w której ostatni element dodawany do kolekcji jest pierwszym elementem z niej pobieranym (jest to kolekcja typu „ostatni na wejściu, pierwszy na wyjściu”; ang. last in, first out — LIFO). Dwie główne metody klasy Stack, czyli Push() i Pop(), dodają elementy do stosu i usuwają je z niego. Deklaracje tych metod z klasy Stack znajdują się na listingu 11.1. Listing 11.1. Sygnatury metod klasy System.Collections.Stack public class Stack { public virtual object Pop() { ... } public virtual void Push(object obj) { ... } // … } W programach stos często służy do umożliwiania wielokrotnego cofania operacji. Na przy- kład na listingu 11.2 kod używa klasy System.Collections.Stack do wycofywania operacji w programie symulującym działanie znikopisu. Listing 11.2. Obsługa wycofywania operacji w programie symulującym działanie znikopisu using System; using System.Collections; class Program { // … public void Sketch() { Stack path = new Stack(); Cell currentPosition; ConsoleKeyInfo key; // Typ dodany w wersji C# 2.0. 2.0 do { // Wymazywanie w kierunku okre(cid:286)lanym przez // strza(cid:225)ki wci(cid:286)ni(cid:266)te przez u(cid:298)ytkownika. key = Move(); switch (key.Key) { case ConsoleKey.Z: // Wymazanie ostatnio narysowanego elementu. if (path.Count = 1) { currentPosition = (Cell)path.Pop(); Console.SetCursorPosition( currentPosition.X, currentPosition.Y); Undo(); Kup książkęPoleć książkę Język C# bez typów generycznych 417 } break; case ConsoleKey.DownArrow: case ConsoleKey.UpArrow: case ConsoleKey.LeftArrow: case ConsoleKey.RightArrow: // SaveState() currentPosition = new Cell( Console.CursorLeft, Console.CursorTop); path.Push(currentPosition); break; default: Console.Beep(); // Dodane w wersji C# 2.0. break; } } while (key.Key != ConsoleKey.X); // Klawisz X pozwala zamkn(cid:261)(cid:252) program. } } public struct Cell { // W wersjach starszych ni(cid:298) C# 6.0 nale(cid:298)y u(cid:298)y(cid:252) pola tylko do odczytu. public int X { get; } public int Y { get; } public Cell(int x, int y) { X = x; Y = y; } } Efekt uruchomienia kodu z listingu 11.2 znajdziesz w danych wyjściowych 11.1 2.0 DANE WYJŚCIOWE 11.1. W zmiennej path typu System.Collections.Stack program zachowuje wcześniejsze ruchy pędzla, przekazując element niestandardowego typu Cell do metody Stack.Push() (w wyra- żeniu path.Push(currentPosition)). Jeśli użytkownik wpisze literę Z (lub wybierze kombi- nację Ctrl+Z), poprzedni ruch pędzla zostaje anulowany. Anulowanie odbywa się przez Kup książkęPoleć książkę 2.0 418 Rozdział 11. Typy generyczne zdjęcie poprzedniego ruchu pędzla ze stosu za pomocą metody Pop(), przeniesienie pozycji kursora na wcześniejszą pozycję i wywołanie metody Undo(). Choć ten kod działa, klasa System.Collections.Stack ma ważną wadę. Na listingu 11.1 pokazano, że klasa Stack przechowuje wartości typu object. Ponieważ każdy obiekt w śro- dowisku CLR jest typu pochodnego od klasy object, klasa Stack nie sprawdza, czy elementy umieszczane w kolekcji są tego samego i odpowiedniego typu. Na przykład zamiast przekazy- wać zmienną currentPosition, możesz przekazać łańcuch znaków zawierający współrzędne X i Y połączone kropką. Kompilator musi zezwalać na zapis wartości niespójnych typów danych, ponieważ klasa Stack przyjmuje dowolny obiekt pochodny od klasy object. Specyficzny typ obiektu nie ma tu znaczenia. Ponadto po pobraniu (za pomocą metody Pop()) danych ze stosu należy zrzutować zwró- coną wartość na typ Cell. Jeśli jednak typ wartości zwróconej przez metodę Pop() jest różny od Cell, kod zgłosi wyjątek. Rzutowanie opóźnia sprawdzanie typu do czasu wykonywania programu, przez co program jest bardziej narażony na błędy. Podstawowy problem z tworze- niem (bez używania typów generycznych) klas, które mają obsługiwać różne typy danych, polega na tym, że typy te muszą działać dla wspólnej klasy bazowej lub wspólnego interfejsu. Zwykle tą wspólną klasą jest klasa object. Używanie w klasach wykorzystujących klasę object typów bezpośrednich, na przykład struktur lub liczb całkowitych, dodatkowo nasila problem. Jeśli do metody Stack.Push() prze- każesz wartość typu bezpośredniego, środowisko uruchomieniowe automatycznie opakuje tę wartość. W trakcie pobierania wartości typu bezpośredniego trzeba jawnie wypakować dane i zrzutować referencję do obiektu typu object (pobraną za pomocą metody Pop()) na typ bezpośredni. Rzutowanie typu referencyjnego na klasę bazową lub interfejs nie ma dużego wpływu na wydajność kodu, jednak operacja opakowywania typu bezpośredniego wiąże się z większymi kosztami, ponieważ trzeba przydzielić pamięć, skopiować wartość, a później odzyskać pamięć. C# to język ułatwiający zachowanie bezpieczeństwa ze względu na typ. Język ten zapro- jektowano w taki sposób, by wiele błędów związanych z typami (takich jak przypisanie liczby całkowitej do zmiennej typu string) było wykrywanych na etapie kompilacji. Problem polega na tym, że klasa Stack nie jest tak bezpieczna ze względu na typ, jak można tego oczekiwać po programach w języku C#. Aby zmodyfikować tę klasę i wymusić, by elementy stosu były określonego typu (jednak bez stosowania typów generycznych), należy utworzyć wyspecjali- zowaną wersję klasy, przedstawioną na listingu 11.3. Listing 11.3. Definicja wyspecjalizowanej wersji klasy Stack public class CellStack { public virtual Cell Pop(); public virtual void Push(Cell cell); // … } Ponieważ klasa CellStack może przechowywać tylko obiekty typu Cell, to rozwiązanie wymaga dodania niestandardowej implementacji metod potrzebnych do obsługi stosu, co Kup książkęPoleć książkę Język C# bez typów generycznych 419 nie jest wygodne. Utworzenie bezpiecznego ze względu na typ stosu liczb całkowitych wymaga następnej niestandardowej implementacji, a każda z nich jest bardzo podobna do wszystkich pozostałych. To skutkuje powstaniem dużej ilości powtarzającego się nadmiarowego kodu. ZAGADNIENIE DLA POCZĄTKUJĄCYCH Inny przykład — typy bezpośrednie z możliwą wartością null W rozdziale 2. opisano możliwość zadeklarowania zmiennych, które mogą zawierać wartość null. Wymaga to użycia modyfikatora ? w deklaracji zmiennej typu bezpośredniego. Ta moż- liwość pojawiła się w wersji C# 2.0, ponieważ potrzebne były do tego typy generyczne. Przed ich wprowadzeniem programiści mieli do wyboru dwa rozwiązania. Pierwsze z nich polegało na zadeklarowaniu typów danych z obsługą wartości null. Potrzebny był jeden taki typ dla każdego typu bezpośredniego, który miał przyjmować war- tości null. Kilka takich typów pokazano na listingu 11.4. Listing 11.4. Deklarowanie wersji różnych typów bezpośrednich z dodaną obsługą wartości null 2.0 struct NullableInt { /// summary /// Udost(cid:266)pnia warto(cid:286)(cid:252), je(cid:286)li metoda HasValue zwraca true. /// /summary public int Value{ get; private set; } /// summary /// Okre(cid:286)la, czy warto(cid:286)(cid:252) jest dost(cid:266)pna, czy jest równa null. /// /summary public bool HasValue{ get; private set; } // … } struct NullableGuid { /// summary /// Udost(cid:266)pnia warto(cid:286)(cid:252), je(cid:286)li metoda HasValue zwraca true. /// /summary public Guid Value{ get; private set; } /// summary /// Okre(cid:286)la, czy warto(cid:286)(cid:252) jest dost(cid:266)pna, czy jest równa null. /// /summary public bool HasValue{ get; private set; } ... } ... Na listingu 11.4 pokazano możliwe implementacje typów NullableInt i NullableGuid. Jeśli w programie potrzebne są dodatkowe typy bezpośrednie z obsługą wartości null, trzeba utwo- rzyć nową strukturę z właściwościami działającymi dla odpowiedniego typu. Każdą poprawkę 2.0 Kup książkęPoleć książkę 420 Rozdział 11. Typy generyczne w implementacji (na przykład dodanie zdefiniowanej przez użytkownika konwersji niejawnej z danego typu na jego odpowiednik obsługujący wartość null) wymaga wtedy zmodyfikowania deklaracji wszystkich typów. Druga strategia implementowania typu z obsługą wartości null bez typów generycznych polega na utworzeniu jednego typu z właściwością Value typu object. To rozwiązanie poka- zano na listingu 11.5. Listing 11.5. Deklarowanie typu z obsługą wartości null, zawierającego właściwość Value typu object struct Nullable { /// summary /// Udost(cid:266)pnia warto(cid:286)(cid:252), je(cid:286)li metoda HasValue zwraca true. /// /summary public object Value{ get; private set; } /// summary /// Okre(cid:286)la, czy warto(cid:286)(cid:252) jest dost(cid:266)pna, czy jest równa null. /// /summary public bool HasValue{ get; private set; } ... } Choć ta technika wymaga utworzenia tylko jednej implementacji typu z obsługą wartości null, środowisko uruchomieniowe zawsze opakowuje wtedy typy bezpośrednie, gdy usta- wiana jest wartość właściwości Value. Ponadto pobieranie wartości z tej właściwości wymaga rzutowania, którego wynik w czasie wykonywania programu może się okazać nieprawidłowy. Żadne z tych rozwiązań nie jest atrakcyjne. Aby wyeliminować ten problem, w wersji C# 2.0 dodano typy generyczne. Typy z obsługą wartości null mają teraz postać typu gene- rycznego Nullable T . Wprowadzenie do typów generycznych 2.0 Typy generyczne zapewniają mechanizm tworzenia struktur danych, które można przekształcić na wyspecjalizowaną wersję w celu obsługi konkretnych typów. Programiści definiują typy parametryzowane w taki sposób, by dla każdej zmiennej określonego typu generycznego używany był ten sam wewnętrzny algorytm. Jednak typy danych i sygnatury metod mogą się zmieniać w zależności od podanego argumentu określającego typ. Aby ułatwić programistom naukę, projektanci języka C# zdecydowali się na zastosowa- nie składni pozornie podobnej do składni szablonów z języka C++. W C# składnia tworzenia klas i struktur generycznych wymaga użycia nawiasów ostrych do deklarowania parametrów w deklaracji typu i do podawania argumentów, gdy typ jest używany. Kup książkęPoleć książkę Wprowadzenie do typów generycznych 421 Używanie klasy generycznej Na listingu 11.6 pokazano, jak w klasie generycznej podać argument określający typ. W kodzie zmienna path jest tworzona jako stos obiektów typu Cell. W tym celu typ Cell jest poda- wany w nawiasie ostrym zarówno w wyrażeniu tworzącym obiekt, jak i w deklaracji zmiennej. Oznacza to, że gdy deklarujesz zmienną (tu jest to zmienna path) typu generycznego, C# wymaga, by podać argument określający typ używany przez dany typ generyczny. Na lis- tingu 11.6 pokazano ten proces na przykładzie nowej generycznej klasy Stack. Listing 11.6. Implementowanie wycofywania operacji za pomocą generycznej klasy Stack using System; using System.Collections.Generic; class Program { // … public void Sketch() { Stack Cell path; // Deklaracja zmiennej typu generycznego. path = new Stack Cell (); // Tworzenie obiektu typu generycznego. Cell currentPosition; ConsoleKeyInfo key; do { // Rysowanie kreski w kierunku okre(cid:286)lonym przez // strza(cid:225)k(cid:266) wci(cid:286)ni(cid:266)t(cid:261) przez u(cid:298)ytkownika. key = Move(); switch (key.Key) { case ConsoleKey.Z: // Cofni(cid:266)cie poprzedniego ruchu p(cid:266)dzla. if (path.Count = 1) { // Rzutowanie nie jest potrzebne. currentPosition = path.Pop(); Console.SetCursorPosition( currentPosition.X, currentPosition.Y); Undo(); } break; case ConsoleKey.DownArrow: case ConsoleKey.UpArrow: case ConsoleKey.LeftArrow: case ConsoleKey.RightArrow: // SaveState() currentPosition = new Cell( Console.CursorLeft, Console.CursorTop); // W wywo(cid:225)aniu Push() mo(cid:298)na u(cid:298)ywa(cid:252) tylko zmiennych typu Cell. path.Push(currentPosition); break; 2.0 Kup książkęPoleć książkę 422 Rozdział 11. Typy generyczne default: Console.Beep(); // Metoda dodana w wersji C# 2.0. break; } } while (key.Key != ConsoleKey.X); // Klawisz X pozwala zamkn(cid:261)(cid:252) aplikacj(cid:266). } } Wynik działania kodu z listingu 11.6 pokazano w danych wyjściowych 11.2. DANE WYJŚCIOWE 11.2. 2.0 Na listingu 11.6 deklarowana jest zmienna path inicjowana nowym obiektem klasy System. (cid:180)Collections.Generic.Stack Cell . W nawiasie ostrym podano, że typ danych elementów stosu to Cell. Dlatego każdy obiekt dodawany do zmiennej path i z niej pobierany jest typu Cell. Nie trzeba więc rzutować wartości zwracanej przez metodę path.Pop() ani samodziel- nie zapewniać, że tylko obiekty typu Cell są dodawane za pomocą metody Push() do zmien- nej path. Definiowanie prostej klasy generycznej Typy generyczne umożliwiają tworzenie algorytmów i wzorców oraz ponowne wykorzystanie napisanego kodu dla innych typów danych. Na listingu 11.7 tworzona jest klasa Stack T , podobna do klasy System.Collections.Generic.Stack T użytej na listingu 11.6. Parametr określający typ (T) należy podać w nawiasie ostrym po nazwie klasy. Później do generycz- nego typu Stack T można przekazać jeden argument określający typ, podstawiany wszędzie tam, gdzie w klasie występuje T. Dzięki temu stos może przechowywać elementy dowolnego podanego typu. Nie wymaga to duplikowania kodu ani konwersji elementów na typ object. Parametr T (określający typ) to symbol zastępczy, który należy zastąpić argumentem określa- jącym typ. Na listingu 11.7 parametr określający typ jest używany w wewnętrznej tablicy Items, w parametrze metody Push() i w wartości zwracanej przez metodę Pop(). Listing 11.7. Deklarowanie generycznej klasy Stack T public class Stack T { // W wersjach starszych ni(cid:298) C# 6.0 nale(cid:298)y zastosowa(cid:252) pole tylko do odczytu. private T[] InternalItems { get; } Kup książkęPoleć książkę Wprowadzenie do typów generycznych 423 public void Push(T data) { ... } public T Pop() { ... } } Zalety typów generycznych 2.0 Stosowanie klas generycznych zamiast ich standardowych odpowiedników (na przykład użytej wcześniej klasy System.Collections.Generic.Stack T zamiast jej pierwowzoru System.Col (cid:180)lections.Stack) daje kilka korzyści. 1. Typy generyczne pozwalają zwiększyć bezpieczeństwo ze względu na typ. Uniemożliwiają stosowanie typów innych niż typy jawnie określone dla składowych parametryzowanej klasy. Na listingu 11.7 reprezentująca stos parametryzowana klasa Stack Cell pozwala stosować tylko typ Cell. Na przykład instrukcja path. (cid:180)Push( garbage ) spowoduje błąd kompilacji informujący, że nie istnieje wersja przeciążonej metody System.Collections.Generic.Stack T .Push(T) działająca na łańcuchach znaków, ponieważ łańcucha nie można przekształcić na typ Cell. 2. Sprawdzanie typów na etapie kompilacji zmniejsza prawdopodobieństwo wystąpienia wyjątków typu InvalidCastException w czasie wykonywania programu. 3. Używanie typów bezpośrednich w składowych klasy generycznej nie powoduje opakowywania wartości tych typów w typ object. Na przykład metody path.Pop() i path.Push() nie wymagają opakowania elementu w momencie dodawania go i wypakowywania w trakcie usuwania. 4. Typy generyczne w języku C# zmniejszają ilość kodu. Pozwalają zachować korzyści, jakie dają specyficzne wersje klasy, ale nie powodują analogicznych kosztów. Nie trzeba na przykład definiować nowej klasy CellStack. 5. Wydajność kodu rośnie, ponieważ nie jest potrzebne rzutowanie z typu object. Eliminuje to operację sprawdzania typu. Inną przyczyną wzrostu wydajności jest to, że nie trzeba opakowywać wartości typów bezpośrednich. 6. Typy generyczne zmniejszają ilość zajmowanej pamięci, ponieważ nie trzeba opakowywać wartości. Dzięki temu program zużywa mniej pamięci na stercie. 7. Kod staje się bardziej czytelny, ponieważ jest w nim mniej operacji sprawdzania typów przy rzutowaniu i mniej implementacji specyficznych typów. 8. Edytory wspomagające pisanie kodu za pomocą jednej z odmian mechanizmu IntelliSense bezpośrednio obsługują wartości zwracane przez klasy generyczne. Nie trzeba rzutować zwracanych danych, aby używać mechanizmu IntelliSense. Kup książkęPoleć książkę 424 Rozdział 11. Typy generyczne Istotą typów generycznych jest umożliwienie implementowania wzorców i wielokrotne wykorzystywanie tych implementacji wszędzie tam, gdzie dany wzorzec jest potrzebny. Wzorce opisują problemy, które często występują w kodzie. Szablony zapewniają jedno rozwiązanie dla tych powtarzających się wzorców. 2.0 Wskazówki związane z tworzeniem nazw parametrów określających typy Podobnie jak nazwy parametrów formalnych metod, tak i nazwy parametrów określających typ powinny być jak najbardziej opisowe. Ponadto aby podkreślić, że dany parametr określa typ, nazwę takiego parametru należy poprzedzić literą T. Na przykład w definicji klasy Entity (cid:180)Collection TEntity nazwa parametru określającego typ to TEntity. Z opisowych nazw można zrezygnować w jednej sytuacji — wtedy, gdy nie dodają żadnej wartości. Na przykład użycie samej litery T w nazwie klasy Stack T jest odpowiednie, ponie- waż informacja, że T to parametr określający typ, jest wystarczająco opisowa. Stos działa dla obiektów dowolnego typu. W następnym podrozdziale zapoznasz się z ograniczeniami. Dobrą praktyką jest używanie nazw opisujących ograniczenia. Na przykład jeśli jako parametr trzeba podać typ z imple- mentacją interfejsu IComponent, możesz nazwać ten parametr TComponent. Wskazówki STOSUJ opisowe nazwy parametrów określających typ i poprzedzaj te nazwy literą T. ROZWAŻ podanie ograniczenia w nazwie parametru określającego typ. Generyczne interfejsy i struktury C# obsługuje stosowanie typów generycznych w różnych konstrukcjach języka, w tym w inter- fejsach i strukturach. Składnia jest tu identyczna jak dla klas. Aby zadeklarować interfejs z parametrem określającym typ, umieść ten parametr w nawiasie ostrym bezpośrednio po nazwie interfejsu. Pokazano to w przykładowym interfejsie IPair T na listingu 11.8. Listing 11.8. Deklarowanie generycznego interfejsu interface IPair T { T First { get; set; } T Second { get; set; } } 2.0 Ten interfejs reprezentuje parę podobnych obiektów (na przykład współrzędnych punktu, biologicznych rodziców danej osoby lub węzłów w drzewie binarnym). Oba elementy w parze są tego samego typu. Kup książkęPoleć książkę Wprowadzenie do typów generycznych 425 Aby zaimplementować ten interfejs, należy zastosować taką samą składnię jak w klasach niegenerycznych. Zauważ, że dozwolone (i często spotykane) jest użycie argumentu określa- jącego typ z jednego typu generycznego także w innym typie. Taka sytuacja ma miejsce na listingu 11.9. Argument określający typ dla interfejsu jest jednocześnie argumentem określa- jącym typ w strukturze. W tym przykładzie zamiast klasy zastosowano właśnie strukturę, co jest dowodem na to, że C# umożliwia tworzenie niestandardowych generycznych typów bez- pośrednich. Listing 11.9. Implementowanie generycznego interfejsu public struct Pair T : IPair T { public T First { get; set; } public T Second { get; set; } } Obsługa generycznych interfejsów jest ważna zwłaszcza w klasach reprezentujących kolekcje. To właśnie w takich klasach najczęściej używa się typów generycznych. Przed wpro- wadzeniem takich typów programiści musieli posługiwać się zestawem interfejsów z prze- strzeni nazw System.Collections. Te interfejsy (podobnie jak klasy z ich implementacjami) działały tylko dla typu object, dlatego dostęp do elementów z takich klas zawsze wymagał rzutowania. Dzięki zastosowaniu generycznych interfejsów bezpiecznych ze względu na typ można uniknąć rzutowania. ZAGADNIENIE DLA ZAAWANSOWANYCH Wielokrotne implementowanie jednego interfejsu w tej samej klasie Dwie deklaracje tego samego generycznego interfejsu są uznawane za różne typy. Dlatego ten sam generyczny interfejs można wielokrotnie zaimplementować w jednej klasie lub strukturze. Przyjrzyj się przykładowi z listingu 11.10. Listing 11.10. Wielokrotne implementowanie interfejsu w jednej klasie public interface IContainer T { ICollection T Items { get; set; } { get; set; } } public class Person: IContainer Address , IContainer Phone , IContainer Email { ICollection Address IContainer Address .Items { get{...} set{...} } 2.0 Kup książkęPoleć książkę 426 Rozdział 11. Typy generyczne ICollection Phone IContainer Phone .Items { get{...} set{...} } ICollection Email IContainer Email .Items { get{...} set{...} } } W tym przykładzie właściwość Items pojawia się wielokrotnie w jawnie zaimplemento- wanych interfejsach z różnymi parametrami określającymi typ. Bez typów generycznych to rozwiązanie byłoby niemożliwe. Kompilator umożliwiłby jawne zaimplementowanie tylko jednej właściwości IContainer.Items. Jednak technikę implementowania wielu wersji „tego samego” interfejsu wiele osób uznaje za złą praktykę, ponieważ może utrudniać zrozumienie kodu (zwłaszcza gdy interfejs pozwala na konwersje kowariantne lub kontrawariantne). Ponadto klasę Person można uznać za źle zaprojektowaną. Normalnie nie uważamy osoby za „coś, co może udostępniać zestaw adresów e-mail”. Jeśli czujesz pokusę zaimplementowania w klasie trzech wersji tego samego interfejsu, pomyśl, czy nie lepiej będzie zamiast tego zaimplementować trzech właściwości, na przykład EmailAddresses, PhoneNumbers i MailingAddresses, z których każda zwraca odpowiednią implementację generycznego interfejsu. Wskazówka UNIKAJ implementowania wielu wersji tego samego generycznego inter- fejsu w jednym typie. 2.0 Definiowanie konstruktora i finalizatora Zaskoczeniem może się okazać to, że konstruktory (i finalizator) klasy lub struktury gene- rycznej nie wymagają parametrów określających typ. Oznacza to, że zapis Pair T (){...} nie jest konieczny. W klasie Pair na listingu 11.11 konstruktor jest zadeklarowany z sygnaturą public Pair(T first, T second). Listing 11.11. Deklarowanie konstruktora typu generycznego public struct Pair T : IPair T { public Pair(T first, T second) { First = first; Second = second; } Kup książkęPoleć książkę Wprowadzenie do typów generycznych 427 public T First { get; set; } public T Second { get; set; } } Określanie wartości domyślnej Na listingu 11.11 występuje konstruktor, który przyjmuje początkowe wartości właściwości First i Second oraz przypisuje je do pól _First i _Second. Ponieważ typ Pair T to struktura, konstruktor musi inicjować wszystkie jej pola. Prowadzi to jednak do problemu. Pomyśl o kon- struktorze z typu Pair T , który w trakcie tworzenia obiektu inicjuje tylko jeden element z pary. Zdefiniowanie takiego konstruktora, pokazanego na listingu 11.12, prowadzi do błędu kompilacji, ponieważ pole _Second po zakończeniu pracy konstruktora wciąż nie jest zaini- cjowane. Zainicjowanie pola _Second sprawia trudność, ponieważ typ danych T nie jest znany. Jeśli używany jest typ referencyjny, można ustawić wartość null, jednak to rozwiązanie nie zadziała, jeżeli T to typ bezpośredni, który nie obsługuje wartości null. Listing 11.12. Jeśli nie wszystkie pola zostaną zainicjowane, wystąpi błąd kompilacji public struct Pair T : IPair T { // B(cid:224)(cid:260)D: Do pola Pair T ._second trzeba przypisa(cid:252) // warto(cid:286)(cid:252) przed wyj(cid:286)ciem sterowania poza konstruktor. // public Pair(T first) // { // First = first; // } // … } 2.0 Aby umożliwić rozwiązanie tego problemu, w języku C# udostępniono operator default, opisany wcześniej w rozdziale 8., gdzie wyjaśniono, że wartość domyślną typu int można podać za pomocą wyrażenia default(int). Gdy używany jest typ T (potrzebny w polu _Second), można podać wartość default(T). Tę technikę zastosowano na listingu 11.13. Listing 11.13. Inicjowanie pola za pomocą operatora default public struct Pair T : IPair T { public Pair(T first) { First = first; Second = default(T); } // … } Operator default pozwala podać wartość domyślną dowolnego typu (także podanego za pomocą parametru). Kup książkęPoleć książkę 428 Rozdział 11. Typy generyczne Wiele parametrów określających typ W typach generycznych można zadeklarować dowolną liczbę parametrów określających typ. W przedstawionym na początku typie Pair T występował tylko jeden taki parametr. Aby umożliwić zapis niejednorodnej pary obiektów, na przykład pary nazwa-wartość, możesz utworzyć nową wersję tego typu, obejmującą dwa parametry określające typ. To rozwiązanie pokazano na listingu 11.14. Listing 11.14. Deklarowanie typu generycznego z kilkoma parametrami określającymi typ interface IPair TFirst, TSecond { TFirst First { get; set; } TSecond Second { get; set; } } 2.0 public struct Pair TFirst, TSecond : IPair TFirst, TSecond { public Pair(TFirst first, TSecond second) { First = first; Second = second; } public TFirst First { get; set; } public TSecond Second { get; set; } } Gdy używasz klasy Pair TFirst, TSecond , powinieneś podać oba parametry określające typ w nawiasie ostrym w deklaracji i przy inicjowaniu obiektu. Następnie należy stosować wła- ściwe typy dla parametrów metod w ich wywołaniach. To podejście pokazano na listingu 11.15. Listing 11.15. Używanie typu z kilkoma parametrami określającymi typ Pair int, string historicalEvent = new Pair int, string (1914, Shackleton wyrusza na Biegun Pó(cid:239)nocny na statku Endurance ); Console.WriteLine( {0}: {1} , historicalEvent.First, historicalEvent.Second); Liczba parametrów określających typ (czyli arność) jednoznacznie odróżnia daną klasę od innych klas o tej samej nazwie. Można więc w jednej przestrzeni nazw zdefiniować klasy Pair T i Pair TFirst, TSecond , ponieważ mają różną arność. Ponadto z powodu podobnego działania typy generyczne różniące się tylko arnością należy umieszczać w tych samych pli- kach z kodem w języku C#. Wskazówka UMIESZCZAJ w jednym pliku klasy generyczne różniące się tylko liczbą para- metrów określających typ. Kup książkęPoleć książkę Wprowadzenie do typów generycznych 429 Różna arność W wersji C# 4.0 zespół odpowiedzialny za środowisko CLR zdefiniował dziewięć nowych typów generycznych. Wszystkie te typy reprezentują krotki i noszą nazwę Tuple. W typach Tuple, podobnie jak w typach Pair … , można wielokrotnie wykorzystać tę samą nazwę, ponieważ typy te różnią się arnością (każda wersja ma inną liczbę parametrów określających typ). Te typy pokazano na listingu 11.16. Początek 4.0 2.0 Listing 11.16. Wykorzystanie arności do przeciążenia definicji typu public class Tuple { ... } public class Tuple T1 : IStructuralEquatable, IStructuralComparable, IComparable {...} public class Tuple T1, T2 : ... {...} public class Tuple T1, T2, T3 : ... {...} public class Tuple T1, T2, T3, T4 : ... {...} public class Tuple T1, T2, T3, T4, T5 : ... {...} public class Tuple T1, T2, T3, T4, T5, T6 : ... {...} public class Tuple T1, T2, T3, T4, T5, T6, T7 : ... {...} public class Tuple T1, T2, T3, T4, T5, T6, T7, TRest : ... {...} Zestaw klas Tuple … zaprojektowano w tym samym celu co typy Pair T i Pair TFirst, TSecond , jednak klasy Tuple umożliwiają obsługę do siedmiu argumentów określających typ. W ostatniej wersji klasy Tuple z listingu 11.16 parametr TRest można wykorzystać do zapi- sania następnego obiektu typu Tuple. Dlatego liczba elementów w krotce jest potencjalnie nieskończona. Inną ciekawą składową rodziny klas Tuple jest niegeneryczna klasa Tuple. Ta klasa ma osiem statycznych metod fabrycznych, które służą do tworzenia obiektów różnych generycz- nych typów Tuple. Choć każdy typ generyczny umożliwia bezpośrednie tworzenie obiektów za pomocą konstruktora, metody fabryczne z klasy Tuple automatycznie wykrywają typy argumentów. Różnicę między bezpośrednimi wywołaniami a używaniem metod fabrycznych pokazano na listingu 11.17. Listing 11.17. Używanie metod fabrycznych Create() z klasy Tuple Tuple string, Contact keyValuePair; keyValuePair = Tuple.Create( 555-55-5555 , new Contact( Inigo Montoya )); keyValuePair = new Tuple string, Contact ( 555-55-5555 , new Contact( Inigo Montoya )); Gdy liczba elementów w obiektach typu Tuple jest duża, podawanie wszystkich argumentów określających typ jest kłopotliwe. Łatwiej zastosować wówczas metody fabryczne Create(). Na podstawie tego, że w bibliotece platformy zadeklarowanych jest osiem różnych gene- rycznych typów Tuple, można się domyślić, iż w systemie plików środowiska CLR nie są obsłu- giwane typy generyczne o różnej liczbie parametrów. Metody mogą przyjmować dowolną liczbę argumentów za pomocą tablic z parametrami, nie istnieje jednak analogiczna techni- ka dla typów generycznych. Każdy typ generyczny musi mieć ściśle określoną arność. Koniec 4.0 Kup książkęPoleć książkę 430 Rozdział 11. Typy generyczne Zagnieżdżone typy generyczne 2.0 Parametry określające typ w typie generycznym są automatycznie kaskadowo przekazywane w dół do typów zagnieżdżonych. Na przykład jeśli w danym typie zadeklarowany jest okre- ślający typ parametr T, wszystkie typy zagnieżdżone też będą generyczne, a parametr T rów- nież będzie w nich dostępny. Jeżeli typ zagnieżdżony ma własny określający typ parametr T, spowoduje on ukrycie parametru z nadrzędnego typu. Wtedy wszystkie referencje do para- metru T w typie zagnieżdżonym będą dotyczyły parametru właśnie z tego typu. Na szczęście ponowne użycie w typie zagnieżdżonym określającego typ parametru o wykorzystanej już nazwie powoduje, że kompilator wyświetla ostrzeżenie. Zapobiega to przypadkowemu uży- ciu parametrów o tej samej nazwie (zobacz listing 11.18). Listing 11.18. Zagnieżdżone typy generyczne class Container T, U { // Klasy zagnie(cid:298)d(cid:298)one dziedzicz(cid:261) parametry okre(cid:286)laj(cid:261)ce typ. // Ponowne wykorzystanie nazwy takiego parametru // prowadzi do zg(cid:225)oszenia ostrze(cid:298)enia. class Nested U { void Method(T param0, U param1) { } } } Określające typ parametry z typu nadrzędnego są dostępne w typie zagnieżdżonym w ten sam sposób jak składowe typu nadrzędnego. Reguła jest prosta — parametr określający typ jest dostępny wszędzie wewnątrz typu, w jakim go zadeklarowano. Wskazówka UNIKAJ ukrywania określającego typ parametru z typu nadrzędnego przez tworzenie parametru o identycznej nazwie w typie zagnieżdżonym. Ograniczenia Typy generyczne umożliwiają definiowanie ograniczeń dotyczących parametrów określają- cych typ. Te ograniczenia gwarantują, że typy podane jako argumenty będą zgodne z wyma- ganymi regułami. Przyjrzyj się przykładowej klasie BinaryTree T z listingu 11.19. 2.0 Listing 11.19. Deklaracja klasy BinaryTree T bez ograniczeń public class BinaryTree T { public BinaryTree ( T item) { Item = item; Kup książkęPoleć książkę Ograniczenia 431 } public T Item { get; set; } public Pair BinaryTree T SubItems { get; set; } } Ciekawostką jest to, że w klasie BinaryTree T wewnętrznie używany jest typ Pair T . Jest to dopuszczalne, ponieważ Pair T to zwykły inny typ. Załóżmy, że chcesz, by drzewo sortowało wartości w obiekcie typu Pair T przypisywanym do właściwości SubItems. Aby posortować dane, akcesor set właściwości SubItems używa metody CompareTo() z podanego klucza. Ilustruje to listing 11.20. Listing 11.20. Do działania interfejsu potrzebny jest parametr określający typ public class BinaryTree T { public T Item { get; set; } public Pair BinaryTree T SubItems { get{ return _SubItems; } set { IComparable T first; // B(cid:224)(cid:260)D: nie mo(cid:298)na przeprowadzi(cid:252) niejawnej konwersji. first = value.First; // Konieczne jest jawne rzutowanie. if (first.CompareTo(value.Second) 0) { // Warto(cid:286)(cid:252) w(cid:225)a(cid:286)ciwo(cid:286)ci First jest mniejsza ni(cid:298) w(cid:225)a(cid:286)ciwo(cid:286)ci Second. // … } else { // Warto(cid:286)ci w(cid:225)a(cid:286)ciwo(cid:286)ci First i Second s(cid:261) takie same // lub warto(cid:286)(cid:252) w(cid:225)a(cid:286)ciwo(cid:286)ci Second jest mniejsza. // … } _SubItems = value; } } private Pair BinaryTree T _SubItems; } 2.0 W trakcie kompilacji określający typ parametr T jest generyczny i nie obowiązują dla niego ograniczenia. Gdy kod wygląda tak jak na listingu 11.20, kompilator przyjmuje, że typ T zawiera jedynie składowe odziedziczone po typie bazowym object. Można tak przyjąć, ponieważ object jest klasą bazową wszystkich typów. Dlatego dla obiektów typu T można wywoływać tylko takie metody jak ToString(). W efekcie kompilator wyświetla błąd kompilacji, ponieważ w typie object nie zdefiniowano metody CompareTo(). By uzyskać dostęp do metody CompareTo(), parametr T można zrzutować na interfejs IComparable T . Ilustruje to listing 11.21. Kup książkęPoleć książkę 432 Rozdział 11. Typy generyczne Listing 11.21. Parametr określający typ musi być zgodny z interfejsem; w przeciwnym razie wystąpi wyjątek public class BinaryTree T { public T Item { get; set; } public Pair BinaryTree T SubItems { get{ return _SubItems; } set { IComparable T first; first = (IComparable T )value.First.Item; if (first.CompareTo(value.Second.Item) 0) { // Warto(cid:286)(cid:252) w(cid:225)a(cid:286)ciwo(cid:286)ci First jest mniejsza ni(cid:298) w(cid:225)a(cid:286)ciwo(cid:286)ci Second. ... } else { // Warto(cid:286)(cid:252) w(cid:225)a(cid:286)ciwo(cid:286)ci Second jest mniejsza lub równa wzgl(cid:266)dem w(cid:225)a(cid:286)ciwo(cid:286)ci First. ... } _SubItems = value; } } private Pair BinaryTree T _SubItems; } 2.0 Niestety, jeśli teraz zadeklarujesz zmienną klasy BinaryTree Typ , a podany typ nie zawiera implementacji interfejsu IComparable Typ , wystąpi błąd czasu wykonania (InvalidCastEx (cid:180)ception). To sprawia, że główny powód stosowania typów generycznych — poprawa bez- pieczeństwa ze względu na typ — staje się nieaktualny. Aby w przypadku gdy podany typ nie zawiera implementacji interfejsu, uniknąć wspomnia- nego wyjątku i zamiast niego otrzymać błąd kompilacji, można podać dostępną w języku C# opcjonalną listę ograniczeń dla każdego określającego typ parametru zadeklarowanego w typie generycznym. Ograniczenie opisuje cechy, jakich dany typ generyczny wymaga od typów podawanych w parametrach. Do deklarowania ograniczeń służy słowo kluczowe where, po którym podawana jest para parametr-wymaganie. Parametry muszą być parametrami danego typu generycznego, a wymagania dotyczą klas lub interfejsów, na jakie możliwe musi być przekształcenie typu podanego w parametrze, wymagają obecności konstruktora domyślnego lub określają, że konieczny jest typ referencyjny bądź bezpośredni. Ograniczenia dotyczące interfejsu Aby zapewnić właściwy porządek węzłów w drzewie binarnym, można wykorzystać metodę Com (cid:180)pareTo() z klasy BinaryTree. Najlepszym rozwiązaniem jest dodanie ograniczenia dotyczącego określającego typ parametru T. Podany typ powinien implementować interfejs IComparable T . Składnię służącą do deklarowania takiego ograniczenia przedstawiono na listingu 11.22. Kup książkęPoleć książkę Ograniczenia 433 Listing 11.22. Deklarowanie ograniczenia dotyczącego interfejsu public class BinaryTree T where T: System.IComparable T { public T Item { get; set; } public Pair BinaryTree T SubItems { get{ return _SubItems; } set { IComparable T first; // Zauwa(cid:298), (cid:298)e teraz mo(cid:298)na pomin(cid:261)(cid:252) rzutowanie. first = value.First.Item; if (first.CompareTo(value.Second.Item) 0) { // Warto(cid:286)(cid:252) w(cid:225)a(cid:286)ciwo(cid:286)ci First jest mniejsza ni(cid:298) warto(cid:286)(cid:252) w(cid:225)a(cid:286)ciwo(cid:286)ci Second. ... } else { // Warto(cid:286)(cid:252) w(cid:225)a(cid:286)ciwo(cid:286)ci Second jest mniejsza lub równa wzgl(cid:266)dem w(cid:225)a(cid:286)ciwo(cid:286)ci First. ... } _SubItems = value; } } private Pair BinaryTree T _SubItems; } Po dodaniu na listingu 11.22 ograniczenia dotyczącego interfejsu kompilator za każdym razem, gdy używasz klasy BinaryTree T , sprawdza, czy podany typ zawiera implementację odpowiedniej wersji interfejsu IComparable T . Ponadto nie trzeba teraz jawnie rzutować zmiennej na interfejs IComparable T przed wywołaniem metody CompareTo(). Rzutowanie nie jest potrzebne nawet do uzyskania dostępu do składowych z jawnie podawanym interfejsem, gdzie w innych kontekstach brak rzutowania powoduje ukrycie danej składowej. Gdy wywołu- jesz metodę obiektu typu podanego w parametrze typu generycznego, kompilator sprawdza, czy dana metoda pasuje do którejś z metod dowolnego interfejsu zadeklarowanego w ograni- czeniach. Jeśli teraz spróbujesz utworzyć zmienną typu BinaryTree T , podając w parametrze typ System.Text.StringBuilder, wystąpi błąd kompilacji, ponieważ typ StringBuilder nie zawiera implementacji interfejsu IComparable StringBuilder . Wyświetlany jest wtedy komunikat podobny do tekstu z danych wyjściowych 11.3. DANE WYJŚCIOWE 11.3. error CS0311: The type System.Text.StringBuilder cannot be used as type parameter T in the generic type or method BinaryTree T . There is no implicit reference conversion from System.Text.StringBuilder to System.IComparable System.Text.StringBuilder . 2.0 Kup książkęPoleć książkę 434 Rozdział 11. Typy generyczne Aby zażądać implementacji danego interfejsu, należy zadeklarować ograniczenie doty- czące interfejsu. Dzięki takiemu ograniczeniu nie trzeba nawet rzutować wartości, by wywo- łać składowe z jawnie podawanym interfejsem. Ograniczenia dotyczące klasy Czasem przydatne jest ograniczenie polegające na tym, że argument ma określać typ, który można przekształcić na wskazaną klasę. W tym celu należy zastosować ograniczenie doty- czące klasy, pokazane na listingu 11.23. Listing 11.23. Deklarowanie ograniczenia dotyczącego klasy public class EntityDictionary TKey, TValue : System.Collections.Generic.Dictionary TKey, TValue where TValue : EntityBase { ... } 2.0 Na listingu 11.23 w klasie EntityDictionary TKey, TValue wymagane jest, by wszystkie typy podawane jako parametr TValue umożliwiały niejawną konwersję na klasę EntityBase. Dzięki temu wymogowi w implementacji typu generycznego możliwe jest używanie skła- dowych klasy EntityBase w wartościach typu TValue. Jest tak, ponieważ ograniczenie gwa- rantuje, że wszystkie typy podane jako argument można niejawnie przekształcić na klasę EntityBase. Składnia służąca do dodawania ograniczenia dotyczącego klasy jest taka sama jak dla ogra- niczenia dotyczącego interfejsu. Ważne jest jednak to, że ograniczenia dotyczące klasy trzeba podawać przed ograniczeniami dotyczącymi interfejsu (podobnie jak w deklaracji klasy klasę bazową podaje się przed listą implementowanych interfejsów). Jednak — inaczej niż w przy- padku ograniczeń dotyczących interfejsu — nie jest możliwe dodanie kilku ograniczeń doty- czących klasy. Wynika to z tego, że klasa nie może dziedziczyć po kilku niepowiązanych ze sobą klasach. W ograniczeniu dotyczącym klasy nie można też podawać klas zamkniętych i typów innych niż klasy. C# nie zezwala na przykład na dodanie ograniczenia dotyczącego typu string lub System.Nullable T , ponieważ wtedy jako argument typu generycznego można podać wyłącznie jeden typ. Trudno wówczas mówić, że typ naprawdę jest „generyczny”. Jeśli jako argument określający typ można podać tylko jeden typ, nie ma sensu stosować do tego parametru. Wystarczy bezpośrednio podać potrzebny typ. Ponadto w ograniczeniu dotyczącym klas nie można podawać niektórych specjalnych typów. Szczegółowe informacje na ten temat znajdziesz w ZAGADNIENIU DLA ZAAWANSOWA- NYCH „Wymogi związane z ograniczeniami” w dalszej części rozdziału. Ograniczenia wymagające struktury lub klasy (struct i class) Innym przydatnym ograniczeniem w typach generycznych jest możliwość zażądania, by typ podany w argumencie był typem bezpośrednim bez obsługi wartości null lub typem referen- cyjnym. Język C# udostępnia w tym celu specjalną składnię, która działa dla typów bezpo- Kup książkęPoleć książkę Ograniczenia 435 średnich i referencyjnych. Zamiast określać klasę, po której T ma dziedziczyć, można podać słowo kluczowe struct lub class. Ilustruje to listing 11.24. Listing 11.24. Dodawanie wymogu, by jako parametr określający typ podawano typ bezpośredni public struct Nullable T : IFormattable, IComparable, IComparable Nullable T , INullable where T : struct { // … } 2.0 Zauważ, że ograniczenie class nie wymaga, by jako argument określający typ podano klasę; wymaganie dotyczy typów referencyjnych, dlatego jego nazwa jest myląca. Typ podany jako parametr z ograniczeniem class może być dowolną klasą, interfejsem, delegatem lub typem tablicowym. Ponieważ ograniczenie dotyczące klasy wymaga podania konkretnej klasy, użycie ograni- czenia struct wyklucza zastosowanie ograniczenia dotyczącego klasy. Nie można więc łączyć ograniczenia struct z ograniczeniem dotyczącym konkretnej klasy. Ograniczenie struct ma pewną cechę — uniemożliwia podawanie typów bezpośrednich z obsługą wartości null. Z czego to wynika? Typy bezpośrednie z obsługą wartości null są imple- mentowane za pomocą typu generycznego Nullable T , w którym do T stosowane jest ogra- niczenie struct. Gdyby typ bezpośredni z obsługą wartości null był zgodny z omawianym ograniczeniem, możliwe byłoby zdefiniowanie bezsensownego typu Nullable Nullable int . Typ int z dwukrotnie dodaną obsługą wartości null jest na tyle nieintuicyjny, że trudno określić jego znaczenie. Z podobnych powodów niedozwolony jest też skrótowy zapis int??. Zestawy ograniczeń Dla parametru określającego typ można ustawić dowolną liczbę ograniczeń dotyczących interfejsu, ale tylko jedno ograniczenie dotyczące klasy (podobnie w klasie można zaimple- mentować dowolną liczbę interfejsów, ale dziedziczyć po tylko jednej innej klasie). Każde nowe ograniczenie jest deklarowane na rozdzielonej przecinkami liście, która znajduje się po nazwie parametru typu generycznego i dwukropku. Jeśli występuje więcej niż jeden parametr okre- ślający typ, słowo kluczowe where należy umieścić przed każdym takim parametrem, do którego dodawane są ograniczenia. Na listingu 11.25 w generycznej klasie EntityDictionary zadeklarowane są dwa parametry określające typ — TKey i TValue. Parametr TKey ma dwa ograniczenia dotyczące interfejsu, a do parametru TValue dodano jedno ograniczenie doty- czące klasy. Listing 11.25. Ustawianie wielu ograniczeń public class EntityDictionary TKey, TValue : Dictionary TKey, TValue where TKey : IComparable TKey , IFormattable where TValue : EntityBase Kup książkęPoleć książkę 436 Rozdział 11. Typy generyczne { ... } 2.0 W tym kodzie ustawianych jest kilka ograniczeń parametru TKey i dodatkowe ograniczenie parametru TValue. Gdy dodawanych jest wiele ograniczeń jednego parametru określającego typ, wszystkie one muszą być spełnione. Na przykład jeśli jako argument TKey podano typ C, typ C musi zawierać implementację interfejsów IComparable C oraz IFormattable. Zauważ, że między klauzulami where nie ma przecinka. Ograniczenia dotyczące konstruktora W pewnych sytuacjach w klasie generycznej potrzebny jest obiekt typu podanego jako argu- ment tej klasy. Na listingu 11.26 metoda MakeValue() klasy EntityDictionary TKey, TValue musi tworzyć obiekt typu podanego jako parametr TValue. Listing 11.26. Ograniczenie wymagające dostępności konstruktora domyślnego public class EntityBase TKey { public TKey Key { get; set; } } public class EntityDictionary TKey, TValue : Dictionary TKey, TValue where TKey: IComparable TKey , IFormattable where TValue : EntityBase TKey , new() { // … public TValue MakeValue(TKey key) { TValue newEntity = new TValue(); newEntity.Key = key; Add(newEntity.Key, newEntity); return newEntity; } // … } Ponieważ nie wszystkie obiekty mają publiczne konstruktory domyślne, kompilator nie pozwala na wywołanie konstruktora domyślnego typu podanego jako parametr, jeśli nie ustawiono odpowiedniego ograniczenia. Aby wyeliminować tę regułę kompilatora, należy dodać słowo new() po wszystkich pozostałych ograniczeniach. To słowo jest ograniczeniem dotyczącym konstruktora. Wskutek jego dodania typ podany jako parametr musi udostęp- niać publiczny konstruktor domyślny. Dodane ograniczenie może dotyczyć tylko konstruk- tora domyślnego. Nie da się utworzyć ograniczenia zapewniającego, że podany typ udostępnia konstruktor przyjmujący parametry formalne. 2.0 Kup książkęPoleć książkę Ograniczenia 437 Ograniczenia dotyczące dziedziczenia Ani parametry typu generycznego, ani ich ograniczenia nie są dziedziczone w klasach pochod- nych. Wynika to z tego, że parametry typu generycznego nie są jego składowymi. Pamiętaj, że dziedziczenie klas polega na tym, iż w klasie pochodnej znajdują się wszystkie składowe klasy bazowej. Często stosuje się technikę polegającą na tworzeniu nowych typów generycz- nych pochodnych od innych typów generycznych. W takiej sytuacji parametry określające typ w pochodnym typie generycznym są używane jako parametry określające typ w generycznej klasie bazowej. Dlatego w klasie pochodnej te parametry muszą mieć takie same (lub mocniej- sze) ograniczenia jak w klasie bazowej. Czujesz się zagubiony? Przyjrzyj się listingowi 11.27. Listing 11.27. Jawnie podawane „odziedziczone” ograniczenia class EntityBase T where T : IComparable T { // … } // B(cid:224)(cid:260)D: // Mo(cid:298)liwa musi by(cid:252) konwersja typu U na typ // System.IComparable U , aby mo(cid:298)na by(cid:225)o poda(cid:252) ‘U’ jako // parametr T w generycznym typie lub w generycznej metodzie. // class Entity U : EntityBase U // { // … // } Na listingu 11.27 klasa EntityBase T wymaga, by podany jako argument typ U (używany jako parametr T w wyniku deklaracji klasy bazowej EntityBase U ) zawierał implementację interfejsu IComparable U . Dlatego w klasie Entity U trzeba zastosować to samo ogranicze- nie do U. W przeciwnym razie wystąpi błąd kompilacji. Ten wzorzec zwiększa świadomość programisty i uwidacznia ograniczenia z klasy bazowej w klasie pochodnej. Pozwala to uniknąć niejasności, które mogą wystąpić, gdy programista używa klasy pochodnej i odkrywa ograni- czenie, ale nie rozumie, z czego ono wynika. Na razie nie omówiono w książce metod generycznych. Zapoznasz się z nimi w dalszej części rozdziału. Zapamiętaj tylko, że także metody mogą być generyczne i można w nich dodawać ograniczenia parametrów określających typ. Jak interpretowane są te ograniczenia, gdy wirtualna metoda generyczna jest dziedziczona lub przesłaniana? Inaczej niż w przypadku ograniczeń parametrów określających typ w klasie generycznej, ograniczenia w nowych wer- sjach wirtualnych metod generycznych (i w składowych z jawnie podawanym interfejsem) są dziedziczone niejawnie i nie można ich ponownie zadeklarować (zobacz listing 11.28). Listing 11.28. Powtórne dodawanie odziedziczonych ograniczeń składowych wirtualnych jest niedozwolone 2.0 class EntityBase { public virtual void Method T (T t) where T : IComparable T Kup książkęPoleć książkę 438 Rozdział 11. Typy generyczne { // … } } class Order : EntityBase { public override void Method T (T t) // Nie mo(cid:298)na powtórnie dodawa(cid:252) ogranicze(cid:276) w // nowych wersjach przes(cid:225)anianych sk(cid:225)adowych. // where T : IComparable T { // … } } W klasie pochodnej od klasy generycznej parametr określający typ można dodatkowo ograniczyć. Wystarczy obok (wymaganych) ograniczeń z klasy bazowej podać dodatkowe ogra- niczenia. Jednak nowa wersja przesłanianej wirtualnej metody generycznej musi być w pełni zgodna z ograniczeniami zdefiniowanymi w wersji metody z klasy bazowej. Dodatkowe ograni- czenia mogą naruszać polimorfizm, dlatego nie są dozwolone. W nowej wersji przesłanianej metody niejawnie obowiązują ograniczenia parametru określającego typ z wersji z klasy bazowej. ZAGADNIENIE DLA ZAAWANSOWANYCH Wymogi związane z ograniczeniami W stosunku do ograniczeń obowiązują wymogi chroniące przed powstawaniem bezsensow- nego kodu. Na przykład nie można łączyć ograniczenia dotyczącego klasy z ograniczeniami struct i class. Ponadto nie można utworzyć ograniczenia wymagającego użycia typu pochod- nego od jednego z typów specjalnych, takich jak object, typy tablicowe, System.ValueType, System.Enum (i typy wyliczeniowe), System.Delegate lub System.MulticastDelegate. W niektórych sytuacjach przydatne byłoby wprowadzenie dodatkowych reguł związanych z ograniczeniami. W przedstawionych dalej podrozdziałach znajdziesz przykłady niedozwo- lonych ograniczeń. Ograniczenia dotyczące operatorów są niedozwolone Nie można utworzyć ograniczenia parametru określającego typ, które wymagałoby implemen- tacji konkretnej metody lub danego operatora. Można to zrobić wyłącznie za pomocą ogra- niczenia dotyczącego interfejsu (w przypadku metod) lub ograniczenia dotyczącego klasy (dla metod i operatorów). Dlatego generyczna metoda Add() z listingu 11.29 nie zadziała. Listing 11.29. W ograniczeniu nie można dodać wymogu dostępności operatorów 2.0 public abstract class MathEx T { public static T Add(T first, T second) { // B(cid:224)(cid:260)D: Operator + nie mo(cid:298)e zosta(cid:252) // u(cid:298)yty do operandów typów T i T . Kup książkęPoleć książkę // return first + second; } } Ograniczenia 439 W metodzie przyjęto, że operator + jest dostępny we wszystkich typach, które mogą zostać podane jako parametr T. Nie istnieje jednak ograniczenie, które pozwala zapobiec podaniu typu bez operatora dodawania. Dlatego występuje błąd. Niestety, nie można utworzyć ogra- niczenia, które wymaga dostępności operatora dodawania. Jedyne rozwiązanie to zastosowanie ograniczenia dotyczącego klasy i zażądanie klasy z implementacją operatora dodawania. Można więc uogólnić i stwierdzić, że nie ma sposobu na ograniczenie dozwolonych typów do tych z potrzebną metodą statyczną. Relacja LUB między ograniczeniami nie jest obsługiwana Jeśli podasz kilka ograniczeń dotyczących interfejsu lub klasy, kompilator zawsze przyjmie, że występuje między nimi relacja I (czyli że wszystkie ograniczenia muszą być spełnione). Na przykład ograniczenie where T : IComparable T , IFormattable wymaga, by zaimple- mentowane były interfejsy IComparable T i IFormattable. Nie da się zapisać relacji LUB między ograniczeniami, dlatego kod z listingu 11.30 jest niedozwolony. Listing 11.30. Łączenie ograniczeń za pomocą relacji LUB nie jest dozwolone public class BinaryTree T // B(cid:224)(cid:260)D: relacja LUB nie jest obs(cid:225)ugiwana. // where T: System.IComparable T || System.IFormattable { ... } 2.0 Dodanie obsługi tego mechanizmu uniemożliwiłoby kompilatorowi określenie na etapie kompilacji, którą metodę należy wywołać. Ograniczenia dotyczące delegatów i wyliczeń są niedozwolone Typy delegatów, typy tablicowe i typy wyliczeniowe nie mogą być używane w ograniczeniach, ponieważ działają jak typy zamknięte (jeśli nie wiesz, czym są typy delegatów, zajrzyj do roz- działu 12.). Typy bazowe wymienionych konstrukcji (System.Delegate, System.MulticastDele (cid:180)gate, System.Array i System.Enum) też nie mogą być używane jako ograniczenia. Dlatego gdy kompilator natrafi na deklarację klasy przedstawioną na listingu 11.31, zgłosi błąd. Listing 11.31. W ograniczeniu dotyczącym dziedziczenia nie można używać typu System.Delegate // B(cid:224)(cid:260)D: w ograniczeniu nie mo(cid:298)na u(cid:298)ywa(cid:252) specjalnej klasy System.Delegate . public class Publisher T where T : System.Delegate { public event T Event; public void Publish() { if (Event != null) { Event(this, new EventArgs()); Kup książkęPoleć książkę 440 Rozdział 11. Typy generyczne } } } Wszystkie typy delegatów są uznawane za klasy specjalne, których nie można pod
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

C# 6.0. Kompletny przewodnik dla praktyków. Wydanie V
Autor:
,

Opinie na temat publikacji:


Inne popularne pozycje z tej kategorii:


Czytaj również:


Prowadzisz stronę lub blog? Wstaw link do fragmentu tej książki i współpracuj z Cyfroteką: