Cyfroteka.pl

klikaj i czytaj online

Cyfro
Czytomierz
00000 003795 19001233 na godz. na dobę w sumie
C# 7.0. Kompletny przewodnik dla praktyków. Wydanie VI - książka
C# 7.0. Kompletny przewodnik dla praktyków. Wydanie VI - książka
Autor: Liczba stron: 840
Wydawca: Helion Język publikacji: polski
ISBN: 978-83-283-5780-8 Data wydania:
Lektor:
Kategoria: ebooki >> komputery i informatyka >> programowanie >> c# - programowanie
Porównaj ceny (książka, ebook (-35%), audiobook).

C# jest jednym z lepiej dopracowanych języków programowania. Cechują go dojrzałość, prostota, nowoczesność i bezpieczeństwo. Został od podstaw zaprojektowany jako obiektowy. Stanowi integralną część platformy Microsoft .NET Framework. Jest ulubionym narzędziem profesjonalnych programistów, którym zależy na pisaniu kodu bezpiecznego, przejrzystego, wydajnego i prostego w konserwacji. W wersji 7.0 tego języka pojawiły się nowe usprawnienia, dzięki którym programowanie stało się jeszcze bardziej efektywne i satysfakcjonujące.

Ta książka jest szóstym, zaktualizowanym i uzupełnionym wydaniem jednego z najlepszych podręczników programowania. Poza znakomitym kompendium języka C# znalazły się tu informacje o poszczególnych metodykach programowania, od sekwencyjnego aż po podstawy programowania deklaratywnego z wykorzystaniem atrybutów. Szczegółowo przedstawiono funkcje wprowadzone do wersji 7.0 języka, a także w platformie .NET Framework 4.7/.NET Core 2.0. Książka jest też wygodnym źródłem wiedzy o pewnych rzadko stosowanych konstrukcjach składniowych, specyficznych szczegółach i subtelnościach języka C#. Jasny i przejrzysty sposób prezentowania treści pozwoli na szybkie zrozumienie nawet najbardziej zawiłych zagadnień.

W tej książce między innymi:

C#. Nowoczesny, elegancki, bezpieczny!

Znajdź podobne książki Ostatnio czytane w tej kategorii

Darmowy fragment publikacji:

Tytuł oryginału: Essential C# 7.0 (6th Edition) Tłumaczenie: Tomasz Walczak ISBN: 978-83-283-5780-8 Authorized translation from the English language edition, entitled ESSENTIAL C# 7.0, 6th Edition; by MICHAELIS, MARK; , published by Pearson Education, Inc, publishing as Addison-Wesley Professional. Copyright © 2018 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 S.A. Copyright © 2019. 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 Helion SA 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 Helion SA nie ponoszą również żadnej odpowiedzialności za ewentualne szkody wynikłe z wykorzystania informacji zawartych w książce. Helion SA 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/c7kop6 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/c7kop6.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 autorze 29 1. Wprowadzenie do języka C# 31 Witaj, świecie 32 Podstawy składni języka C# 40 Korzystanie ze zmiennych 47 Dane wejściowe i wyjściowe w konsoli 51 Wykonywanie kodu w środowisku zarządzanym i platforma CLI 57 Różne wersje platformy .NET 62 Podsumowanie 64 2. 3. Typy danych 65 Podstawowe typy liczbowe 65 Inne podstawowe typy 73 Wartości null i void 85 Konwersje typów danych 86 Podsumowanie 92 Jeszcze o typach danych 93 Kategorie typów 93 Modyfikator umożliwiający stosowanie wartości null 96 Krotki 98 Tablice 104 Podsumowanie 118 Poleć książkęKup książkę 6 Spis treści 4. Operatory i przepływ sterowania 119 Operatory 120 Zarządzanie przepływem sterowania 133 Bloki kodu ({}) 138 Bloki kodu, zasięgi i przestrzenie deklaracji 140 Wyrażenia logiczne 142 Operatory bitowe ( , , |, , ^, ~) 150 Instrukcje związane z przepływem sterowania — ciąg dalszy 155 Instrukcje skoku 165 Dyrektywy preprocesora języka C# 170 Podsumowanie 177 5. Metody i parametry 179 Wywoływanie metody 180 Deklarowanie metody 185 Dyrektywa using 190 Zwracane wartości i parametry metody Main() 195 Zaawansowane parametry metod 197 Rekurencja 207 Przeciążanie metod 209 Parametry opcjonalne 212 Podstawowa obsługa błędów z wykorzystaniem wyjątków 216 Podsumowanie 227 6. Klasy 229 Deklarowanie klasy i tworzenie jej instancji 232 Pola instancji 235 Metody instancji 237 Stosowanie słowa kluczowego this 238 Modyfikatory dostępu 244 Właściwości 246 Konstruktory 260 Składowe statyczne 269 Metody rozszerzające 277 Hermetyzacja danych 278 Klasy zagnieżdżone 281 Klasy częściowe 283 Podsumowanie 287 7. Dziedziczenie 289 Tworzenie klas pochodnych 290 Przesłanianie składowych z klas bazowych 300 Klasy abstrakcyjne 310 Wszystkie klasy są pochodne od System.Object 315 Poleć książkęKup książkę Spis treści 7 Sprawdzanie typu za pomocą operatora is 316 Dopasowanie do wzorca z użyciem operatora is 317 Dopasowanie do wzorca w instrukcji switch 318 Konwersja z wykorzystaniem operatora as 320 Podsumowanie 321 8. Interfejsy 323 Wprowadzenie do interfejsów 324 Polimorfizm oparty na interfejsach 325 Implementacja interfejsu 329 Przekształcanie między klasą z implementacją i interfejsami 334 Dziedziczenie interfejsów 335 Dziedziczenie po wielu interfejsach 337 Metody rozszerzające i interfejsy 337 Implementowanie wielodziedziczenia za pomocą interfejsów 339 Zarządzanie wersjami 341 Interfejsy a klasy 343 Interfejsy a atrybuty 344 Podsumowanie 345 9. Typy bezpośrednie 347 Struktury 351 Opakowywanie 356 Wyliczenia 363 Podsumowanie 373 10. Dobrze uformowane typy 375 Przesłanianie składowych z klasy object 375 Przeciążanie operatorów 387 Wskazywanie innych podzespołów 394 Definiowanie przestrzeni nazw 402 Komentarze XML-owe 405 Odzyskiwanie pamięci 409 Porządkowanie zasobów 411 Leniwe inicjowanie 418 Podsumowanie 420 11. Obsługa wyjątków 421 Wiele typów wyjątków 421 Przechwytywanie wyjątków 424 Ogólny blok catch 427 Wskazówki związane z obsługą wyjątków 429 Definiowanie niestandardowych wyjątków 433 Ponowne zgłaszanie opakowanego wyjątku 435 Podsumowanie 439 Poleć książkęKup książkę 8 Spis treści 12. Typy generyczne 441 Język C# bez typów generycznych 442 Wprowadzenie do typów generycznych 446 Ograniczenia 457 Metody generyczne 468 Kowariancja i kontrawariancja 472 Wewnętrzne mechanizmy typów generycznych 479 Podsumowanie 482 13. Delegaty i wyrażenia lambda 483 Wprowadzenie do delegatów 484 Deklarowanie typu delegata 487 Wyrażenia lambda 494 Metody anonimowe 499 Podsumowanie 513 14. Zdarzenia 515 Implementacja wzorca publikuj-subskrybuj za pomocą delegatów typu multicast 516 Zdarzenia 528 Podsumowanie 538 15. Interfejsy kolekcji ze standardowymi operatorami kwerend 539 Inicjatory kolekcji 540 Interfejs IEnumerable T sprawia, że klasa staje się kolekcją 542 Standardowe operatory kwerend 547 Typy anonimowe w technologii LINQ 576 Podsumowanie 583 16. Technologia LINQ i wyrażenia z kwerendami 585 Wprowadzenie do wyrażeń z kwerendami 586 Wyrażenia z kwerendą to tylko wywołania metod 601 Podsumowanie 603 17. Tworzenie niestandardowych kolekcji 605 Inne interfejsy implementowane w kolekcjach 606 Podstawowe klasy kolekcji 608 Udostępnianie indeksera 623 Zwracanie wartości null lub pustej kolekcji 626 Iteratory 627 Podsumowanie 639 18. Refleksja, atrybuty i programowanie dynamiczne 641 Mechanizm refleksji 642 Operator nameof 651 Atrybuty 652 Programowanie z wykorzystaniem obiektów dynamicznych 672 Podsumowanie 682 Poleć książkęKup książkę Spis treści 9 19. Wielowątkowość 683 Podstawy wielowątkowości 685 Używanie klasy System.Threading 691 Zadania asynchroniczne 698 Anulowanie zadania 715 Wzorzec obsługi asynchroniczności za pomocą zadań 720 Równoległe wykonywanie iteracji pętli 746 Równoległe wykonywanie kwerend LINQ 754 Podsumowanie 759 20. Synchronizowanie wątków 761 Po co stosować synchronizację? 762 Zegary 786 Podsumowanie 788 21. Współdziałanie między platformami i niezabezpieczony kod 789 Mechanizm P/Invoke 790 Wskaźniki i adresy 801 Wykonywanie niezabezpieczonego kodu za pomocą delegata 809 Podsumowanie 810 22. Standard CLI 811 Definiowanie standardu CLI 811 Implementacje standardu CLI 812 Specyfikacja .NET Standard 815 Biblioteka BCL 815 Kompilacja kodu w języku C# na kod maszynowy 816 Środowisko uruchomieniowe 818 Podzespoły, manifesty i moduły 821 Język Common Intermediate Language 823 Common Type System 824 Common Language Specification 825 Metadane 825 Architektura .NET Native i kompilacja AOT 826 Podsumowanie 826 Skorowidz 829 Poleć książkęKup książkę 10 Spis treści Poleć książkęKup książkę 12 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. Poleć książkęKup książkę 442 Rozdział 12. 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 12.1. Listing 12.1. Sygnatury metod klasy System.Collections.Stack public class Stack { public virtual object Pop() { ... } public virtual void Push(object obj) { ... } // … } 2.0 W programach stos często służy do umożliwiania wielokrotnego cofania operacji. Na przy- kład na listingu 12.2 kod używa klasy System.Collections.Stack do wycofywania operacji w programie symulującym działanie znikopisu. Listing 12.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. 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(); Poleć książkęKup książkę Język C# bez typów generycznych 443 } 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 12.2 znajdziesz w danych wyjściowych 12.1 DANE WYJŚCIOWE 12.1. 2.0 Poleć książkęKup książkę 444 Rozdział 12. Typy generyczne 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 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 12.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. 2.0 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 12.3. Listing 12.3. Definicja wyspecjalizowanej wersji klasy Stack public class CellStack { public virtual Cell Pop(); public virtual void Push(Cell cell); // … } Poleć książkęKup książkę Język C# bez typów generycznych 445 Ponieważ klasa CellStack może przechowywać tylko obiekty typu Cell, to rozwiązanie wymaga dodania niestandardowej implementacji metod potrzebnych do obsługi stosu, co 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 3. 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 12.4. 2.0 Listing 12.4. Deklarowanie wersji różnych typów bezpośrednich z dodaną obsługą wartości null 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; } ... } ... Poleć książkęKup książkę 446 Rozdział 12. Typy generyczne Na listingu 12.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ę 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 12.5. Listing 12.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; } 2.0 /// 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 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. Poleć książkęKup książkę 2.0 Wprowadzenie do typów generycznych 447 Używanie klasy generycznej Na listingu 12.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 12.6 pokazano ten proces na przykładzie nowej generycznej klasy Stack. Listing 12.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; Poleć książkęKup książkę 448 Rozdział 12. 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 12.6 pokazano w danych wyjściowych 12.2. DANE WYJŚIOWE 12.2. 2.0 Na listingu 12.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 12.7 tworzona jest klasa Stack T , podobna do klasy System.Collections.Generic.Stack T użytej na listingu 12.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 12.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(). Poleć książkęKup książkę Wprowadzenie do typów generycznych 449 Listing 12.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; } 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 12.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. Poleć książkęKup książkę 450 Rozdział 12. Typy generyczne 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. 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. 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 EntityCollection 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. 2.0 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 12.8. Listing 12.8. Deklarowanie generycznego interfejsu interface IPair T { T First { get; set; } T Second { get; set; } } Poleć książkęKup książkę Wprowadzenie do typów generycznych 451 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. 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 12.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 12.9. Implementowanie generycznego interfejsu public struct Pair T : IPair T { public T First { get; set; } public T Second { get; set; } } 2.0 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 12.10. Listing 12.10. Wielokrotne implementowanie interfejsu w jednej klasie public interface IContainer T { ICollection T Items { get; set; } } public class Person: IContainer Address , IContainer Phone , IContainer Email { ICollection Address IContainer Address .Items { get{...} set{...} } Poleć książkęKup książkę 452 Rozdział 12. Typy generyczne ICollection Phone IContainer Phone .Items { get{...} set{...} } ICollection Email IContainer Email .Items { get{...} set{...} } } 2.0 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. 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 12.11 konstruktor jest zadeklarowany z sygnaturą public Pair(T first, T second). Listing 12.11. Deklarowanie konstruktora typu generycznego public struct Pair T : IPair T { public Pair(T first, T second) { First = first; Second = second; } Poleć książkęKup książkę Wprowadzenie do typów generycznych 453 public T First { get; set; } public T Second { get; set; } } Określanie wartości domyślnej Na listingu 12.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 i automatycznie implementowane właściwości. Prowadzi to jednak do problemu. Pomyśl o konstruktorze z typu Pair T , który w trakcie two- rzenia obiektu inicjuje tylko jeden element z pary. Zdefiniowanie takiego konstruktora, pokazanego na listingu 12.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. 2.0 Listing 12.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; // } // … } Aby umożliwić rozwiązanie tego problemu, w języku C# udostępniono operator default, opisany wcześniej w rozdziale 9., 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 12.13. Listing 12.13. Inicjowanie pola za pomocą operatora default public struct Pair T : IPair T { public Pair(T first) { First = first; Second = default(T); } // … } Poleć książkęKup książkę 454 Rozdział 12. Typy generyczne Operator default pozwala podać wartość domyślną dowolnego typu (także podanego za pomocą parametru). W C# 7.1 wprowadzono możliwość używania operatora default bez podawania para- metru, jeśli możliwe jest wywnioskowanie typu danych. Na przykład przy inicjowaniu lub przypisywaniu wartości zmiennej można zastosować składnię Pair T pair = default za- miast Pair T pair = default(Pair T ). Ponadto jeśli metoda zwraca wartość typu int, można zastosować zapis return default, a kompilator wywnioskuje wywołanie default(int) na podstawie typu zwracanej wartości. Takie wnioskowanie jest możliwe także dla (opcjo- nalnych) parametrów domyślnych i argumentów wywołań metod. Wiele parametrów określających typ 2.0 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 12.14. Listing 12.14. Deklarowanie typu generycznego z kilkoma parametrami określającymi typ interface IPair TFirst, TSecond { TFirst First { get; set; } TSecond Second { get; set; } } 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 12.15. Listing 12.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); Poleć książkęKup książkę Wprowadzenie do typów generycznych 455 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. ZAGADNIENIE DLA POCZĄTKUJĄCYCH Krotki — typy o różnej arności W rozdziale 3. opisano obsługę składni dla krotek z wersji C# 7.0. Wewnętrznie typ używany na potrzeby tej składni jest typem generycznym System.ValueTuple. Podobnie jak przy stosowaniu typu Pair … można wielokrotnie wykorzystać tę samą nazwę dzięki różnym arnościom (w każ- dej klasie używana jest inna liczba parametrów określających typ), co pokazano na listingu 12.16. Początek 7.0 2.0 Listing 12.16. Przesłanianie definicji typów na podstawie arności public class ValueTuple { ... } public class ValueTuple T1 : IStructuralEquatable, IStructuralComparable, IComparable {...} public class ValueTuple T1, T2 : ... {...} public class ValueTuple T1, T2, T3 : ... {...} public class ValueTuple T1, T2, T3, T4 : ... {...} public class ValueTuple T1, T2, T3, T4, T5 : ... {...} public class ValueTuple T1, T2, T3, T4, T5, T6 : ... {...} public class ValueTuple T1, T2, T3, T4, T5, T6, T7 : ... {...} public class ValueTuple T1, T2, T3, T4, T5, T6, T7, TRest : ... {...} Rodzinę klas ValueTuple … zaprojektowano w tym samym celu co klasy Pair T i Pa- ir TFirst, TSecond , przy czym klasy ValueTuple … obsługują do ośmiu argumentów okre- ślających typ. W ostatniej wersji klasy ValueTuple z listingu 12.16 parametr TRest można wykorzystać do zapisania następnego obiektu typu ValueTuple. Dlatego liczba elementów w krotce jest potencjalnie nieskończona. Jeśli zdefiniujesz krotki za pomocą wprowadzonej w C# 7.0 składni dla krotek, kompilator wygeneruje obiekty tego typu. Inną ciekawą składową rodziny klas reprezentujących krotki jest niegeneryczna klasa ValueTuple. Ta klasa ma osiem statycznych metod fabrycznych, które służą do tworzenia obiektów różnych generycznych typów Tuple. Choć każdy typ generyczny umożliwia bezpo- średnie tworzenie obiektów za pomocą konstruktora, metody fabryczne z klasy Tuple au- tomatycznie wykrywają typy argumentów, gdy wywoływana jest metoda Create(). W C# 7.0 nie ma to znaczenia, ponieważ kod jest bardzo prosty: var keyValuePair = ( 555-55-5555 , new Contact( Inigo Montoya )) (przy założeniu, że nie są używane elementy nazwane). Jednak, co pokazano na listingu 12.17, stosowanie metody Create() w połączeniu z inferencją typów upraszcza pracę w C# 6.0. Poleć książkęKup książkę 456 Rozdział 12. Typy generyczne Listing 12.17. Porównanie sposobów tworzenia instancji typu System.ValueTuple #if !PRECSHARP7 (string, Contact) keyValuePair; keyValuePair = 555-55-5555 , new Contact( Inigo Montoya )); #else // W wersjach starszych ni(cid:298) C# 7.0 nale(cid:298)y stosowa(cid:252) sk(cid:225)adni(cid:266) System.ValueTupe string,Contact . Tuple string, Contact keyValuePair; keyValuePair = ValueTuple.Create( 555-55-5555 , new Contact( Inigo Montoya )); keyValuePair = new ValueTuple string, Contact ( 555-55-5555 , new Contact( Inigo Montoya )); #endif // !PRECSHARP7 2.0 Początek 4.0 Koniec 4.0 Koniec 7.0 Gdy liczba elementów w obiektach typu ValueTuple jest duża, podawanie wszystkich ar- gumentów określających typ jest kłopotliwe. Łatwiej zastosować wówczas metody fabryczne Create(). Warto zauważyć, że podobną klasę reprezentującą krotki, System.Tuple, dodano w C# 4.0. Stwierdzono jednak, że powszechne używanie składni dla krotek wprowadzonej w C# 7.0 i wynikająca z tego wszechobecność krotek uzasadniają utworzenie typu System.ValueTuple, ponieważ pozwala on zwiększyć wydajność. Na podstawie tego, że w bibliotece platformy zadeklarowanych jest osiem różnych gene- rycznych typów ValueTuple, można się domyślić, iż w systemie plików środowiska CLR nie są obsługiwane typy generyczne o różnej liczbie parametrów. Metody mogą przyjmować do- wolną liczbę argumentów za pomocą tablic z parametrami, nie istnieje jednak analogiczna technika dla typów generycznych. Każdy typ generyczny musi mieć ściśle określoną arność. W zagadnieniu dla początkujących „Krotki — typy o różnej arności” znajdziesz przykład ilustrujący to zagadnienie. Zagnieżdżone typy generyczne 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 12.18). Listing 12.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. Poleć książkęKup książkę Ograniczenia 457 // 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. 2.0 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 12.19. Listing 12.19. Deklaracja klasy BinaryTree T bez ograniczeń public class BinaryTree T { public BinaryTree ( T item) { Item = item; } 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 12.20. Listing 12.20. Do działania interfejsu potrzebny jest parametr określający typ public class BinaryTree T { public T Item { get; set; } Poleć książkęKup książkę 458 Rozdział 12. Typy generyczne 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 12.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 12.21. Listing 12.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. ... } Poleć książkęKup książkę Ograniczenia 459 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; } 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. 2.0 Ograniczenia dotyczące interfejsu Aby zapewnić właściwy porządek węzłów w drzewie binarnym, można wykorzystać metodę CompareTo() 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 12.22. Listing 12.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. ... Poleć książkęKup książkę 460 Rozdział 12. Typy generyczne } 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 Po dodaniu na listingu 12.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 ograniczeniach. 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 12.3. DANE WYJŚCIOWE 12.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 . 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 12.23. Listing 12.23. Deklarowanie ograniczenia dotyczącego klasy public class EntityDictionary TKey, TValue : System.Collections.Generic.Dictionary TKey, TValue where TValue : EntityBase { ... } Poleć książkęKup książkę 2.0 Ograniczenia 461 Na listingu 12.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- ś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 12.24. Listing 12.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 { // … } 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. Poleć książkęKup książkę 462 Rozdział 12. Typy generyczne 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ą im- plementowane 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ń 2.0 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 12.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 12.25. Ustawianie wielu ograniczeń public class EntityDictionary TKey, TValue : Dictionary TKey, TValue where TKey : IComparable TKey , IFormattable where TValue : EntityBase { ... } W tym kodzie ustawianych jest kilka ograniczeń parametru TKey i dodatkowe ograniczenie pa- rametru 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 12.26 metoda MakeValue() klasy EntityDictionary TKey, TValue musi tworzyć obiekt typu podanego jako parametr TValue. Poleć książkęKup książkę 2.0 Listing 12.26. Ograniczenie wymagające dostępności konstruktora domyślnego Ograniczenia 463 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. 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 12.27. Listing 12.27. Jawnie podawane „odziedziczone” ograniczenia class EntityBase T where T : IComparable T { // … } Poleć książkęKup książkę 464 Rozdział 12. Typy generyczne // 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 // { // … // } 2.0 Na listingu 12.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 12.28). Listing 12.28. Powtórne dodawanie odziedziczonych ograniczeń składowych wirtualnych jest niedozwolone class EntityBase { public virtual void Method T (T t) where T : IComparable T { // … } } 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 Poleć książkęKup książkę Ograniczenia 465 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 12.29 nie zadziała. Listing 12.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 . // return first + second; } } 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 kla
Pobierz darmowy fragment (pdf)

Gdzie kupić całą publikację:

C# 7.0. Kompletny przewodnik dla praktyków. Wydanie VI
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ą: