Ian Griffiths, Matthew Adams, Jesse Liberty - C#. Programowanie. Wydanie VI (2012) [SHARP]

839 Pages • 381,604 Words • PDF • 16.2 MB
Uploaded at 2021-09-24 17:56

This document was submitted by our user and they confirm that they have the consent to share it. Assuming that you are writer or own the copyright of this document, report to us by using this DMCA report button.


I

. ajleps.; ) ·podręc

O REILLY®

u.ik: po 'ił tęco1�l

Jan

�iallh U' i $·"' ,J '

r(f.litlts.

da1us.

i*b rtJ · •

Tytuł oryginału: Programming C# 4.0: Building Windows, Web, and RIA Applications for the .NET 4.0 Framework Tłumaczenie: Piotr Rajca (wstęp, rozdz. 1 - 11, 16, 19 - 22t Łukasz Suma (rozdz. 12 - 15, 17, 18) ISBN: 978-83-246-5694-3 ©Helion 2012 All rights reserved Authorized Polish translation of the English edition of Programming C# 4.0, 61h Edition 9780596159832 © 2010 Ian Griffiths, Matthew Adams This translation is published and sold by permission of O'Reilly Media, Inc., which owns or controls all rights to publish and sell the same. All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanicat including photocopying, recording or by any information storage retrieval system, without permission from the Publisher. 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. Pliki z przykładami omawianymi w książce można znaleźć pod adresem: ftp://ftp.helion.pl/przyklady/cshpr6.zip Wydawnictwo HELION ul. Kościuszki le, 44-100 GLIWICE tel. 32 231 22 19, 32 230 98 63 e-mail: [email protected] 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/cshpr6_ebook Możesz tam wpisać swoje uwagi, spostrzeżenia, recenzję.

Printed in Poland . • • •

Kup w wersji papierowej

Poleć książkę na Facebook.com

Oceń książkę



Księgarnia internetowa



Lubię to!» Nasza społeczność

Spis treści

Wstęp

......................................................................................................................................

1. Prezentacja C#

.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„..„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„„.

13 19

Dlaczego C#? Dlaczego .NET? Biblioteka klas platformy .NET

20

Styl języka Łatwość konstruowania oprogramowania Kod zarządzany Ciągłość i „ekosystem" Windows C# 4 .0, .NET 4 .0 oraz Visual Studio 2010

21 22 23 24 25

Podsumowanie

27

2. Podstawowe techniki programowania

19

„„„„„„„„„„„„„„„„„„„„„„„.„„„„„„„„„„„.

29

Początki

29

Przestrzenie nazw i typy Projekty i solucje

32

Komentarze, regiony oraz czytelność Nieprawidłowe komentarze Komentarze dokumentujące XML Zmienne Typy zmiennych Wyrażenia i instrukcje Instrukcje przypisania Operatory inkrementacji i dekrementacji Instrukcje sterowania przepływem i wyboru Instrukcje if Instrukcje switch oraz case Instrukcje iteracji Instrukcje foreach Instrukcje for Instrukcje while oraz do Przerywanie wykonywania pętli

42 43 44 45 46 52 55 55 56 58 62 64 65 67 69 70

37

3

Metody

71

Podsumowanie

74

3. Wyodrębnianie idei przy wykorzystan iu klas i struktur

„„„„„„„„„„„„„„„„„„„„„„

Dziel i rządź Wyodrębnianie idei w formie metod Wyodrębnianie idei przy użyciu obiektów i klas Definiowanie klas Reprezentowanie stanu przy użyciu właściwości Poziomy ochrony Inicjalizacja przy użyciu konstruktora Pola: miejsca do zapisywania danych Pola mogą się zmieniać, lecz stałe nie Pola i właściwości tylko do odczytu Typ enum - powiązane ze sobą stałe Typy wartościowe i referencyjne

77 77 80 81 82 84 86 90 92 93 96 100

Zbyt wiele konstruktorów, Panie Mozart Przeciążanie Metody przeciążone oraz domyślne parametry nazwane Inicjalizatory obiektów

105 106 108

Definiowanie metod Deklarowanie metod statycznych Pola i właściwości statyczne Konstruktory statyczne Podsumowanie

112 115 116 117 119

4. Rozszerzalność i pol imorfizm

4

77

. „ . „ . „ . „ . „ . „ „ . „ . „ . „ .. „ „ . „ . „ . „ . „ . „ . „ . „ . „ . „ . „ . „ . „ . „ . „ . „ . „ .

105

121

Tworzenie asocjacji poprzez kompozycję i agregację

122

Dziedziczenie i polimorfizm Zastępowanie metod w klasach pochodnych Ukrywanie składowych klasy bazowej przy użyciu new Zastępowanie metod przy użyciu modyfikatorów virtual i override Dziedziczenie i ochrona

124 126 127 129 132

Wywoływanie metod klasy bazowej Dotąd i ani kroku dalej: modyfikator sealed

136

Wymuszanie przesłaniania - metody abstrakcyjne

138

Wszystkie typy dziedziczą po klasie Object Pakowanie i rozpakowywanie typów wartościowych C# nie obsługuje wielokrotnego dziedziczenia implementacji C# obsługuje wielokrotne dziedziczenie interfejsów Tworzenie jednych interfejsów na bazie innych Jawna implementacja interfejsów Ostateczne rozwiązanie: sprawdzanie typów podczas wykonywania programu Podsumowanie

144 144 149

Spis treści

134

149 152 153 157 158

5. Delegacje - łatwość komponowania i rozszerzal ność

„„„„„„„„„„„„„„„„„„„„„

159

Kompozycja funkcyjna wykorzystująca delegacje Typ Action - akcje ogólne Predicate - predykaty ogólne

166

Stosowanie metod anonimowych Tworzenie delegacji przy użyciu wyrażeń lambda Delegacje we właściwościach Ogólne delegacje do funkcji Informowanie klientów za pomocą zdarzeń Udostępnianie dużej liczby zdarzeń Podsumowanie

177

6. Obsługa błędów

172 175 178 180 182 186 194 197

„.„.„.„.„.„.„.„.„.„.. „.„.„.„.„.„.„. „.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„

.. 199

Kiedy i jak uznać niepowodzenie Zwracanie kodu błędu Debugowanie wartości zwracanych Wyjątki Obsługa wyjątków Kiedy są wykonywane bloki finally? Określanie, jakie wyjątki będą przechwytywane Wyjątki niestandardowe Podsumowanie

7. Tabl ice i l isty

204 207 213 214 219 226 227 230 232

„.„.„.„.„.„.„.„.„.. „.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„„„.

Tablice Tworzenie i inicjalizacja Własne typy w tablicach Składowe tablic Wielkość tablic List Niestandardowe indeksatory Wyszukiwanie i sortowanie Kolekcje i polimorfizm Tworzenie własnych implementacji IEnumerable Podsumowanie

8. LINQ

233 233 234 237 242 247 254 257 264 264 268 274

.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.....„.„.„.„.„.„.„.„.„.„.„.„.„.„.„„.„.

Wyrażenia zapytań Wyrażenia zapytań a wywołania metod Metody rozszerzeń a LINQ Klauzule let Koncepcje i techniki LINQ Delegacje i wyrażenia lambda Styl funkcyjny i kompozycja Wykonywanie opóźnione

275 275 277 278 280 281 281 283 284

Spis treści

5

Operatory LINQ Filtrowanie Porządkowanie Konkatenacja Grupowanie Projekcje Spinanie Robimy się wybredni Testowanie całej kolekcji Agregacja Operacje na zbiorach Łączenie Konwersje Podsumowanie

9. Klasy kolekcji

.„.„.„.„.„••„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„••„.„.„.„„.

307

Słowniki Popularne zastosowania słowników IDictionary Słowniki i LINQ HashSet oraz SortedSet

307 309 315 317 318

Kolejki Listy połączone

319

Stosy Podsumowanie

321

10. Łańcuchy znaków

320 322 .„.„.„.„.„.„.„.„.„.„.„.„.„.„.„••„. „.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„••

Czym są łańcuchy znaków? Typy String i Char

6

285 285 286 289 289 291 298 299 300 302 304 304 305 306

323 324 325

Literały łańcuchowe i znakowe Oznaczanie znaków specjalnych Formatowanie wyświetlanych danych Standardowe łańcuchy formatowania liczb Niestandardowe łańcuchy formatujące Daty i godziny W drugą stronę - konwersja łańcuchów na dane innych typów Złożone formatowanie przy użyciu metody String.Format Wrażliwość na ustawienia kulturowe Poznawanie reguł formatowania Uzyskiwanie dostępu do znaków na podstawie indeksów Łańcuchy znaków są niezmienne

326 327 330 331 337 340 343 345 346 348 349

Pobieranie ciągu znaków

351

Składanie łańcuchów znaków Ponowne dzielenie łańcuchów Wielkie i małe li tery

352 354 355

Spis treści

349

Operacje na tekście StringBuilder - modyfikowalne łańcuchy znaków Odnajdywanie i zastępowanie łańcuchów Wszelkiego typu „puste" łańcuchy znaków

356 357 361

Usuwanie białych znaków

365

Sprawdzanie typu znaków

368

Kodowanie znaków Dlaczego kodowanie ma znaczenie Kodowanie i dekodowanie Po co reprezentować łańcuchy w formie sekwencji bajtów? Podsumowanie

368 370 371 378 378

11. Pliki i strumienie

362

„.„••„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„••„.„.„.„.„.„.

379

Sprawdzanie katalogów i plików

379

Badanie katalogów

382

Operacje na ścieżkach Ścieżka i aktualny katalog roboczy Zdobywanie informacji o pliku

383 384 385

Tworzenie plików tymczasowych

388

Usuwanie plików

389

Powszechnie znane katalogi

390

Bezpieczne łączenie elementów ścieżek

393

Tworzenie i zabezpieczanie hierarchii katalogów Usuwanie katalogu

394 401

Zapis plików tekstowych Zapis całego pliku tekstowego w jednym wywołaniu Zapis tekstu przy użyciu klasy StreamWriter Gdy pliki schodzą na złą drogę: obsługa wyjątków Określanie i modyfikacja uprawnień Wczytywanie plików do pamięci

402 402 403 406 410 414

Strumienie Poruszanie się wewnątrz strumienia Zapis danych przy użyciu strumieni Odczyt, zapis i blokowanie plików

418 424 425 426

Konstruktory klasy FileStream Bufory strumieni Określanie uprawnień podczas tworzenia strumieni Opcje zaawansowane Asynchroniczne operacje na plikach

428 428 429 429 430

Mechanizm Is ola ted Storage Magazyny Zapis i odczyt tekstu Definicja izolowania

433 434 435 436

Spis treści

7

Zarządzanie magazynami użytkownika przy użyciu limitów 440 441 Zarządzanie magazynami Strumienie, które nie są plikami 444 447 Strumień adaptujący - CryptoStream Wszystko w pamięci - MemoryStream 448 Reprezentowanie danych binarnych jako tekstu przy użyciu kodowania Base64 449 Podsumowanie 452

12. XML

„.„.„.„.„..„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„..„.„.„.„.„.„.„.„„.„.

Podstawy XML (krótki przegląd) Elementy XHTML Litera „X" oznacza „rozszerzalny" (eXtensible)

453 453 455 456

Tworzenie dokumentów XML Elementy XML Atrybuty XML Umieszczanie kodu LINQ w LINQ to XML Przeszukiwanie kodu XML za pomocą LINQ Wyszukiwanie pojedynczego węzła Osie wyszukiwania Klauzule where Serializacja XML Dostosowywanie serializacji XML za pomocą atrybutów Podsumowanie

456 459 460 463 464 467 468 469 469 472 473

13. Sieci

............................................................................................................................

Wybór technologii sieciowej Aplikacja WWW z kodem klienta Klient .NET i serwer .NET Klient .NET i usługa WWW pochodząca z zewnątrz Klient zewnętrzny i usługa WWW .NET Platforma WCF Tworzenie projektu WCF Kontrakty WCF Testowy host i klient WCF Udostępnianie usługi WCF Pisanie klienta WCF Dwukierunkowa komunikacja z dwustronnymi kontraktami Protokół HTTP Klient WWW Klasy WebRequest i WebResponse Gniazda Protokoły IP, 1Pv6 oraz TCP Łączenie się z usługami za pomocą klasy Socket Implementowanie usług za pomocą klasy Socket

8

453

Spis treści

475 475 476 480 482 483 483 483 484 486 489 496 504 513 514 518 525 526 531 535

Inne możliwości związane z siecią

540

Podsumowanie

540

14. Bazy danych

.„••„.„.„.„.„.„.„.„. „.„.„.„.„.„.„.„.„.„. „.„.„.„.„.„.„.„••„.„. „.„.„.„.„.„.„.

Krajobraz możliwości dostępu do danych w ramach platformy .NET Klasyczny mechanizm ADO .NET LINQ i bazy danych Technologie dostępu do danych nieopracowane przez firmę Microsoft WCF Data Services Technologia Silverlight i dostęp do danych Bazy danych Model encji danych Wygenerowany kod Zmiana odwzorowywania Związki Dziedziczenie Zapytania LINQ to Entities Entity SQL Mieszanie języków ESQL oraz LINQ Dostawca ADO.NET EntityClient Kontekst obiektu Obsługa połączenia Tworzenie, aktualizowanie i usuwanie Transakcje Optymistyczna współbieżność Czas życia kontekstu i encji WCF Data Services Podsumowanie

15. Podzespoły

„„.„.„.„.„..„.„.„.„.„.„.„.„.„.„.„.„.„.„.. „„.„.„.„.„.„.„.„.„.„.„..„.„.„.„.„

Komponenty .NET - podzespoły Odwołania Pisanie bibliotek Ochrona Nazwy Podpisywanie i silne nazwy Ładowanie Ładowanie z folderu aplikacji Ładowanie z bufora GAC Ładowanie z pliku Silverlight o rozszerzeniu xap Jawne ładowanie Podsumowanie

541 541 542 546 548 548 549 550 551 555 557 558 565 566 566 570 573 574 574 574 577 579 584 586 587 591

.. 593 593 594 597 599 602 603 605 606 606 607 607 609

Spis treści

9

16. Wątki i kod asynchroniczny

..„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.. „.„.„.„.„.„.„.„.„.„.„.„.„.

VVątki VV ątki i systemowy mechanizm szeregujący Stos Pula wątków Powinowactwo oraz kontekst wątków Popularne błędne opinie dotyczące wątków Tworzenie kodu wielowątkowego jest trudne Strategie tworzenia kodu wielowątkowego Podstawowe narzędzia synchronizacji Monitor Inne typy blokad Inne mechanizmy synchronizacji Zdarzenia Odliczanie Programowanie asynchroniczne Model programowania asynchronicznego Programowanie asynchroniczne bazujące na zdarzeniach Doraźne operacje asynchroniczne Task Parallel Library Zadania Obsługa anulowania Obsługa błędów Równoległość danych Metody Parallel.For oraz Parallel.ForEach PLINQ - równoległe LINQ Podsumowanie

17. Atrybuty i odzwierciedlanie

„. „.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„ ••

Atrybuty Typy atrybutów VVłasne atrybuty Odzwierciedlanie Badanie metadanych Odkrywanie typów Odzwierciedlanie na rzecz określonego typu Późne wiązanie Podsumowanie

18. Typ dynamie

„„.„.„.„.„.„.„.„..„.„.„.„.„.„.„.„.„.„.„ .„.„.„.„.„.„.„.„.„.„.„.„.„.„.„..„„.

Styl statyczny kontra styl dynamiczny Styl dynamiczny i automatyzacja COM Typ dynamie Typy obiektów i słowo dynamie Typ dynamie w zastosowaniach niezwiązanych z interoperacyjnością? Podsumowanie 10

Spis treści

611

613 615 617 624 625 627 634 637 638 639 649 653 653 654 655 656 659 660 661 661 668 669 671 671 673 674

675 675 676 677 681 681 683 684 686 689

691 691 693 694 697 707 710

19. Współdziałan ie z COM i Win32

„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„„.

711

Importowanie kontrolek ActiveX Importowanie kontrolek do projektów .NET Podzespoły współdziałania Bez PIA 64 czy 32 bi ty?

71 1 712 714 716 717

Mechanizm P /lnvoke

720

Wskaźniki

724

Rozszerzenia składni C# 4.0 Właściwości indeksowane Opcjonalny modyfikator ref Podsumowanie

729 729 730 731

20. WPF i Silverl ight

„„.„.„.„.„.„.„.„.„.„.„.„.„••„.„.„.„ .„.„.„.„.„.„.„.„.„.„. „.„.„.„.„.„.„.

XAML i kod ukryty XAML i obiekty Elementy i kontrolki Panele układów Elementy graficzne Kontrolki Kontrolki użytkownika Szablony kontrolek Style Menedżer stanu wizualnego Wiązanie danych Szablony danych Podsumowanie

21. Tworzen ie aplikacji w ASP.NET

735 739 742 743 752 759 763 765 767 769 771 773 776 ..„.„.„.„.„.„.„.„.„.„.„.„.„.„.„.„••„.„.„.„.„.„.„.„.„.„

777 777 779 780 781 782 783 785 786 789 792 796

Podstawy technologii Web Forms Zdarzenia formularzy sieciowych Cykl życia stron w technologii Web Forms Tworzenie aplikacji internetowych Pliki kodu ukrytego Dodawanie kontrolek Kontrolki serwerowe Wiązanie danych Sprawdzanie kodu Dodawanie kontrolek i formularzy Podsumowanie

22. Windows Forms

733

.„.„.„.„••„.„.„. „.„.„.„.„.„.„.„.„.„. „.„.„.„.„.„.„.„.„.„.„.„.„••„.„.„.„.

Tworzenie aplikacji Dodawanie źródła wiązania

797 798 799

Spis treści

11

Kontrolki Dokowanie i kotwiczenie Wiązanie danych Obsługa zdarzeń Podsumowanie

Skorowidz

12

...„• •„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„• •„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .„ .

Spis treści

801 806 808 813 814

817

Wstęp

Microsoft udostępnił .NET Framework w 2000 roku i w ciągu dekady, która upłynęła od tego czasu, stał się on niezwykle popularną platformą do tworzenia aplikacji przeznaczonych dla systemu Windows . Choć platforma .NET obsługuje wiele języków programowania, to jednak jest ona szczególnie mocno powiązana z językiem zaprojektowanym specjalnie dla niej - C#. Od momentu swego powstania język C# znacznie się rozwinął. Każda jego kolejna wersja udo­ stępniała nowe technologie programistyczne - w C# 2.0 dodano typy ogólne i rozwinięte moż­ liwości programowania funkcyjnego, następnie, w wersji C# 3.0, zapytania zintegrowane oraz jeszcze większe możliwości programowania funkcyjnego, a w końcu, w wersji C# 4.0 wprowa­ dzono nowe dynamiczne możliwości języka. Platforma .NET rozwijała się wraz z językiem C#. Początkowo, w wersji .NET 1 .0, biblioteka klas pokrywała jedynie fragmentarycznie cały zakres możliwości systemu operacyjnego. Co więcej, możliwości biblioteki unikalne dla platformy .NET, zamiast stanowić opakowania dla innych rozwiązań, były stosunkowo skromne . Aktualnie oprócz obsługi znacznie szerszego zakresu możliwości platformy systemowej dysponujemy także rozwiązaniami związanymi z graficznym interfejsem użytkownika (WPF), znacznie poprawionymi możliwościami obsługi baz danych, potężnymi mechanizmami wspierającymi współbieżną realizację kodu oraz szerokim zakresem usług komunikacyjnych (WCF) . A to i tak tylko kilka spośród wielu możliwości platformy .NET. Dodatkowo funkcjonalności, które były dostępne w platformie od momentu jej powstania, takie jak technologia do tworzenia aplikacji internetowych (ASP.NET), zostały znacząco rozwinięte. Możliwości korzystania z platformy .NET nie ograniczają się już wyłącznie do systemu Windows. Niektórzy już wcześniej zauważali potencjał .NET do tworzenia rozwiązań niezależnych od platformy systemowej, jednak przez wiele lat Microsoft rozwijał język C# wyłącznie w wersji przeznaczonej dla Windows, a jedynym sposobem uruchamiania C# w innych systemach pozo­ stawały różne projekty o otwartym kodzie źródłowym. Jednak w 2008 roku, wraz z wprowa­ dzeniem technologii Silverlight 2, po raz pierwszy można było zobaczyć kod C# działający z pełnym wsparciem firmy Microsoft w systemach innych niż Windows (na przykład na kom­ puterach Mac) . Język C# od momentu swego debiutu w roku 2000 przeszedł długą drogę, i to zarówno pod względem zakresu możliwości, jak i wielkości . Naszym celem w niniejszej książce jest poka­ zanie Czytelnikowi, jak należy go używać.

13

Struktura książki Niniejsza książka zaczyna się od przedstawienia szczegółów języka C#, których Czytelnik będzie używał w codziennej pracy. Następnie prezentujemy najbardziej popularne części biblio­ teki klas .NET Framework, z których także będziemy regularnie korzystać. Dalej zajmujemy się nieco bardziej wyspecjalizowanymi możliwościami .NET Framework, by w końcu przedsta­ wić wchodzące w skład .NET platformy używane do tworzenia aplikacji przeznaczonych dla systemu Windows, jak również aplikacji internetowych. Rozdział 1 . „Prezentacja C#" Ten rozdział przedstawia naturę C# oraz związki tego języka z .NET Framework. Rozdział 2. „Podstawowe techniki programowania" W tym rozdziale opisujemy podstawowe elementy kodu pisanego w języku C# - czyn­ ności, jakie należy wykonać, by zacząć pracę, oraz podstawowe elementy języka takie jak zmienne, sterowanie przepływem, pętle i metody. Rozdział 3. „Wyodrębnianie idei przy wykorzystaniu klas i struktur" C# jest językiem obiektowym, zatem w tym rozdziale opisane zostały te jego cechy, które są związane z technikami programowania obiektowego. Rozdział 4. „Rozszerzalność i polimorfizm" W tym rozdziale kontynuujemy prezentację zagadnień poruszonych w rozdziale poprzed­ nim, ilustrując sposoby, w jakie C# obsługuje dziedziczenie, interfejsy oraz inne związane z nimi zagadnienia. Rozdział 5. „Delegacje - łatwość komponowania i rozszerzalność" Możliwości C# nie ograniczają się do programowania obiektowego - udostępnia on także pewne niezwykle potężne mechanizmy programowania funkcyjnego. W tym rozdziale poka­ zujemy, że czasami mogą one być znacznie bardziej elastyczne i prostsze od technik obiek­ towych. Rozdział 6. „Obsługa błędów" We wszystkich programach mogą się pojawić problemy związane bądź to z błędami pro­ gramistycznymi czy nieoczekiwanymi danymi wejściowymi, bądź też z wieloma innymi czynnikami. Ten rozdział prezentuje dostępne sposoby ich wykrywania oraz obsługi . Rozdział 7. „Tablice i listy" W tym rozdziale przedstawiono dostępne w C# narzędzia służące do reprezentowania kolekcji danych. Rozdział 8. „LINQ" Samo reprezentowanie danych w formie kolekcji nie wystarcza, dlatego też ten rozdział przedstawia zintegrowany z C# język zapytań i pokazuje, jak można go używać do prze­ twarzania kolekcji danych. Rozdział 9. „Klasy kolekcji" Rozdział zawiera prezentację niektórych spośród bardziej wyspecjalizowanych klas służą­ cych do operowania na kolekcjach danych na określone sposoby.

14

Wstęp

Rozdział 10. „Łańcuchy znaków" Dla większości aplikacji tekst jest szczególnie istotnym typem danych, dlatego też w tym rozdziale pokazujemy, w jaki sposób jest on reprezentowany oraz jak można formatować dane w postaci tekstowej . Rozdział 1 1 . „Pliki i strumienie" Ten rozdział pokazuje, w jaki sposób można zapisywać informacje na dysku, jak je potem odczytywać oraz jak wykonywać inne operacje na systemie plików. Zawiera on także infor­ macje o tym, jak niektóre spośród koncepcji używanych do pracy z plikami można wyko­ rzystywać w innych sytuacjach. Rozdział 12. „XML" Rozdział ten przedstawia udostępniane przez .NET Framework klasy służące do prze­ twarzania kodu XML oraz wyjaśnia, jak można z nich korzystać w połączeniu z technologią LINQ. Rozdział 13. „Sieci" W tym rozdziale zajmujemy się różnymi technikami komunikacji sieciowej . Rozdział 14. „Bazy danych" Ten rozdział pokazuje, w jaki sposób można korzystać z baz danych w aplikacjach C#. Rozdział 15. „Podzespoły" W tym rozdziale wyjaśniono, jak można tworzyć biblioteki nadające się do wielokrotnego stosowania oraz jak działają programy składające się z wielu komponentów. Rozdział 16. „Wątki i kod asynchroniczny" W wielu programach pojawia się konieczność działania współbieżnego, a ten rozdział poka­ zuje narzędzia i techniki współbieżnego wykonywania kodu dostępne w języku C#. Rozdział 17. „Atrybuty i odzwierciedlanie" C# dysponuje możliwością badania struktury kodu, która ułatwia automatyzację niektó­ rych typów zadań. Ten rozdział przedstawia interfejs API służący do tego celu oraz wyja­ śnia, jak można rozszerzać informacje strukturalne przy wykorzystaniu atrybutów. Rozdział 18. „ Typ dynamie" Jedną z nowych możliwości języka C# jest wiązanie dynamiczne. Jest ono szczególnie przy­ datne w niektórych scenariuszach współdziałania, co też opisujemy w tym rozdziale. Rozdział 19. „Współpraca z COM i Win32" Czasami pojawia się konieczność komunikowania się kodu C# z komponentami, które nie zostały stworzone z myślą o platformie .NET. W tym rozdziale pokazujemy, jak w pro­ gramach pisanych w C# można używać komponentów COM oraz bibliotek Win32 DLL. Rozdział 20. „WPF i Silverlight" WPF oraz Silverlight udostępniają bardzo podobne modele programowania służące do tworzenia interfejsu użytkownika. Ten rozdział opisuje, jak można ich używać w aplikacjach pisanych w C#. Rozdział 21 . „ Tworzenie aplikacji ASP .NET" Ten rozdział przedstawia sposoby korzystania z technologii ASP.NET, jednego z elemen­ tów .NET Framework przeznaczonych do tworzenia aplikacji internetowych.

Struktura książki

15

Rozdział 22. „ Windows Forms" Ten rozdział przedstawia zasady korzystania z Windows Forms - technologii stanowią­ cej opakowanie dla klasycznych mechanizmów tworzenia interfejsu użytkownika w sys­ temie Windows . Choć jest ona mniej elastyczna od WPF, to jednak pozwala na łatwiejszą integrację ze starszymi komponentami takimi jak kontrolki ActiveX .

Gdzie szukać i nformacji o nowych możl iwościach C# 4.0 oraz . N ET 4? Choć niniejsza książka została napisana tak, by czytać ją w całości, to jednak przypuszczamy, że niektórzy Czytelnicy będą chcieli przejrzeć informacje dotyczące nowych możliwości C# 4.0 oraz platformy .NET 4. Naszym celem było pokazanie, w jaki sposób język C# jest używany obecnie, dlatego też zrezygnowaliśmy z odzwierciedlania w strukturze książki jego historii Czytelnik i tak będzie używał jednocześnie możliwości wprowadzanych w różnych wersjach języka . Okazuje się, że jedna z możliwości wprowadzonych w C# 4.0 ma bardzo konkretne i szczególne zastosowanie, dlatego też poświęciliśmy jej osobny rozdział, jednak w znaczącej większości przypadków informacje o nowych możliwościach są rozsiane po całej książce . Chcie­ liśmy je zamieszczać tam, gdzie wiedza o nich będzie potrzebna, nie możemy zatem wskazać konkretnych rozdziałów. Zamiast tego zamieściliśmy poniżej krótki przewodnik opisujący, gdzie można szukać informacji o tych nowych możliwościach C#. Rozdział 1 . opisuje ogólne cele, jakim służą nowe możliwości wprowadzone w C# 4.0. Roz­ dział 3. przedstawia wykorzystanie wartości domyślnych oraz nazwanych argumentów (poja­ wiają się one także w rozdziałach 1 1 . i 17.) . Rozdział 7. opisuje wariancję - raczej techniczny aspekt systemu typów, którego implikacje są przydatne w przypadku korzystania z kolekcji. W rozdziale 16. opisujemy nowe szerokie możliwości obsługi wielowątkowości wprowadzone w C# 4. Rozdział 18. został w całości poświęcony zupełnie nowej możliwości języka: wspar­ ciu dla programowania dynamicznego. I w końcu rozdział 19. przedstawia nowe możliwości no-PIA oraz inne pozwalające na tworzenie bardziej eleganckiego kodu w niektórych rozwią­ zaniach wykorzystujących współdziałanie .

Dla kogo jest przeznaczona ta ks iążka? Jeśli Czytelnik zna język C# w podstawowym zakresie, a chciałby swą wiedzę pogłębić, bądź dobrze zna inny język programowania taki jak C++ lub Java lub też jeśli C# jest pierwszym językiem, jaki chce poznać, to niniejsza książka jest przeznaczona właśnie dla niego.

Czego potrzeba, by korzystać z tej ks iążki? By w pełni wykorzystać informacje zamieszczone w niniejszej książce, należy pobrać najnowszą wersję Visual Studio 2010. Można skorzystać z dowolnej wersji C#, nawet z bezpłatnej wersji Express, którą można pobrać ze strony http://www.microsoft.com/express/. Przykłady zamieszczone w rozdziale 14. wymagają także zainstalowania programu SQL Server lub SQL Server Express. Niektóre wersje Visual Studio domyślnie instalują SQL Server Express, może się zatem okazać, że znajduje się on już na komputerze Czytelnika .

16

Wstęp

Kody źródłowe przykładów prezentowanych w książce można pobrać z serwera FTP wydaw­ nictwa Helion: ftp:/lftp .helion .pl/przyklady/cshpr6 zip .

.

Konwencje używane w ks iążce W książce używamy poniższych rodzajów czcionek do oznaczania poszczególnych elementów tekstu.

Kursywą pisane są: •

ścieżki dostępą nazwy plików i nazwy programów;



adresy internetowe takie jak nazwy domen lub adresy URL;



nowe pojęcia w miejscu ich definiowania .

C z c i on k i o stałej s zero kośc i używamy do oznaczania: •

tekstu w wierszu poleceń i opcji, które są tam wpisywane;



nazw i słów kluczowych w przykładowych programach, włączając w to nazwy metod, zmiennych i klas.

Kursywę o stałej szerokości stosujemy: •

w wierszach zawierających kod do przedstawienia elementów, które mogą ulec zmianie, na przykład opcjonalnych składników programów lub zmiennych.

Pogrubi ona czci onka o sta lej szerokości •

służy do:

podkreślania fragmentów kodu w programach.

Warto zwrócić szczególną uwagę na fragmenty tekstu oznaczone poniższymi ikonami. „ „-

Tak oznaczone są wskazówki. Zawierają one dodatkowe wartościowe informacje na opisywany temat.

To jest ostrzeżenie. Pomaga rozwiązać irytujące problemy lub ich uniknąć.

Korzystan ie z przykładów Ta książka została napisana po to, by ułatwić Czytelnikowi wykonywanie stawianych przed nim zadań. Ogólnie rzecz biorąc, można korzystać z prezentowanych w niej przykładów w swo­ ich programach oraz dokumentacji. Nie trzeba prosić nas o pozwolenie, chyba że Czytelnik ma zamiar zastosować znaczne fragmenty kodu. Na przykład napisanie programu wykorzystu­ jącego kilka fragmentów kodu z zamieszczonych w książce przykładów nie wymaga uzyski­ wania pozwolenia. Jest ono natomiast potrzebne w przypadku sprzedawania lub dystrybucji płyty CD zawierającej komplet dołączonych do niej przykładów. Odpowiadanie na pytania przy wykorzystaniu cytatu z tej książki lub poprzez zamieszczenie fragmentu pochodzącego z niej kodu nie wymaga zgody. Wymaga jej jednak zastosowanie znaczącej ilości tych kodów w dokumentacji własnego produktu. Korzystanie z przykładów

17

Na pewno docenimy podanie źródła, z którego zostały zaczerpnięte przykłady, lecz tego nie wymagamy. Jeśli Czytelnik będzie chciał to zrobić, to zazwyczaj należy podać tytuł, autorów, wydawcę oraz numer ISBN książki, na przykład: C#. Programowanie. Wydanie VI, ISBN: 978-83-246-3701-0 autorstwa Iana Griffithsa, Matthew Adamsa i Jessego Liberty, wydawnictwo Helion.

Podziękowan ia Od lana Griffithsa Chciałbym podziękować korektorom technicznym, którzy pomogli w poprawianiu tej książki: Nicholasowi Paldino, Chrisowi Smithowi, Chrisowi Williamowi, Michaelowi Batonowi, Brianowi Peekowi oraz Stephenowi Toubowi. Wszyscy pracownicy wydawnictwa O'Reilly okazali nam wiele wsparcia i cierpliwości podczas prac nad tym projektem, dlatego chciałbym bardzo podziękować Mike' owi Hendricksonowi, Laurel Rumie, Audrey Doyle oraz Sumicie Mukherjee. Dziękuję także Johnowi Osbornowi za uruchomienie wszystkiego na samym początku prac nad tą książką oraz za wprowadzenie Matthew i mnie przed wielu laty do ekipy jako autorów pracujących dla wydawnictwa O'Reilly. Dziękuję też memu współautorowi za to, że nie zapamiętał lekcji wyniesionej z pisania poprzed­ niej książki i zgodził się napisać za mną kolejną. I w końcu dziękuję Jessemu Liberty za zapyta­ nie, czy chcielibyśmy przejąć prace nad tą książką.

Od Matthew Adamsa Do podziękowań zamieszczonych przez mego współautora i skierowanych do wszystkich pra­ cowników wydawnictwa O'Reilly, których cierpliwość, pomoc i wsparcie umożliwiły powstanie tej książki, oraz do wszystkich korektorów, których praca okazała się nieoceniona, chciałbym dodać także i swoje . Dodatkowo chciałbym także podziękować Karolinie Lemiesz, magicznej kawiarce z kawiarni Starbucks, w której napisałem większą część swoich tekstów, za ciągłe dostawy ristretto oraz edukację w zakresie degustacji kawy, kiedy praca mnie już za bardzo przytłaczała. Jak zwykle moja partnerka Una zapewniła mi niezbędny fundament miłości i wsparcia (nie zważając na zbliżające się terminy jej własnej książki) . I w końcu, choć wszyscy, którzy sądzą, że przejęcie książki po innym autorze jest bezproblemowe, są w błędzie, to jednak mój współ­ autor sprawił, że wszystko wyglądało na łatwe. Dziękuję mu bardzo, w szczególności za wyro­ zumiałość, poczucie humoru oraz przyjaźń. I dobre obiady.

18

Wstęp

ROZDZIAŁ 1.

Prezentacja C#

C# - wymawiany jako „C szarp" - jest językiem programowania zaprojektowanym dla platformy .NET firmy Microsoft. Już od momentu wprowadzenia tego języka w 2002 roku był on wykorzystywany w wielu obszarach. Znalazł powszechne zastosowanie w tworzeniu aplikacji działających na serwerach i obsługujących witryny WWW, jak również w tradycyjnych aplikacjach biznesowych. Można w nim pisać aplikacje przeznaczone na smartfony oraz gry przeznaczone na konsole Xbox 360. Od niedawna, dzięki wprowadzeni u technologii Microsoft Silverlight, języka C# można także używać do tworzenia aplikacji działających w przeglądarkach WWW (ang. Rich Internet Applications) . Jednak jakiego rodzaju językiem jest C#? Aby zrozumieć język programowania na tyle dobrze, by móc go efektywnie używać, nie wystarczy skoncentrować się na jego szczegółach i mecha­ nizmach, choć w tej książce poświęcimy im dużo uwagi. Równie duże znaczenie ma zrozumie­ nie idei kryjących się za tymi szczegółami . Właśnie dlatego w tym rozdziale przyjrzymy się problemom, do rozwiązywania których język ten został stworzony. Następnie przyjrzymy się stylowi C#, analizując, czym różni się on od innych języków programowania. Rozdział ten zakończymy prezentacją najnowszego etapu w ewolucji C# - czwartej wersji tego języka.

Dlaczego C#? Dlaczego .N ET? Języki programowania istnieją po to, by zapewniać programistom możliwość bardziej efek­ tywnej pracy. Wiele języków, które odniosły największe sukcesy, automatyzuje żmudne czyn­ ności, które wcześniej trzeba było wykonywać samodzielnie . Wiele z nich udostępnia nowe techniki pozwalające na rozwiązywanie starych problemów nowymi, bardziej efektywnymi sposobami bądź też w większej skali. To, na ile język C# będzie dla Czytelnika czymś odmien­ nym, zależy oczywiście od jego doświadczeń programistycznych. Dlatego też warto zastanowić się, o jakich programistach myśleli projektanci C#, tworząc ten język. C# jest przeznaczony dla programistów używających platformy systemowej Windows, a jego składnia nie będzie obca osobom znającym C, C ++ oraz inne języki wywodzące się z tej samej tradycji takie jak Java oraz JavaScript. Podstawowe elementy języka, takie jak instrukcje, wyra­ żenia, deklaracje funkcji oraz przepływ sterowania, są w możliwie najwierniejszy sposób wzo­ rowane na rodzinie języków C .

19

Oczywiście znajoma składnia nie jest wystarczającym powodem, by zacząć używać nowego języka programowania, dlatego też C# udostępnia mechanizmy poprawiania wydajności pracy, których jego poprzednicy nie posiadają. Mechanizm odzyskiwania pamięci (ang. garbage collector) uwalnia programistów od tyranii powszechnie występujących problemów związanych z zarzą­ dzaniem pamięcią takich jak przecieki pamięci oraz odwołania cykliczne . Sprawdzalne bezpie­ czeństwo typów w skompilowanym kodzie eliminuje wiele błędów i potencjalnych zagrożeń. Programiści używający języków C i C ++ w systemie Windows mogą nie znać tych możliwości, jednak będą one czymś dobrze znanym dla doświadczonych programistów Javy, choć Java nie ma niczego, co można by porównać z LINQ - technologią używaną w C# do operowania na kolekcjach informacji niezależnie od tego, czy są to grupy obiektów, dokumenty XML, czy też bazy danych. Integracja kodu pochodzącego z komponentów zewnętrznych jest w C# w znacznej mierze bezbolesna, a dotyczy to także komponentów pisanych w innych językach. Język ten wspiera także stosowanie programowania funkcyjnego - jest to potężna możliwość, która wcześniej pojawiała się jedynie w językach akademickich . Wiele spośród najbardziej użytecznych cech języka C# pochodzi z platformy .NET, która zawiera środowisko uruchomieniowe oraz biblioteki używane zarówno w C#, jak i we wszystkich innych językach .NET takich jak YB.NET. Język C# został zaprojektowany na potrzeby platformy .NET, a jedną z głównych zalet ich ścisłego związku jest to, że korzystanie z wielu możliwości plat­ formy, takich jak biblioteki klas, odbywa się w sposób bardzo naturalny.

Bibl ioteka klas platformy . N ET Korzystanie z C# nie oznacza wyłącznie stosowania tego języka - klasy wchodzące w skład .NET Framework odgrywają w codziennej pracy programistów C# niesłychanie ważną rolę (a informacje na ich temat stanowią znaczącą część niniejszej książki) . Większość funkcjonalności tej biblioteki można zaliczyć do jednej z trzech kategorii: możliwości pomocniczych wcho­ dzących w skład .NET, „opakowań" osłaniających możliwości funkcjonalne systemu Windows oraz platform. Do pierwszej z tych kategorii zaliczają się typy pomocnicze, takie jak słowniki, listy oraz klasy innych rodzajów kolekcji, jak również mechanizmy operujące na łańcuchach znaków, takie jak mechanizm obsługi wyrażeń regularnych. Do tego dochodzą także możliwości działające w nieco większej skali takie jak model obiektowy używany do reprezentowania dokumen­ tów XML. Oprócz tego niektóre możliwości platformy .NET stanowią swoiste opakowania dla możli­ wości systemu operacyjnego. Można do nich zaliczyć na przykład klasy zapewniające obsługę systemu plików czy pozwalające na wykonywanie operacji sieciowych takich jak korzystanie z gniazd. Dostępne są także klasy pozwalające na wyświetlanie tekstów na konsoli, które możemy zaprezentować w obowiązkowym programie przykładowym rozpoczynającym wszyst­ kie książki programistyczne (przedstawionym na listingu 1 .1) .

Listing 1 . 1 . Nieunikniony program Witaj, świecie {

cl ass Program

20

{

s t at i c vo i d Ma i n ()

Rozdział 1. Prezentacja C#

Sys tem . Cons o l e . Wri teli ne ( "Wi taj , świ eci e " ) ;

Wszystkie elementy przedstawionego programu zostaną dokładnie opisane w dalszej części książki . Na razie warto zwrócić uwagę, że nawet ten najprostszy program w celu wykonania swego zadania korzysta z klas należących do biblioteki, a ściśle rzecz biorąc, z klasy Sys t em . "+Con sol e . W końcu, biblioteka klas udostępnia także całe platformy służące do tworzenia pewnych okre­ ślonych typów aplikacji. Na przykład Windows Presentation Foundation (WPF) jest platformą służącą do tworzenia standardowych aplikacji dla systemu Windows, a ASP.NET (co wbrew pozorom jest skrótem) jest platformą służącą do pisania aplikacji internetowych. Nie wszystkie platformy służą do tworzenia interfejsów użytkownika - Windows Communication Founda­ tion (WCF) została zaprojektowana w celu tworzenia usług sieciowych wykorzystywanych na przykład przez inne systemy komputerowe. Przedstawiony podział na trzy kategorie nie jest ścisły, gdyż niektóre z klas można by zaliczyć do dwóch spośród nich . Na przykład klasy zapewniające dostęp do systemu plików nie „owijają" jedynie dostępnych funkcjonalności Win32 API. Dodają one nowy obiektowy poziom abstrakcji i znaczące możliwości funkcjonalne wykraczające poza podstawowe usługi operujące na plikach, przez co można je zaliczyć zarówno do pierwszej, jak i do drugiej z wymienionych kategorii . Także platformy zazwyczaj muszą być do pewnego stopnia zintegrowane z innymi usługami - na przykład, choć platforma Windows Form posiada własny bogaty interfejs programowania aplikacji (API), to jednak znaczna część jego możliwości jest udostępniana przez komponenty Win32. Z tych powodów podział na trzy wymienione wcześniej kategorie nie jest ścisły. Jest on jednak wygodny i daje pojęcie na temat tego, co można zrobić przy uży­ ciu biblioteki klas .NET.

Styl języka C# nie jest jedynym językiem działającym na platformie .NET. W rzeczywistości możliwość obsługi wielu języków programowania od zawsze była jej kluczową cechą, która znalazła odzwierciedlenie w nazwie używanego mechanizmu uruchomieniowego - Common Language Runtime (CLR) . Nazwa ta sugeruje, że platforma .NET nie jest przeznaczona tylko dla jednego języka - z usług biblioteki klas .NET Framework może korzystać wiele z nich. Dlaczego Czy­ telnik miałby wybrać akurat C#, a nie jakiś inny dostępny język? Już wcześniej wspomnieliśmy o jednym istotnym powodzie: C# został zaprojektowany wła­ śnie z myślą o platformie .NET. Jeśli używamy technologii .NET takich jak WPF czy ASP.NET, to korzystając z C#, będziemy rozmawiać w ich języku. Proszę to porównać z C ++, który także obsługuje .NET dzięki specjalnym rozszerzeniom swojej oryginalnej wersji. Rozszerzenia te zostały dokładnie przemyślane i działają dobrze, jednak kod, w którym są wykorzystywane biblioteki .NET, wygląda nieco inaczej od standardowego kodu C++, dlatego też programy łączące ze sobą światy .NET i standardowego języka C ++ nigdy nie będą wyglądać całkowicie spójnie. Taka podwójna „osobowość" często jest przyczyną dylematów - na przykład czy należy używać standardowych klas kolekcji dostępnych w C++, czy tych pochodzących z biblio­ teki klas .NET. W językach stworzonych z myślą o platformie .NET takie problemy w ogóle się nie pojawiają.

Styl języka

21

Jednak C# nie jest jedynym językiem zaprojektowanym na potrzeby platformy .NET. W Visual Studio 2010 dostępne są trzy takie języki: C#, YB.NET oraz F#. (Choć YB.NET bardzo przypo­ mina swoich poprzedników, którzy nie mieli nic wspólnego z .NET, to jednak pod wieloma ważnymi względami jest on od nich odmienny. Jest to język zaprojektowany i stworzony dla platformy .NET posiadający składnię podobną do składni VB 6, a nie VB 6 wyposażony w moż­ liwości korzystania z .NET) . Wybór pomiędzy tymi językami zależy od preferowanego stylu programowania. Język F# jest nieco odmienny. Przede wszystkim jest to język funkcyjny silnie wzorowany na języku ML. W 1991 roku, kiedy autorzy tej książki byli studentami pierwszego roku, w ramach zajęć z informatyki na ich uniwersytecie jako pierwszy język zaprezentowano właśnie ML. Stało się tak po części dlatego, że był to język o charakterze akademickim i żaden ze studentów raczej nie miał okazji spotkać się wcześniej z czymś, co by go przypominało. Na skali języków progra­ mowania F# wciąż znajduje się po stronie języków akademickich, choć zszedł on z naukowych wyżyn na tyle nisko, by móc stać się standardową częścią jednego z najpopularniejszych śro­ dowisk programistycznych. Koncentruje się na złożonych obliczeniach i algorytmach, a niektóre spośród jego cech ułatwiają przetwarzanie współbieżne. Niemniej jednak, podobnie jak to bywa w przypadku wielu innych języków funkcyjnych, także i w F# ceną za ułatwienie rozwiązywa­ nia złożonych problemów jest znaczne skomplikowanie rozwiązywania prostych zadań. Języki funkcyjne doskonale nadają się bowiem do złożonych zagadnień, a z prostymi radzą sobie nie najlepiej . Najprawdopodobniej F# będzie używany w aplikacjach naukowych i finansowych, w których złożoność obliczeń, jakie należy wykonać, znacznie przewyższa złożoność kodu, który musi operować na ich wynikach. Podczas gdy język F# jest odczuwalnie inny, to YB.NET oraz C# mają wiele cech wspólnych. Najbardziej oczywistym czynnikiem wpływającym na wybór jednego z tych dwóch języków jest ten, iż nauka YB .NET będzie łatwiejsza dla osób znających Visual Basic, natomiast C# dla osób znających języki z rodziny C . Niemniej jednak występują pewne subtelne różnice w filozofii tych dwóch języków, które wykraczają poza samą ich składnię .

Łatwość konstruowania oprogramowania Jednym ze spójnych elementów C# jest preferowanie przez jego twórców rozwiązań ogólnych, a nie szczególnych . Jednym z najlepszych przykładów tej tendencji jest LINQ - Language INtegrated Query - technologia wprowadzona w C# 3.0. Na pierwszy rzut oka wydaje się, że dodaje ona do języka mechanizm realizacji zapytań podobnych do poleceń SQL i zapewnia naturalny sposób implementowania w kodzie operacji na bazach danych. Listing 1 .2 przed­ stawia proste zapytanie LINQ .

Listing 1 .2 . Dostęp do danych realizowany przy użyciu LINQ var cal i forn i anAuthors

=

from author i n pubs . authors where author . s tate = = " CA " sel ect new author . au_fname , author . au l name

}; foreach (var author i n cal i forn i anAuthors) {

22

Consol e . Wri teli ne (author) ;

Rozdział 1. Prezentacja C#

Wbrew pozorom C# nie wie nic na temat SQL-a ani bazy danych. Aby możliwe było stosowa­ nie podobnej składni, w C# 3.0 wprowadzono grupę mechanizmów, których połączenie spra­ wia, że kod taki jak ten przedstawiony powyżej jest w stanie nie tylko zapewnić dostęp do baz danych, lecz także przetwarzać dane XML lub operować na modelach obiektowych. Co więcej, wiele konkretnych możliwości języka można stosować w innych kontekstach i sytu­ acjach przedstawionych w dalszej części książki. C# preferuje niewielkie możliwości o ogólnym przeznaczeniu, które zapewniają dużą łatwość konstruowania programów, a nie możliwości monolityczne i wyspecjalizowane. Dobitnym przykładem tej filozofii jest możliwość, która została zademonstrowana w języku C# jako rozwiązanie prototypowe, lecz nad którą prace w końcu zostały wstrzymane - chodzi o literały XML. Ta eksperymentalna składnia pozwalała na umieszczanie bezpośrednio w kodzie programu kodu XML, który w efekcie kompilacji był przekształcany na reprezentujący go model obiektów. Decyzja o pominięciu tej możliwości podjęta przez twórców języka pokazuje, że w C# preferowane są rozwiązania ogólne nad wysoko wyspecjalizowanymi. Składnia LINQ może być używana do wielu celów, natomiast literały XML służą wyłącznie do obsługi XML - taki stopień specjalizacji jest nie na miejscu w C#1 .

Kod zarządzany Platforma .NET to coś więcej niż jedynie biblioteka klas. Jej usługi są dostępne w znacznie bardziej subtelne sposoby niż tylko za pośrednictwem jawnych odwołań do biblioteki klas. Wspominaliśmy na przykład, że C# może automatyzować pewne aspekty zarządzania pamię­ cią, które były notorycznie powodem błędów w C ++ . Porzucenie umieszczonego na stercie obiektu, kiedy nie jest on już dłużej potrzebny, jest błędem w programach pisanych w C++, natomiast w programach korzystających z platformy .NET jest to standardowy sposób pozbycia się go. Usługa ta jest udostępniana przez CLR - środowisko uruchomieniowe platformy .NET. Choć w celu zapewnienia takiej możliwości ze środowiskiem uruchomieniowym współpracuje ściśle kompilator C#, który dostarcza niezbędnych informacji dotyczących sposobu używania obiektów i danych przez kod programu, to jednak ostateczne zadanie odzyskiwania pamięci spada właśnie na CLR. W zależności od tego, z jakimi językami programowania Czytelnik miał kontakt do tej pory, idea ścisłego uzależnienia języka od środowiska uruchomieniowego może być bądź to całkowicie naturalna, bądź też nieco niepokojąca. Bez wątpienia jest ona całkowicie odmienna od sposobu działania języków C i C++ . W ich przypadku wynik działania kompilatora można bowiem uruchomić bezpośrednio na komputerze i choć języki te posiadają pewne usługi uruchomie­ niowe, to jednak można pisać kod, który nie będzie z nich korzystał . Tymczasem programy pisane w C# nie mogą być wykonywane bez pomocy środowiska uruchomieniowego. Taki kod, który jest całkowicie uzależniony od środowiska uruchomieniowego, nazywamy kodem zarzą­ dzanym (ang. managed code) . Kompilatory generujące kod zarządzany nie tworzą binarnych programów wykonywalnych. Zamiast tego generują one pośrednią postać kodu określaną jako IL - Intermediate Language 1 Literały XML są obsługiwane w języku YB.NET. Od momentu wprowadzenia języka C# 2.0 zespoły twórców C# i YB.NET prowadzą politykę udostępniania w nich bardzo podobnego zbioru możliwości, dlatego też decy­ zja o zachowaniu w YB.NET możliwości, która nie jest dostępna w C#, stoi w sprzeczności z filozofią języka YB.NET.

Styl języka

23

(język pośredni) 2 • Środowisko uruchomieniowe dokładnie określa, w jaki sposób przekształcić go w coś nadającego się do wykonania . Efektem zastosowania kodu zarządzanego jest możli­ wość wykonywania programów napisanych w języku C# - bez konieczności wprowadzania jakichkolwiek zmian w kodzie - zarówno w systemach 32-, jak i 64-bitowych, a nawet na komputerach wykorzystujących procesory o różnej architekturze. Czasami możliwe jest nawet uruchamianie programów przeznaczonych dla komputerów kieszonkowych z procesorami ARM na komputerach z procesorami firmy Intel bądź programów napisanych dla komputerów z procesorem PowerPC na konsolach Xbox 360. Choć uniezależnienie programu od procesora, na jakim ma on działać, jest interesujące, to jednak w praktyce najbardziej przydatnym aspektem zarządzanego kodu oraz IL jest udostęp­ nianie przez środowisko uruchomieniowe .NET użytecznych usług, których implementacja w tradycyjnych systemach kompilowanych byłaby bardzo trudna . Innymi słowy, celem jest zwiększenie produktywności programistów. Jednym z przykładów może być zarządzanie pamięcią, o którym wspominaliśmy wcześniej. Do innych można zaliczyć: model bezpieczeństwa, w którym uwzględniane jest pochodzenie kodu, a nie jedynie tożsamość uruchamiającego go użytkownika; elastyczne mechanizmy do wczytywania wspólnych komponentów dysponujące solidnymi możliwościami ich serwisowania oraz obsługi wersji; optymalizację kodu w trakcie działania programu opartą na sposobie, w jaki dany kod jest używany w praktyce, a nie na przypuszczeniach kompilatora odnośnie tego, jak będzie się z niego korzystać. W końcu, należy także wymienić możliwość weryfikacji przez CLR zgodności kodu z regułami bezpieczeństwa typów, zanim zostanie on wykonany, co eliminuje całe klasy błędów związanych z bezpieczeń­ stwem i stabilnością działania programów. Jeśli Czytelnik jest programistą używającym języka Java, to wszystkie te informacje będą brzmiały znajomo - wystarczy zamienić termin „IL" na „kod bajtowy", a reszta będzie bardzo podobna. Wśród nieco mniej rozważnych członków społeczności programistów używających Javy krąży nawet nieco ignorancki żart określający C# mianem jej kiepskiej implementacji. Kiedy pojawiła się pierwsza wersja C#, różnice pomiędzy tymi dwoma językami były nieznaczne, jednak fakt, że kilka możliwości C# zostało wprowadzonych w Javie, oznacza, że nie był on jedynie jej kopią. Wraz z pojawianiem się kolejnych wersji C# oba języki zaczęły się różnić w znacznie większym stopniu, lecz jedna z różnic istniejących pomiędzy nimi od samego początku ma szczególnie duże znaczenie dla programistów piszących programy dla systemu Windows: w C# zawsze znacznie łatwiej można było korzystać z możliwości udostępnianych przez sam system operacyjny.

C iągłość i „ekosystem" Windows Platformy służące do tworzenia oprogramowania nie odnoszą sukcesów wyłącznie dzięki swoim własnym zasługom - liczy się kontekst. Na przykład powszechna dostępność kompo­ nentów oraz narzędzi tworzonych przez firmy trzecie może znacząco zwiększyć atrakcyjność konkretnej platformy. Prawdopodobnie najbardziej wyraźnym przykładem tego fenomenu jest platforma Windows . Każdy nowy system programowania, który stara się zdobyć uznanie, wiele zyska na możliwości współpracy z już istniejącym „ekosystemem" . Jedną z ogromnych 2 W zależności od tego, czy będziemy czytać dokumentację firmy Microsoft, czy też specyfikację CDMA CLI

(Common Language Infrastructure) określającą standaryzowane elementy platformy .NET i C#, właściwą nazwą języka pośredniego będzie MSIL (Microsoft IL) bądź CIL (Common IL). W praktyce jednak bardziej popularna jest nieoficjalna nazwa - IL.

24

Rozdział 1. Prezentacja C#

różnic pomiędzy C# i Javą jest to, że C# i .NET doskonale współpracują z platformą Windows, natomiast Java robi wszystko, co może, by odseparować programistów od systemu operacyjnego. Jeśli Czytelnik będzie pisał programy przeznaczone do uruchamiania na konkretnym systemie operacyjnym, to odcinanie się języka od narzędzi i komponentów charakterystycznych dla danej platformy nie będzie szczególnie dobrą i użyteczną cechą. Platforma .NET, zamiast zmuszać programistów do zerwania z przeszłością, pozwala na zachowanie ciągłości, umożliwiając korzy­ stanie z komponentów i usług bądź to wbudowanych w system Windows, bądź też dla niego stworzonych. Choć w większości przypadków nie trzeba będzie tego robić - gdyż biblioteka platformy .NET udostępnia klasy pozwalające na korzystanie z wielu możliwości systemu operacyjnego - to jednak jeśli konieczne będzie wykorzystanie jakiegoś komponentu lub usługi systemowej, dla których nie ma klasy w bibliotece .NET, możliwość użycia takich nie­ zarządzanych funkcjonalności w zarządzanym kodzie będzie miała nieocenione znaczenie . '

. .

"';

....,'.___�_

.NET udostępnia możliwości ułatwiające integrację pisanych programów z systemem operacyjnym, na jakim będą one działać, a dodatkowo dysponuje także wsparciem dla platform innych niż Windows. Technologia Silverlight firmy Microsoft czyni możliwym wykonywanie kodu napisanego w językach C# oraz YB.NET zarówno w systemach Windows, jak i Mac OS X . Dostępny jest także projekt Mono, rozpowszechniany jako oprogramowanie otwarte (open source) i umożliwiający wykonywanie kodu korzy­ stającego z platformy .NET w systemie operacyjnym Linux, oraz powiązany z nim pro­ jekt Moonlight, stanowiący odpowiednik technologii Silverlight. Oznacza to, że fakt integracji z lokalną platformą nie przekreśla możliwości uruchamiania kodu napisanego w języku C# na wielu platformach systemowych - jeśli zależy nam na możliwości uruchamiania programu w wielu systemach operacyjnych, wystarczy powstrzymać się od stosowania możliwości dostępnych tylko na jednym z nich i charakterystycznych dla niego. A zatem największą koncepcyjną różnicą pomiędzy C# i Javą jest to, że C# zapewnia takie samo wsparcie zarówno dla bezpośredniego wykorzystania możliwości systemu operacyjnego, jak i dla uniezależnienia kodu od platformy, na jakiej będzie on działał. W przypadku Javy to pierwsze działanie jest nieporównanie trudniejsze od drugiego.

Najnowsza wersja języka C# zawiera usprawnienia, które w jeszcze większym stopniu ulep­ szają tę możliwość. Kilka nowych możliwości C# 4.0 znacznie ułatwia współpracę z aplikacjami wchodzącymi w skład pakietu Microsoft Office, jak również z innymi aplikacjami przeznaczo­ nymi dla systemu Windows i wykorzystującymi automatyzację COM (ang. COM automation), co było słabym punktem C# 3.0 . Względnie duża łatwość wykorzystania możliwości wykra­ czających poza kod zarządzany sprawia, że C# staje się atrakcyjnym rozwiązaniem oferującym wszelkie zalety realizacji kodu w środowisku zarządzanym, jak również pozwalającym na korzystanie z dowolnego kodu przeznaczonego dla platformy Windows - zarówno tego zarzą­ dzanego, jak i nie.

C# 4.0, NET 4.0 oraz Visual Stud io 2010 .

Ponieważ C# preferuje ogólne możliwości języka zaprojektowane tak, by można j e było ze sobą swobodnie łączyć, często opisywanie nowych cech języka w oderwaniu od pozostałych mija się z celem. Dlatego też, zamiast opisywać nowe możliwości C# w osobnych sekcjach lub nawet całych rozdziałach, będziemy je prezentować w kontekście - w odpowiednim połącze­ ni u z innymi, starszymi możliwościami. Niniejszy podrozdział jest oczywiście wyjątkiem C# 4.0,

.

N ET 4.0 oraz Visual Studio 2010

25

napisaliśmy go, gdyż przypuszczamy, że osoby, które już znają C# 3.0, będą przeglądać tę książkę w księgarniach, poszukując naszych opinii i informacji na temat nowych możliwości języka. Jeśli Czytelnik jest jedną z takich osób, to witamy! We wstępie można znaleźć infor­ macje o tym, co i gdzie się w tej książce znajduje, jak również podrozdział opisujący, gdzie w treści książki należy szukać informacji o nowych możliwościach języka C# 4.0 . P o tych wyjaśnieniach możemy wskazać pewien temat, który łączy ze sobą wszystkie nowe możliwości wprowadzone w czwartej wersji języka C#: obsługują one programowanie dyna­ miczne, a w szczególności koncentrują się na ułatwieniu realizacji pewnych scenariuszy współ­ działania . Przyjrzyjmy się na przykład programowi napisanemu w C# 3.0 przedstawionemu na listingu 1 .3, który próbuje odczytać wartość właściwości Aut hor wskazanego dokumentu programu Microsoft Word, korzystając przy tym z obiektowego modelu MS Office .

Listing 1 .3. Koszmar współdziałania z pakietem Office we wcześniejszych wersjach C# {

s t at i c vo i d Mai n (s tri ng O arg s ) var wordApp = new M i crosoft . Offi ce . I n terop . Word . Appl i cat i on () ; obj ect fi l eName = @ " WordFi l e . docx " ; obj ect mi s s i ng = Sys tem . Refl ect i on . Mi s s i ng . Val u e ; obj ect readOnl y = true ; M i crosoft . Offi ce . Interop . Word . -Document doc = wordApp . Documents . Open (ref fi l eName , ref mi ss i ng , ref readOn l y , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi ss i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi ss i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng) ; obj ect docPropert i es = doc . Bu i l t i nDocumentPropert i es ; Type docPropType = docPropert i es . GetType () ; obj ect authorProp = docPropType . I nvokeMember ( " I tem " , B i nd i ngFl ags . Defau l t I B i nd i ngFl ags . Get Property , nul l , docPropert i es , new obj ect [] { "Author" } ) ; Type propType = authorProp . GetType ( ) ; s t r i ng authorName = propType . InvokeMember ( " Val ue" , B i nd i ngFl ags . Defau l t I B i nd i ngFl ags . Get Property , nul l , authorProp , new obj ect [] { } ) . ToStri ng () ; obj ect s aveChanges = fal s e ; doc . Cl ose (ref s aveChanges , ref mi s s i ng , ref mi s s i ng) ; Consol e . Wri teli ne (authorName) ;

Ten kod wygląda naprawdę okropnie - bardzo trudno jest określić, co on robi, ponieważ jego przeznaczenie ukrywa znaczna liczba szczegółów. Tak nieprzyjemna postać kodu wynika z faktu, że programowa obsługa Microsoft Office została zaprojektowana z myślą o językach dynamicznych, które są w stanie uzupełniać wiele szczegółowych informacji w trakcie dzia­ łania programu. Jednak C# 3.0 nie dysponował takimi możliwościami, dlatego programiści musieli ręcznie wykonywać wszystkie niezbędne operacje. Listing 1 .4 pokazuje, w jaki sposób można zrobić dokładnie to samo w C# 4.0. Ten program znacznie łatwiej jest zrozumieć, gdyż jego kod zawiera wyłącznie to, co najistotniejsze. Zde­ cydowanie prościej można prześledzić sekwencję wykonywanych czynności - otworzenie

26

Rozdział 1. Prezentacja C#

dokumentu, pobranie jego właściwości, odczytanie wartości właściwości A u t hor i, w końcu, zamknięcie dokumentu. Dzięki swoim nowym dynamicznym możliwościom język C# 4.0 jest już w stanie podać za nas wszelkie niezbędne szczegóły.

Listing 1 .4. Współpraca z Microsoft Office w C# 4.0 {

s t at i c vo i d Mai n (s tri ng O arg s ) var wordApp = new M i crosoft . Offi ce . I n terop . Word . Appl i cat i on () ; M i crosoft . Offi ce . Interop . Word . Document doc = wordApp . Documents . Open ( "Word F i l e . docx " , ReadOn l y : true) ; dynami e docPropert i es = doc . Bu i l t i n DocumentPropert i es ; s t r i ng authorName = docPropert i es [ " Author"] . Val ue ; doc . Cl ose (SaveChanges : fal se) ; Consol e . Wri teli ne (authorName) ;

W powyższym przykładzie zastosowano kilka możliwości języka C# 4.0. Pierwszą z nich jest nowe słowo kluczowe dynam i c służące do wiązania składowych w trakcie działania programu. Oprócz tego wykorzystano także możliwość obsługi parametrów opcjonalnych. Metody Open oraz Cl ose pobierają odpowiednio 16 oraz 3 argumenty, podczas gdy w języku C# 3.0 konieczne było podanie ich wszystkich, co było widać w przykładzie 1 .3. W odróżnieniu od niego w przykładzie 1 .4 podane zostały wyłącznie te wartości argumentów, które są różne od warto­ ści domyślnej . Oprócz zastosowania tych dwóch nowych możliwości projekt zawierający powyższy kod zostałby zapewne zbudowany z wykorzystaniem kolejnej możliwości związanej ze współdziałaniem określanej jako no-PIA . Nie można jej zobaczyć w kodzie, gdyż kiedy zostanie włączona w projekcie C#, do kodu źródłowego programu nie trzeba wprowadzać żadnych zmian - jest ona bowiem związana z wdrażaniem programów. W C# 3.0, aby można było skorzystać z inter­ fejsów programowania aplikacji COM takich jak Office Automation, konieczne było zainsta­ lowanie na docelowym komputerze specjalnych bibliotek pomocniczych nazywanych primary interop assemb lies (w skrócie PIA - główne podzespoły współdziałania) . W języku C# 4.0 nie jest to już niezbędne. Wciąż trzeba dysponować tymi podzespołami na komputerze używanym do pisania programu, jednak kompilator C# potrafi pobrać informacje potrzebne do działania kodu i skopiować je do tworzonej aplikacji. W ten sposób unika się konieczności instalowania podzespołów PIA na komputerze docelowym - stąd możliwość tę określa się właśnie jako „no-PIA" (bez PIA) . Choć te nowe możliwości języka nadają się wyjątkowo dobrze do zastosowania w rozwią­ zaniach korzystających z automatyzacji COM, to jednak można ich używać wszędzie. (Zasto­ sowanie no-PIA jest nieco bardziej ograniczone, lecz w rzeczywistości jest to element plat­ formy .NET, a nie cecha języka C#) .

Podsu mowan ie W tym rozdziale zamieściliśmy krótką prezentację natury języka C# i przedstawiliśmy jego mocne strony oraz ewolucję jego najnowszej wersji. Zanim przejdziemy do szczegółów zamiesz­ czonych w następnym rozdziale, warto, by Czytelnik zdał sobie sprawę z jeszcze jednej zalety tego języka - ogromnej liczby przeróżnych zasobów z nim związanych dostępnych w internecie.

Podsumowanie

27

Po udostępnieniu platformy .NET popularność i obszar zastosowania języka C# rosły znacznie szybciej niż w przypadku innych języków wchodzących w jej skład. W efekcie, jeśli szukamy przykładów i informacji o sposobie rozwiązania konkretnych problemów, C# jest dobrym wyborem, gdyż dostępnych jest bardzo wiele informacji na jego temat - w formie blogów, przykładów, projektów otwartych oraz internetowych prezentacji. Dokumentacje języków C# i YB .NET dostarczane przez firmę Microsoft są mniej więcej porównywalne, jednak w inter­ necie programista C# znajdzie znacznie więcej materiałów. Pamiętając o tym, możemy się już przyjrzeć podstawowym elementom programów pisanych w języku C#.

28

Rozdział 1. Prezentacja C#

ROZDZIAŁ 2.

Podstawowe techniki programowania

Aby móc korzystać z języka programowania, należy doskonale opanować jego podstawy. Konieczne jest zrozumienie elementów niezbędnych do stworzenia działającego programu oraz poznanie sposobów korzystania ze środowiska programistycznego służącego do tworzenia i uruchamiania kodu. Należy także dobrze poznać najpopularniejsze możliwości reprezento­ wania informacji, wykonywania obliczeń oraz podejmowania decyzji. W tym rozdziale zostaną zaprezentowane właśnie te podstawowe cechy języka C#.

Początki Podczas nauki C# będziemy korzystali z Visual Studio - środowiska programistycznego firmy Microsoft. Istnieją także inne sposoby tworzenia i budowania programów w C#, jednak Visual Studio jest najpopularniejszym narzędziem, a co więcej jest ogólnodostępne, więc skorzystamy z niego. '

Jeśli Czytelnik nie posiada Visual Studio, może pobrać jego wersję Express ze strony

. .



http://www.microsoft.com/express/.

'

....'' .___...z.r_

W pierwszej części tego rozdziału utworzymy bardzo prosty program, tak by Czytelnik mógł zapoznać się z absolutnie najprostszymi czynnościami wymaganymi do przygotowania i uru­ chomienia programu. Opiszemy także wszystkie elementy, które Visual Studio tworzy, tak byśmy dokładnie wiedzieli, co środowisko programistyczne dla nas robi. I w końcu napi­ szemy nieco bardziej skomplikowany przykład, który posłuży nam do lepszego poznania języka C#. Aby utworzyć nowy program w C#, należy wybrać z menu głównego opcję File/New Project bądź też skorzystać z kombinacji klawiszy Ctrl+Shift+N. W efekcie wyświetlone zostanie okno dialogowe New Project przedstawione na rysunku 2.1, w którym można wybrać rodzaj tworzo­ nego programu. Należy zwrócić uwagę, by na liście Installed Templates wyświetlanej w lewej kolumnie okna została zaznaczona rozwinięta zawartość opcji Visual C# i zaznaczona opcja Windows - aplikacje, które będą uruchamiane na lokalnym komputerze, są najłatwiejsze do napisania. W dalszej części książki przyjrzymy się także innym rodzajom programów takim jak aplikacje internetowe .

29

New

Project

I Search Instal led Templ ates

Recent Tem f>I at es Installed Tern piates " Visual C# Wi n d ows Web

I

� Offi ce Cl' ol!.J dl Rep orting

Si lverl i g hl

� �

Test WCF Wo•kfl ow

� Other P roject Typ es D ata base

Na me: L o c ation: Solution n a m e:

il �€1' !I

� Share� oi nt

� Other La ngu·a g es

� Gif]



� � �

Win dows. Fmms. App licat:i on

Vis.U' al C#

WPf Ap pl icati on

Visu al C#

Consol e App li cati o· n

Visu al (#

Cl1ass Library

I Con sol e Application �

Visu·al C#

WPf Bmwse• App l i:catio n

Visu al C#

Empty Prnj ect

Visu al C#

Win dows Servic e

Visu al C#

WPf CUi>l:om Contro l Lib ra ry

Visu al C#

WPf User Control Li b"ry

Visu al C#

Win dows Fo•ms Contro l Li b ra ry

Visu·al C#

Ty,pe: Visu a l C#

A p roj ect for creati ng a com rn a n d-lin e a p pl icati on

I

c on so l e p l i c ati o A p_ _ _..:. _ _ _ _ .:. _ _ n 1_ _ _ _ _ _ _ _ -= == == == == == == == == � c:\ u :< ers\ p8r\ d o c u m ents\vi s ual studio 2010\ Proj ects •

..-..;,_________.;..._ ._ -=__________________...., ConooleAp p l icationl

I

Browse„.

[] C reate d i rectory for so·lution [] Add to source co ntro·I

Rysunek 2 . 1 . Okno dialogowe New Project w Visual Studio 2010 W środkowej części okna dialogowego należy wybrać szablon Console Application . Służy on do tworzenia aplikacji w starym stylu, działających z poziomu wiersza poleceń. Nie jest to co prawda najbardziej fascynujący typ programów, jednak najłatwiej go utworzyć i zrozumieć, dlatego właśnie zaczniemy od niego. Teraz należy podać nazwę programu - domyślnie Visual Studio zasugeruje jakąś bardzo mało oryginalną taką jak ConsoleApplication1 . W polu Name u dołu okna dialogowego wpiszmy zatem HelloWorld. (No dobrze, ta nazwa jest równie mało oryginalna, niemniej jednak jest nieco bardziej opisowa) . Oprócz tego Visual Studio chce wiedzieć, gdzie na twardym dysku chcemy umieścić projekt. Dodatkowo można zażądać utworzenia osobnego katalogu „rozwiązania" (ang. solution) . Z tej opcji można skorzystać w większych programach składających się z wielu komponentów, jednak w tak prostym przykładzie jak ten, który chcemy utworzyć, pole wyboru Create directory for solution lepiej zostawić niezaznaczone . Po kliknięciu przycisku OK Visual Studio utworzy nowy projekt - kolekcję plików używanych do utworzenia programu. Projekty C# zawsze zawierają pliki źródłowe, jednak czasami umiesz­ cza się w nich także pliki innych typów, na przykład mapy bitowe. Nowo utworzony projekt będzie zawierał plik źródłowy C# o nazwie Program.es, który zostanie od razu wyświetlony w edytorze Visual Studio. Na wypadek gdyby Czytelnik nie wykonywał opisywanych czyn­ ności w Visual Studio od razu podczas lektury tego rozdziału, listing 2.1 przedstawia kod naszego nowego programu. Warto także zauważyć, że nazwa pliku źródłowego - Program.es - nie ma szczególnego znaczenia . Visual Studio nie zwraca uwagi na nazwy plików źródłowych. Domyślnie mają one rozszerzenie .es, co jest skrótem od C#, niemniej jednak jest ono całko­ wicie opcjonalne. 30

Rozdział 2. Podstawowe techniki programowania

Listing 2 . 1 . Kod nowej aplikacji konsolowej us i ng us i ng us i ng us i ng

Sys tem ; Sys tem . Col l ect i ons . Generi c ; Sys tem . L i n ą ; Sys tem . Text ;

namespace Hel l oWorl d { cl ass Program { stat i c voi d Mai n (stri ng O arg s ) { }

Ten program jeszcze nic nie robi. Aby zmienić go w standardową pierwszą aplikację, należy dodać do niego jeden wiersz kodu. Trzeba go będzie umieścić pomiędzy dwoma wierszami zawierającymi najbardziej wciętą parę nawiasów klamrowych ( { oraz } ) . Zmodyfikowana wersja programu została przedstawiona na listingu 2.2, a nowy wiersz kodu wyróżniono w nim pogrubioną czcionką.

Listing 2 .2 . Tradycyjny pierwszy przykład - Witaj, świecie us i ng us i ng us i ng us i ng

Sys tem ; Sys tem . Col l ect i ons . Generi c ; Sys tem . L i n ą ; Sys tem . Text ;

namespace Hel l oWorl d { cl ass Program { stat i c voi d Mai n (stri ng O arg s ) { Consol e . Wri teLi ne ( "Wi taj , §wi eci e " ) ;

Nasz przykład jest już gotowy do uruchomienia . Należy z menu Debug wybrać opcję Start Without Debugging lub nacisnąć kombinację klawiszy Ctrl+F5. W efekcie program zostanie uru­ chomiony, a ponieważ napisaliśmy go jako aplikację konsolową, zostanie wyświetlone okno wiersza poleceń. Pierwszy wiersz wyświetlony w oknie będzie zawierał tekst „Witaj, świecie", w kolejnym znajdzie się natomiast następująca informacja: „Aby kontynuować, naciśnij dowolny klawisz . . . " . Kiedy Czytelnik skończy już podziwiać efekty swej kreacji, wystarczy nacisnąć dowolny klawisz by zamknąć okno. Nie należy używać opcji Debug/Start Debugging lub naciskać klawisza F5, spowodo­ wałoby to bowiem uruchomienie Visual Studio w trybie debugowania, w którym po zakończeniu wykonywania aplikacji okno wiersza poleceń jest automatycznie zamy­ kane. W naszym przypadku nie byłoby to rozwiązanie szczególnie użyteczne, gdyż aplikacja zapewne zakończyłaby działanie i okno zostałoby zamknięte, zanim w ogóle mielibyśmy szansę zobaczyć wyświetlone wyniki.

Początki

31

Teraz, skoro już dysponujemy kompletnym programem, przeanalizujmy jego kod, by zobaczyć, jakie jest przeznaczenie jego poszczególnych fragmentów - wszystkie z nich będą używane za każdym razem podczas pisania programów w C#. Zaczynając od góry, na samym początku pliku Program.es znajduje się kilka wierszy rozpoczynających się od słowa kluczowego u s i ng: us i ng us i ng us i ng us i ng

Sys tem ; Sys tem . Col l ect i ons . Generi c ; Sys tem . L i n ą ; Sys tem . Text ;

Te dyrektywy us i ng pomagają kompilatorowi C# pracować z kodem zewnętrznym, który będzie używany w danym pliku źródłowym. Żaden kod nie jest całkowicie niezależny - aby wyko­ nać jakieś użyteczne zadanie, nasze programy będą zleżeć od kodu zewnętrznego i z niego korzystać. Wszystkie programy pisane w języku C# są zależne od biblioteki klas platformy .NET. Oto przykład: wiersz kodu, który dodaliśmy do naszego programu, korzysta z biblioteki klas, by wyświetlić komunikat. Dyrektywy us i ng pozwalają zadeklarować chęć wykorzystania klas pochodzących z dowolnej biblioteki - naszej własnej, napisanej przez Microsoft lub utworzonej przez dowolnego innego programistę . Wszystkie dyrektywy użyte w naszym przykładzie rozpoczynają się od słowa System, które oznacza, że chcemy używać klas pochodzą­ cych z biblioteki platformy .NET Framework. Cały tekst umieszczony po słowie kluczowym u s i ng określa przestrzeń nazw (ang. namespace) .

Przestrzenie nazw i typy Biblioteka klas .NET Framework jest bardzo duża . Aby ułatwić nam lokalizowanie dostęp­ nych wewnątrz niej usług, podzielono ją na przestrzenie nazw. Na przykład przestrzeń nazw System . I O zawiera usługi związane z obsługą wejścia-wyjścia, takie jak operacje na plikach prze­ chowywanych na dysku, natomiast przestrzeń nazw System . Data . Sql Cl i ent służy do nawiązy­ wania połączeń z serwerem baz danych SQL Server. Przestrzeń nazw zawiera z kolei typy. Typ zazwyczaj reprezentuje pewien rodzaj informacji lub obiektu. Dostępne są na przykład typy reprezentujące podstawowe formy informacji stosowane w programach, takie jak Sys t em . Stri n g reprezentujący łańcuchy znaków, lub przeróżne typy numeryczne, takie jak System . Doubl e czy System . I n t 3 2 . Niektóre typy są bardziej skompliko­ wane - na przykład klasa Sys t em . Ht t pWebReq uest reprezentuje żądanie HTTP, które ma zostać przesłane na serwer. Istnieje także kilka typów, które nie reprezentują niczego konkretnego, a jedynie udostępniają pewną grupę usług; przykładem może tu być klasa Sys t em . Ma t h, która udostępnia funkcje matematyczne, takie jak S i n lub Log, oraz stałe: n lub e - podstawę loga­ rytmu naturalnego. (Zagadnieniami związanymi z typami, obiektami oraz wartościami zaj­ miemy się szczegółowo w następnym rozdziale) . Wszystkie klasy wchodzące w skład biblioteki .NET Framework należą do jakiejś przestrzeni nazw. Dyrektywa u s i ng pozwala nam uniknąć konieczności każdorazowego podawania nazwy przestrzeni nazw, kiedy używamy klasy. Na przykład w pliku zawierającym dyrektywę us i ng System można napisać Mat h . PI, by użyć wartości liczby n; gdyby tej dyrektywy nie było, trzeba by napisać Sys t em . Ma t h . P I . Nie ma wymogu stosowania dyrektyw u s i n g . Co więcej, jeśli Czy­ telnik lubi dużo pisać, to nic nie stoi na przeszkodzie, by wszędzie stosował pełne nazwy klas. Niemniej jednak, ponieważ niektóre przestrzenie nazw mają całkiem długie nazwy - na przy­ kład System . Wi ndows . Med i a . Imag i ng - łatwo zauważyć, że skrótowy zapis, który można stosować dzięki dyrektywie u s i ng, może znacząco uprościć kod programu.

32

Rozdział 2. Podstawowe techniki programowania

Można się zastanawiać, po co w ogóle używane są przestrzenie nazw, skoro pierwszą rzeczą, jaką zazwyczaj robimy, pisząc nowy plik źródłowy, jest umieszczenie w nim dyrektyw u s i ng w celu uniknięcia konieczności określania przestrzeni nazw. Powodem jest uściślanie - niektóre nazwy typów pojawiają się w wielu miejscach. Na przykład platforma ASP.NET zawiera typ o nazwie Con t rol , który występuje jednak także w WPF oraz w Windows Forms. Wszystkie te typy reprezentują podobną ideę, lecz są stosowane w całkowicie odmiennych kontekstach (w aplikacjach internetowych lub klasycznych aplikacjach przeznaczonych dla systemu Win­ dows) . Choć każdy z tych typów ma nazwę Control , są one czymś całkowicie różnym, gdyż należą do innych przestrzeni nazw. To uściślanie zapewnia także programistom możliwość stosowania w swoim kodzie całkowicie dowolnych nazw, nawet jeśli są one już używane w różnych miejscach biblioteki klas .NET Framework, o których istnieniu nawet nie mamy pojęcia. Ponieważ biblioteka ta zawiera ponad 10 tysięcy typów, jest całkiem prawdopodobne, że zdarzy się nam wybrać już używaną nazwę, jednak dzięki zastosowaniu przestrzeni nazw nie jest to wielkim problemem. Przykła­ dowo w .NET istnieje klasa o nazwie Bo l d, jeśli jednak nie korzystamy z fragmentu biblioteki, do którego ona należy (a jest ona związana z usługami tekstowymi oferowanymi przez WPF), to możemy jej używać we własnym kodzie w jakimś innym celu. Typ Bal d .NET należy do prze­ strzeni nazw Sy s t em . W i ndows . Do c umen t s, więc o ile tylko nie podamy jej w dyrektywie u s i ng, będziemy mogli nadać nazwie Bal d dowolne inne znaczenie. Nawet kiedy niejednoznaczności nie występują, to przestrzenie nazw pomagają w przeglą­ daniu i poszukiwaniu klas w bibliotece .NET, gdyż powiązane ze sobą typy są zazwyczaj umieszczane w jednej przestrzeni nazw lub grupowane w kilku przestrzeniach powiązanych ze sobą. (Dostępnych jest na przykład wiele przestrzeni nazw rozpoczynających się od System . Web zawierają one typy używane przy pisaniu aplikacji internetowych w technologii ASP .NET) . A zatem zamiast przeszukiwać tysiące typów w poszukiwaniu tego jednego lub kilku, których potrzebujemy, wystarczy przeglądnąć przestrzenie nazw - tych jest tylko kilkaset . .

.„ , •

'..��,·

""•

L---� '

Na stronie http://msdn.microsoft.com/library/ms229335 można znaleźć kompletną listę przestrzeni nazw dostępnych w bibliotece klas .NET Framework wraz z krótkim opisem przeznaczenia każdej z nich.

Visual Studio dodaje do pliku Program.es należącego do nowego projektu aplikacji konsolo­ wej cztery dyrektywy u s i ng. Przestrzeń nazw Sys t em zawiera usługi ogólnego przeznaczenia, w tym podstawowe typy danych, takie jak Stri ng, oraz różne typy numeryczne. Należy do niej także klasa Consol e, której nasz program używa do wyświetlenia powitania i która udostępnia także inne usługi przydatne aplikacjom konsolowym takie jak odczyt danych z klawiatury oraz określanie koloru wyświetlanych danych wynikowych. Pozostałe trzy dyrektywy us i ng nie będą wykorzystywane w naszym przykładzie. Visual Studio dodaje je, gdyż jest duże prawdopodobieństwo, że w większości aplikacji okażą się one uży­ teczne . Przestrzeń nazw Sys t em . Cal l e c t i a n s . Generi c zawiera typy przeznaczone do pracy z kolekcjami takimi jak listy liczb . Przestrzeń nazw Sys t em . L i n q zawiera typy używane przez LINQ udostępniające wygodne sposoby przetwarzania kolekcji informacji. Przestrzeń System . Text zawiera natomiast typy przeznaczone do wykonywania operacji na tekście. Dyrektywy u s i n g , które Visual Studio dodaje do nowych plików źródłowych C#, są w nich umieszczane wyłącznie po to, by zaoszczędzić programistom trochę pisania. Jeśli się zdarzy, że nie będziemy używać tych przestrzeni, nic nie stoi na przeszkodzie, by je usunąć. Oczywiście można je także dodawać.

Przestrzenie nazw i typy

33

Usuwan ie n iepożądanych dyrektyw using Istnieje szybszy sposób usuwania niepożądanych dyrektyw usi ng. Kiedy klikniemy prawym przy­ ciskiem myszy w dowolnym miejscu kodu C#, w wyświetlonym menu kontekstowym będzie dostępna opcja Organize Usings. Po jej wybraniu zostanie wyświetlone podmenu zawierające opcję Remove Unused Usings - określa ona, które dyrektywy usi ng nie są potrzebne w naszym kodzie, a następnie je usuwa. Opisywane podmenu zawiera opcje, które przypadną do gustu wszystkim programistom lubiącym porządek w kodzie. Opcja Remove and Sort pozwala na usunięcie niepotrzebnych dyrektyw usi ng i posortowanie pozostałych w kolejności alfabetycznej . Całe menu kontekstowe wraz z opcjami porządkującymi dyrektywy us i ng zostało przedstawione na rysunku 2.2.

l

1 �

El u s ng Sys t e m ; u s ng Sys t e m .

(,� H ec t i o ns . Ge n eric ;

u s r n g Sys t e m . Li n q ; u s ing sys t e m . Text ;

El n a me s p a c e H ell oworl d

t

I{ I {

El

c la s s Program

;

" ' '" �

id

"' '' 0

't] � „o

�. Al,

;>:§

Refacto r O rg an ize u,in g> C reate U n it Terts„.

�(}

Remove U n u >ed U >i ng >

�O

Sort U >i n g >



Generate Sequ ence D i a g ram „ .

em o,ve a n d

ln>ert Sn i p p et„ .

Ctrl + K, X

Surround With „ .

Ctrl + K, 5

G o T o D efi n iti o n

F12

Fi n d All Refer en ces

Ctrl + K, R

View C all H i erarc hy

Ctrl + K, Ctrl + T

[)°

rt

B rea kp o i nt Run To C u rso r

Ctrl + FlO

Cut

Ctrl + X

C o py

Ctrl + C

Parte

Ctrl+\/

O utl i n i n g

Rysunek 2 .2 . Usuwanie niepotrzebnych dyrektyw using Opisane wcześniej dyrektywy u s i ng to niejedyne elementy naszego krótkiego przykładowego programu, które mają związek z przestrzeniami nazw. Okazuje się bowiem, że kolejny wiersz kodu, umieszczony poniżej ostatniej z dyrektyw, także jest z nimi związany: namespace Hel l oWorl d {

O ile dyrektywy us i ng określają, z jakich przestrzeni nazw nasz kod będzie korzystał, to słowo kluczowe namespace informuje kompilator, jaką przestrzeń nazw będziemy definiować w kodzie typy tworzone w naszych programach także należą do przestrzeni nazw, podobnie jak typy 1 w bibliotece klas .NET . W naszym przypadku Visual Studio założyło, że będziemy chcieli umieścić nasz kod w przestrzeni nazw, której nazwa odpowiada nazwie projektu. To roz­ wiązanie jest stosowane bardzo często, choć nazwy przestrzeni nazw mogą być całkowicie dowolne - nie ma wymogu, by odpowiadały nazwom programów.

1 Precyzyjnie rzecz ujmując, można pominąć określenie przestrzeni nazw. W takim przypadku typy definio­ wane w kodzie zostaną umieszczone w tak zwanej globalnej przestrzeni nazw. Takie rozwiązanie jest jednak uznawane za złą praktykę programistyczną, gdyż zazwyczaj będziemy chcieli, by nasz kod korzystał z tych samych zalet przestrzeni nazw, które są udziałem klas biblioteki NET .

34

Rozdział 2. Podstawowe techniki programowania

.

Kompilator C# pozwala nawet umieszczać własny kod w przestrzeniach nazw zaczy­ nających się od słowa System, jednak nie należy tego robić (chyba że pracujemy dla firmy Microsoft i dodajemy typy do kolejnych wersji biblioteki klas platformy .NET). Zła­ manie konwencji, w myśl której w przestrzeni nazw zaczynającej się od słowa Sys tern znajdują się typy należące do .NET Framework, mogłoby wprowadzić spore zamieszanie.

Należy zauważyć, że za nazwą przestrzeni nazw znajduje się otwierający nawias klamrowy ( { ) . W języku C# nawiasy klamrowe są używane do określania relacji zawierania - w tym przy­ padku cały kod napisany za tym nawiasem otwierającym będzie umieszczony w przestrzeni nazw Hel l oWorl d . Przestrzenie nazw zawierają typy, zatem fakt, że w kolejnym wierszu kodu została umieszczona definicja typu, nie powinien być dla Czytelnika wielkim zaskoczeniem. Konkretnie rzecz biorąc, jest to definicja klasy. Biblioteka .NET Framework nie jest jedynym miejscem, gdzie są definiowane klasy. W rze­ czywistości, chcąc napisać jakikolwiek kod w języku C#, trzeba utworzyć klasę, w której zosta­ nie on umieszczony. Niektóre języki programowania (takie jak C ++) nie narzucają takiego wymogu, jednak C# jest językiem obiektowym (ang. object oriented, w skrócie OO) . Pojęcia związane z obiektowością zostały bardziej szczegółowo opisane w następnym rozdziale w tym przypadku najważniejszą konsekwencją obiektowości jest dla nas to, że każdy fragment kodu C# musi być umieszczony wewnątrz jakiegoś typu. W C# istnieje kilka różnych sposobów definiowania typów. Zostały one dokładniej opisane w następnym rozdziale; na potrzeby naszego prostego przykładu wskazywanie różnic pomię­ dzy nimi nie jest konieczne. A zatem użyliśmy tu najprostszego sposobu - klasy: cl ass Program {

Także tutaj należy zwrócić uwagę na nawiasy klamrowe - podobnie jak zawartość prze­ strzeni nazw, zawartość klas jest zapisywana pomiędzy parą tych nawiasów. Wciąż jeszcze nie udało się nam dojść do samego kodu naszego przykładowego programu. Jest on umieszczony wewnątrz klasy, a konkretnie należy go umieścić wewnątrz jakiejś metody, która będzie umieszczona w klasie . Metoda to blok kodu opatrzony nazwą, który opcjonalnie zwraca jakieś dane. Klasa zastosowana w naszym przykładzie definiuje metodę o nazwie Ma i n i także w tym przypadku początek oraz koniec kodu metody zostały oznaczone przy użyciu pary nawiasów klamrowych: s t at i c vo i d Mai n (s tri ng O arg s ) {

Pierwsze słowo kluczowe rozpoczynające definicję metody - stat i c - informuje, że nie ma potrzeby tworzyć obiektu typu Prog ram (przypominamy, że Prog ram jest klasą zawierającą metodę Mai n ) , by z tej metody skorzystać. Jak się przekonamy w następnym rozdziale, w bardzo wielu przypadkach do wywołania metody potrzebne są obiekty, jednak nie dotyczy to naszego przykładu. Kolejnym słowem kluczowym jest voi d . Informuje ono kompilator, że nasza metoda nie zwraca żadnych danych - po prostu robi, co do niej należy. Jednak wiele metod zwraca jakieś infor­ macje. Na przykład metoda Cos klasy Sys t em . Ma t h wylicza wartość cosinusa podanego kąta,

Przestrzenie nazw i typy

35

a ponieważ nie ma pojęcia, co chcemy z tą wartością zrobić, po prostu ją zwraca jako wynik wywołania . Kod naszego przykładu jest bardziej zdecydowany i aktywny - jego zadaniem jest wyświetlenie komunikatu na ekranie, więc nie musi niczego zwracać2 • W przypadku metod zwracających jakieś dane należałoby dodatkowo określić ich typ, lecz ponieważ w naszym przykładzie metoda nic nie zwraca, fakt braku wartości wynikowej jest sygnalizowany przy użyciu słowa kluczowego voi d . Kolejny element kodu - M a i n - to nazwa metody. W tym przypadku nazwa ta ma szczególne znaczenie - kompilator C# oczekuje, że tworzony program będzie zawierał dokładnie jedną statyczną metodę o nazwie Mai n, którą wykona podczas jego uruchamiania. Po nazwie metody podawana jest lista parametrów deklarująca oczekiwane dane wejściowe . W naszym przykładzie lista parametrów ma następującą postać: (stri ng [] args ) . Oznacza to, że metoda Ma i n wymaga jednej danej wejściowej, do której w kodzie będziemy się odwoływali, używając nazwy a rg s . Dodatkowo metoda oczekuje, że dana ta będzie zawierać sekwencje łańcuchów znaków (dodatkowe nawiasy kwadratowe umieszczone za nazwą sugerują, że może zostać przekazany nie jeden, lecz kilka łańcuchów) . Przypadkowo okazuje się, że nasz przykładowy program nie używa tych danych wejściowych, niemniej jednak są one standar­ dową cechą metody Mai n - to właśnie za ich pomocą są przekazywane do programu argu­ menty podawane w wierszu wywołania . Wrócimy do tego zagadnienia w dalszej części roz­ działu przy okazji pisania programu, który wykorzystuje takie argumenty; program, który aktualnie analizujemy, na razie z nich nie korzysta. A zatem udało się nam dotrzeć do ostat­ niego elementu naszego przykładu - kodu umieszczonego wewnątrz metody Mai n, który był jedynym fragmentem dodanym przez nas do pliku wygenerowanego przez Visual Studio i który jest jedyną rzeczą wykonywaną przez program: Consol e . Wri teli ne ( "Wi taj , świ eci e " ) ;

Powyższy wiersz kodu przedstawia składnię używaną w C# do wywoływania metod. W tym przypadku korzystamy z metody udostępnianej przez klasę Consol e należącą do biblioteki .NET Framework i zdefiniowaną w przestrzeni nazw System. W jej wywołaniu można by zasto­ sować w pełni kwalifikowaną nazwę; w takim przypadku wywołanie przybrałoby następującą postać: Sys tem . Consol e . Wri t e l i ne ( "Wi taj , świ eci e " ) ;

Dzięki wcześniejszemu zastosowaniu dyrektywy us i ng Sy stem można jednak zastosować zapis skrócony - ma on dokładnie to samo znaczenie, lecz jest bardziej zwięzły . Klasa Con s o l e zapewnia możliwość wyświetlania tekstów w oknie konsoli oraz wczytywania danych teksto­ wych wpisywanych przez użytkownika w tradycyjnych aplikacjach wykonywanych z poziomu wiersza poleceń systemu operacyjnego. W naszym przykładzie wywołujemy metodę Wri tel i ne, przekazując do niej łańcuch znaków "Wi taj , świ eci e " . Metoda ta wyświetla dowolny przekazany w jej wywołani u tekst w oknie konsoli .

2 Swoją drogą jest to kluczowa różnica pomiędzy

funkcyjnym oraz proceduralnym stylem tworzenia kodu. Kod, który wykonuje obliczenia i zwraca wyniki, jest nazywany funkcyjnym, gdyż w swej naturze jest zbli­ żony do funkcji matematycznych takich jak cosinus lub pierwiastek kwadratowy. Z kolei kod proceduralny zazwyczaj realizuje sekwencję czynności. W niektórych językach, takich jak F#, dominuje styl funkcjonalny, jednak kod programów C# zazwyczaj stanowi mieszankę obu tych stylów.

36

Rozdział 2. Podstawowe techniki programowania

'

. .

, ..._-__-� ·

Czytelnik zapewne zauważył, że znak kropki ( . ) został tu zastosowany w innym znaczeniu. Można go użyć, by rozdzielić nazwę przestrzeni nazw oraz typu - na przykład System . Consol e oznacza typ Consol e zdefiniowany w przestrzeni nazw System. Ten sam znak może także zostać użyty do podzielenia nazwy przestrzeni nazw, jak w zapisie: System. I O. Natomiast znak kropki zastosowany w naszym przykładzie ozna­ cza, że chcemy wywołać konkretną metodę udostępnianą przez klasę - Consol e . Wri teli ne. Jak się okaże w dalszej części książki, znak kropki jest stosowany w języku C# także do kilku innych celów. Ogólnie rzecz ujmując, oznacza on, że chcemy użyć czegoś, co znajduje się wewnątrz czegoś innego. Jego dokładne znaczenie kompilator C# określa na podstawie kontekstu.

Choć przeanalizowaliśmy każdy wiersz kodu naszego prostego przykładu, nie wyjaśniliśmy do końca, co Visual Studio robi w naszym imieniu, gdy prosimy je o utworzenie nowej apli­ kacji. Aby w pełni docenić jego znaczenie, musimy przestać analizować sam plik źródłowy Program.es i przyjrzeć się całemu projektowi.

Projekty i sol ucje Rzadko kiedy użyteczne programy są tak proste, by ich cały kod źródłowy zmieścił się w jed­ nym pliku. Czasami można natknąć się na tak koszmarne rozwiązania jak pojedynczy plik źródłowy zawierający dziesiątki tysięcy wierszy kodu, jednak mając na względzie jakość (oraz własne zdrowie psychiczne), znacznie lepiej jest przechowywać kod w mniejszych fragmentach, którymi można łatwiej zarządzać - większe i bardziej złożone fragmenty kodu niosą ze sobą większe ryzyko występowania błędów. Dlatego też Visual Studio zostało stworzone z myślą o zarządzaniu większą liczbą plików źródłowych i wykorzystuje dwa rozwiązania ułatwiające określanie struktury programów składających się z wielu plików: projekty oraz solucje (ang. solutions) . Projekt jest kolekcją plików źródłowych, które kompilator C# łączy ze sobą, by wygenerować pojedynczy wynik - zazwyczaj program wykonywalny lub bibliotekę . (Więcej informacji o procesie kompilacji można znaleźć w ramce umieszczonej na następnej stronie) . Zgodnie z konwencją przyjętą w systemie Windows programy wykonywalne mają rozszerzenie .exe, natomiast biblioteki rozszerzenie .dll (są to skróty od angielskich słów: executable - wykony­ walny, oraz dynamie link library - biblioteka dołączana dynamicznie) . Pomiędzy tymi dwoma rodzajami plików nie ma zbyt dużej różnicy. Największą jest to, że program wykonywalny musi posiadać punkt wejścia, czyli metodę Ma i n . Z kolei biblioteka nie jest przeznaczona do samodzielnego wykonywania. Jest ona tworzona po to, by można było z niej korzystać w innych programach; dlatego też biblioteki DLL nie posiadają własnego punktu wejścia. Oprócz tej jednej różnicy oba rodzaje plików generowanych przez kompilator są praktycznie takie same są to po prostu pliki zawierające kod oraz dane. (W rzeczywistości są one do siebie tak podobne, że istnieje możliwość używania programów wykonywalnych tak, jak gdyby były one bibliote­ kami) . Dlatego też projekty w Visual Studio działają tak samo w przypadku tworzenia progra­ mów i bibliotek. Solucja (ang. solution) to kolekcja powiązanych ze sobą projektów. Jeśli piszemy bibliotekę, to zapewne będziemy chcieli także napisać aplikację, która będzie z niej korzystać. Nawet jeśli biblioteka ta ma być docelowo używana przez innych, to i tak będziemy chcieli mieć możliwość wypróbowania jej, na przykład w celach testowych, dlatego też dobrze będzie mieć jedną lub

Projekty i solucje

37

Kod źródłowy, binarny i kompilacja Pliki .exe oraz .dll generowane przez Visual Studio nie zawierają kodu źródłowego. Kiedy przejrzymy plik HelloWorld.exe wygenerowany po skompilowaniu naszego przykładu, nie będzie on bynajmniej zawierał kopii kodu źródłowego umieszczonego w pliku Program.es. C# jest bowiem językiem kom­ pilowanym, co oznacza, że w ramach procesu tworzenia programu jego kod źródłowy jest tłumaczony na postać binarną, którą komputer może znacznie łatwiej wykonać. Visual Studio skompilowało nasz kod automatycznie, kiedy zażądaliśmy wykonania programu. Jednak nie wszystkie języki programowania działają w taki sposób. Na przykład język JavaScript, używany do tworzenia dynamicznych rozwiązań na stronach WWW, nie wymaga kompilacji przeglądarka pobiera kod źródłowy wszelkich niezbędnych skryptów, a następnie bezpośrednio je wykonuje. Rozwiązanie takie ma jednak kilka wad. Przede wszystkim kod źródłowy może być dosyć długi. Taki kod musi być czytelny zarówno dla ludzi, jak i dla komputerów, gdyż kiedy trzeba go zmodyfikować, programista musi go najpierw zrozumieć. Komputery mogą natomiast pracować z informacjami zapisanymi w niezwykle zwartym binarnym formacie, dzięki czemu kod skompilowany może być znacznie krótszy od źródłowego, co z kolei sprawia, że zajmuje on mniej miejsca na dysku i można go pobrać znacznie szybciej . Poza tym przetworzenie kodu źródłowego, czyli zapisanego w formacie czytelnym dla człowieka, wymaga od komputera stosunkowo dużego nakładu pracy - komputery radzą sobie bowiem znacznie lepiej z danymi binarnymi niż z tekstem. Kompilacja zapewnia możliwość przekonwer­ towania całego kodu źródłowego zrozumiałego dla człowieka na postać, która będzie znacznie wygodniejsza dla komputerów. Właśnie dlatego kod skompilowany zazwyczaj jest wykonywany znacznie szybciej niż rozwiązania operujące bezpośrednio na kodzie źródłowym. (W rzeczywistości, choć JavaScript nie był projektowany jako język kompilowany, jego nowoczesne środowiska wyko­ nawcze przed wykonaniem pobranego skryptu kompilują go, by uzyskać większą szybkość dzia­ łania. Jednak takie rozwiązanie jest mniej korzystne niż to obecne w C#, gdzie kompilacja jest częścią procesu tworzenia programu, gdyż podczas pierwszego uruchamiania skryptu użytkownik musi czekać na jego pobranie i skompilowanie). Niektóre języki umożliwiają kompilację kodu do rodzimego języka maszynowego - kodu binarnego, który może zostać wykonany bezpośrednio przez procesor komputera. Oczywiście pozwala to na poprawienie wydajności, bowiem wykonanie kodu skompilowanego w taki właśnie sposób nie wymaga żadnych dodatkowych przygotowań. Jednak języki używane przez platformę .NET nie działają w taki sposób, gdyż rozwiązanie to ogranicza późniejsze możliwości wykonywania kodu na różnych platformach systemowych. Jak już wspominaliśmy w poprzednim rozdziale, progra­ my pisane w językach platformy .NET są kompilowane do tak zwanego języka pośredniego (ang. Intermediate Language, w skrócie IL). Jest to binarna reprezentacja kodu, a zatem jest ona bardzo zwię­ zła i zapewnia wysoką wydajność działania, niemniej jednak nie jest dostosowana do żadnego kon­ kretnego typu procesora. Dzięki temu programy przeznaczone dla platformy .NET mogą działać na komputerach wyposażonych zarówno w procesory 32-, jak i 64-bitowe, a także w systemach kom­ puterowych o różnej architekturze. Platforma .NET konwertuje kod IL na język maszynowy bezpo­ średnio przed jego wykonaniem. Technika ta jest określana mianem kompilacji na bieżąco - Just In Time (w skrócie JIT). Ten rodzaj kompilacji daje korzyści typowe dla obu opisywanych wcześniej sposobów wykonywania programów - zapewnia znacznie większą szybkość działania programów niż kompilacja z kodu źródłowego, a j ednocześnie pozwala zachować elastyczność niezbędną do wykonywania programów na komputerach różnych typów.

nawet kilka aplikacji sprawdzających jej możliwości funkcjonalne. Po umieszczeniu wszystkich tych projektów w jednej solucji będzie można jednocześnie pracować zarówno nad biblioteką, jak i nad aplikacjami używanymi do jej testowania. Swoją drogą, Visual Studio zawsze wymaga

38

Rozdział 2. Podstawowe techniki programowania

zastosowania solucji - nawet jeśli pracujemy tylko nad jednym projektem, to i tak zostanie on w niej umieszczony . To właśnie z tego powodu zawartość projektów jest pokazywana w panelu o nazwie Solution Explorer, co pokazaliśmy na rysunku 2.3. 5o l utio n Explon�r

l:d Solutio·n ' H el l oWo rld ' (1 proj ect)

""

H elloWorld

"" ""



P ro-p erti es

As�em blylnfo . c s

Ref er en c es

-O -O -O -O

M i croso.ft.C:5harp System System.Co re System.D ata

-O

System, D ata, DataSetExtensi ons

-O

System.Xm l. Li nq

-O

System.Xm l

'1!l P roo g r.am . cs

Rysunek 2 .3. Projekt HelloWorld wyświetlony w panelu Solution Explorer '

.

.

Niektóre typy projektów dostępnych w Visual Studio nie generują ani programów wykonywalnych, ani bibliotek. Na przykład dostępny jest typ projektów służący do tworzenia plików .msi (instalatora Windows) na podstawie wyników wygenerowanych przez inne projekty. A zatem, precyzyjnie rzecz ujmując, projekt jest ideą dosyć abstrak­ cyjną: operuje on na pewnej grupie plików i na ich podstawie generuje jakieś wyniki. Niemniej jednak projekty zawierające kod C# będą zazwyczaj generować bądź to pliki EXE, bądź biblioteki DLL.

Panel Solution Explorer zazwyczaj jest umieszczony z prawej strony Visual Studio, jeśli jednak nie jest widoczny, to zawsze można go otworzyć, wybierając z menu głównego opcję View/Solution Explorer. W tym oknie wyświetlane są wszystkie projekty znajdujące się w solucji - w naszym przypadku jest to jedynie projekt Hello World. Oprócz tego są w nim widoczne wszystkie pliki należące do solucj i . U dołu okna przedstawionego na rysunku 2 .3 można zobaczyć plik Program.es, którym zajmowaliśmy się wcześniej . Nieco wyżej widoczny jest dodatkowy plik Assemblyinfo.cs, którego jeszcze nie przedstawiliśmy. Gdybyśmy go otworzyli, przekonalibyśmy się, że Visual Studio umieściło w nim informacje dotyczące numeru wersji aplikacji oraz praw autorskich - informacje te zostaną wyświetlone, kiedy użytkownik spróbuje obejrzeć właści­ wości skompilowanego programu w Eksploratorze Windows. '

.

.

Może się okazać, że na komputerze Czytelnika w panelu Solution Explorer nie będzie widoczny węzeł Solution (znajdujący się w górnej części rysunku 2.3), a jedynie węzeł projektu HelloWorld. Otóż Visual Studio można skonfigurować w taki sposób, by auto­ matycznie ukrywało solucję, w przypadku gdy należy do niej tylko jeden projekt. Jeśli Czytelnik nie widzi węzła Solution, a chciałby, żeby był on widoczny, to należy wybrać z menu opcję Tools/Options, a następnie w oknie dialogowym Options zaznaczyć opcję Projects and Solutions. Wśród wyświetlonych opcji znajdzie się także pole wyboru Always show solution (zawsze pokazuj solucję) - jeśli chcemy, by w panelu Solution Explorer był wyświetlany węzeł solucji, nawet jeśli należy do niej tylko jeden projekt, to należy to pole zaznaczyć.

Projekty i solucje

39

Oprócz plików źródłowych C# w panelu Solution Explorer przedstawionym na rysunku 2.3 widoczna jest także sekcja References. Zawiera ona listę wszystkich bibliotek używanych w danym projekcie. Domyślnie Visual Studio wyświetla na niej biblioteki DLL pochodzące z biblioteki klas .NET Framework, które według niego mogą się nam przydać. Być może w tym momencie Czytelnik poczuje się, jakby doświadczał dejf vu - czyż nie powie­ dzieliśmy już kompilatorowi, z których bibliotek chcemy korzystać, przy użyciu dyrektyw usi ng? Początkujący programiści uczący się języka C# bardzo często mają z tym problemy. Otóż przestrzenie nazw nie są bibliotekami i bynajmniej się w nich nie zawierają. Ten fakt łatwo przegapić, jeśli zasugerujemy się widocznym związkiem pomiędzy bibliotekami i przestrzeniami nazw. Na przykład biblioteka Sys t em . Data faktycznie definiuje wiele typów należących do przestrzeni nazw System . Data, niemniej jednak jest to jedynie konwencja, i to taka, którą zazwy­ czaj traktuje się jako niezobowiązującą. Biblioteki bardzo często, choć nie zawsze, noszą nazwy odpowiadające przestrzeniom nazw, z którymi są najmocniej powiązane. Zdarza się jednak, że biblioteka zawiera typy należące do kilku różnych przestrzeni nazw, jak również że typy należące do jednej przestrzeni nazw są umieszczone w kilku bibliotekach. (Gdyby Czytelnik zastanawiał się, co doprowadziło do powstania takiego chaosu, proszę przeczytać informacje zamieszczone w poniższej ramce) . W efekcie kompilator C# nie jest w stanie określić potrzebnych bibliotek na podstawie samych dyrektyw u s i ng, gdyż zazwyczaj nie da się wskazać ich tylko w oparciu o przestrzenie nazw. Właśnie dlatego w projekcie niezbędna jest lista używanych bibliotek, a oprócz niej poszcze­ gólne pliki źródłowe wchodzące w skład projektu mogą zadeklarować, z jakich przestrzeni nazw korzystają. Visual Studio tworzy dla nas grupę odwołań, które według niego mogą się nam przydać, choć w naszym prostym przykładzie i tak większość z nich nie będzie nam potrzebna. ' ..

, ...._ .__ __,,._ ,

Visual Studio sygnalizuje sytuację, gdy kod nie używa wszystkich bibliotek, które zostały umieszczone na liście odwołań, i automatycznie pomija te, które nie są potrzebne. Dzięki temu generowane pliki binarne są nieco mniejsze, niż gdyby zawierały wszystkie odwołania.

Odwołania do bibliotek można dodawać i usuwać w zależności od potrzeb tworzonego pro­ gramu. Aby usunąć odwołanie, wystarczy zaznaczyć jedną z bibliotek wyświetlonych na liście w panelu Solution Explorer i nacisnąć klawisz Delete. (Jak się okazuje, nasz przykładowy program jest tak prosty, że korzysta wyłącznie z obowiązkowej biblioteki ms corl i b, a zatem Czytelnik mógłby usunąć wszystkie wyświetlone biblioteki DLL i, o ile tylko usunąłby także wszystkie niepotrzebne dyrektywy u s i ng, program i tak działałby prawidłowo) . Aby dodać odwołanie do biblioteki, należy kliknąć opcję References prawym przyciskiem myszy i wybrać z menu podręcznego opcję Add Reference. Zagadnienia związane z bibliotekami zostały znacznie bardziej szczegółowo opisane w rozdziale 15 . Już prawie dotarliśmy do momentu, kiedy przyjdzie nam zakończyć analizę programu „Witaj, świecie" i zająć się poznawaniem kolejnych podstawowych cech oraz możliwości języka C#, najpierw jednak warto byłoby podsumować wszystko, czego się dowiedzieliśmy. Jedyny wiersz kodu wykonywany przez nasz program wywołuje metodę Wri tel i n e klasy Sys t em . Con sol e, by wyświetlić komunikat tekstowy w oknie konsoli. Kod ten znajduje się wewnątrz metody, której specjalna nazwa - Ma i n - określa, że należy ją wywołać podczas uruchamiania programu. Metoda ta jest umieszczona w klasie o nazwie Program . Jest tak, gdyż C# wymaga, by każda metoda należała do jakiegoś typu. W naszym przykładzie klasa Program jest elementem przestrzeni nazw He 1 1 oWorl d, gdyż zdecydowaliśmy się zachować zgodność z konwencją, by przestrzeń nazw 40

Rozdział 2. Podstawowe techniki programowania

Przestrzenie nazw i bibl ioteki Rozmieszczenie typów w różnych bibliotekach DLL tworzących bibliotekę .NET jest p o części spowodowane przez względy wydajności, a po części ma podłoże historyczne. Dobrym przykładem ilustrującym powody historyczne jest biblioteka System . Core. W .NET Framework nie istnieje prze­ strzeń nazw System . Core - w bibliotece o tej nazwie umieszczonych jest wiele klas pochodzących z takich przestrzeni nazw jak System, System . I O oraz System. Threadi ng. Niemniej jednak typy należące do tych samych przestrzeni nazw można także znaleźć w bibliotece System, jak również w bibliotece mscorl i b. (Wszystkie programy .NET muszą się odwoływać do biblioteki mscorl i b, dlatego Visual Studio nie wyświetla jej na liście w panelu Solution Explorer. Zostały w niej zdefiniowane najważniejsze typy takie jak System . Str i ng oraz System . I n t32 ) . Jednym z powodów istnienia osobnej biblioteki System. Core jest fakt, że pojawiła się ona po raz pierwszy w wersji 3.5 platformy .NET. W wersjach 3.0 oraz 3.5 .NET Framework Microsoft zdecydował się umieszczać nowe możliwości funkcjonalne w nowych bibliotekach DLL, zamiast modyfikować stare dostarczone w wersji 2.0 platformy. Ta decyzja określająca, które typy zostaną umieszczone w poszczególnych bibliotekach DLL - była zupełnie niezależna od konceptualnych decyzji związanych z umieszczaniem typów w konkretnych prze­ strzeniach nazw. Jednak nie można wszystkiego wyjaśnić jedynie względami historycznymi. Już w pierwszej wersji .NET typy należące do niektórych przestrzeni nazw były umieszczane w kilku różnych bibliotekach. Jednym z najważniejszych powodów takiego postępowania była chęć uniknięcia wczytywania kodu, który nigdy nie będzie używany. Na pewno nie chcielibyśmy tracić czasu i pamięci w zwyczajnej aplikacji okienkowej, wczytując biblioteki używane do tworzenia aplikacji internetowych. W niektó­ rych przypadkach przestrzenie nazw doskonale wyznaczają granice podziału - istnieje całkiem spore prawdopodobieństwo, że jeśli w aplikacji używamy jakiegoś typu pochodzącego z prze­ strzeni nazw System . Web, to będziemy używali znacznie więcej typów z tej przestrzeni. Niemniej jednak można wskazać kilka przypadków, w których przestrzenie nazw nie są najlepszym sposobem określania podziałów. Na przykład zawartość przestrzeni nazw System . Pri nti ng została rozdzielona pomiędzy dwie biblioteki: System . Pri nti ng, zawierającą ogólne klasy związane z drukowaniem, oraz ReachFramework, dodającą do tej przestrzeni nazw dodatkowe typy, których możemy potrzebować w przypadku pracy z pewnym konkretnym rodzajem dokumentów drukowanych - plikami XPS. Jeśli z nich nie korzystamy, to nie musimy odwoływać się do tej wyspecjalizowanej biblioteki DLL. To prowadzi do pytania: skąd mamy wiedzieć, w których bibliotekach szukać konkretnych klas? Sytuacja, w której dodanie odwołania do biblioteki System . Pri nti ng nie zapewni nam dostępu do niezbędnych typów przestrzeni nazw System . Pri nti ng, może być niezwykle frustrująca. Na szczęście dokumentacja biblioteki .NET poświęcona konkretnym typom zawiera informacje zarówno o prze­ strzeni nazw, jak i o bibliotece (podzespole - ang. assembly), w jakiej zostały one umieszczone.

odpowiadała nazwie skompilowanego pliku binarnego. Nasz program korzysta z dyrektyw us i ng domyślnie dodawanych przez Visual Studio, dzięki czemu możemy używać klasy Con so l e bez konieczności jawnego podawania przestrzeni nazw, w której została ona zadeklarowana. Teraz, kiedy ponownie przejrzymy kod programu, będziemy rozumieli znaczenie każdego wiersza . (Został on przedstawiony na listingu 2.3, przy czym usunięto z niego niepotrzebne dyrektywy u s i ng) .

Listing 2 .3. Program Witaj, świecie" po raz wtóry (bez niepotrzebnych dyrektyw using) „

us i ng Sys tem ; namespace Hel l oWorl d { cl ass Program

Projekty i solucje

41

stat i c voi d Mai n (stri ng O arg s ) { Consol e . Wri teli ne ( "Wi taj , świ eci e " ) ;

Spoglądając na ten przykład, jasno zobaczymy, że kod odzwierciedla strukturę. Jest to często stosowana praktyka, choć nie jest konieczna. Dla kompilatora C# odstępy pomiędzy poszcze­ gólnymi elementami kodu nie mają znaczenia. Niezależnie od tego, czy jest to pojedynczy znak odstępu, kilka odstępów lub znaków tabulacji, czy nawet znaki nowego wiersza, są one wszystkie traktowane jako jeden znak odstępu3 . A zatem znaków odstępu można używać w kodzie dowolnie, by poprawić jego czytelność. To właśnie z tego powodu C# wymaga stoso­ wania nawiasów klamrowych określających zawieranie się jednego bloku kodu w innym. Z tego samego powodu na końcu wiersza zawierającego wywołanie wyświetlające komunikat umie­ ściliśmy znak średnika . Ponieważ C# nie zwraca uwagi na to, czy jedna instrukcja została umieszczona w jednym wierszu kodu źródłowego, czy została zapisana w kilku wierszach, czy też upchnęliśmy kilka instrukcji w jednym wierszu, konieczne jest precyzyjne określenie końca instrukcji. Do tego właśnie służy średnik ( ; ) wskazujący miejsce, gdzie zaczyna się kolejny krok programu.

Komentarze, regiony oraz czytel ność Rozważając zagadnienia związane ze strukturą oraz układem kodu źródłowego, należy także wspomnieć o jednej możliwości, która jest niezwykle ważna, choć nie ma żadnego wpływu na działanie tworzonego kodu. C# pozwala dodawać do kodu fragmenty tekstu, które będą cał­ kowicie ignorowane. Można sądzić, że są one nieistotne, a nawet bezużyteczne, jednak okazuje się, że mają kolosalne znaczenie, jeśli chcemy mieć jakąkolwiek nadzieję na to, że zrozumiemy kod, który napisaliśmy sześć miesięcy wcześniej . Istnieje pewien nieszczęśliwy fenomen znany jako tworzenie samego kodu. Taki kod - mający jakiś sens dla programisty, który go napisał - jest jednak całkowicie niezrozumiały dla każdej innej osoby próbującej go później pojąć, i to nawet wtedy, gdy tą osobą jest sam autor. Najlep­ szą obroną przed takimi problemami jest rozważne dobieranie nazw stosowanych w kodzie oraz nadawanie mu odpowiedniej struktury. Ze wszystkich sił należy się starać pisać kod, który działa tak, jak można tego oczekiwać po jego przejrzeniu. Niestety czasami konieczne jest wykonywanie pewnych czynności w sposób niezbyt oczywisty, więc nawet jeśli kod jest wystarczająco czytelny, by zrozumieć, co robi, nie zawsze będzie można domyślić się, dlaczego wykonywane są niektóre operacje . Takie sytuacje najczęściej występują tam, gdzie nasz kod współpracuje z innym - na przykład gdy korzystamy z komponentu lub usługi, które są specyficzne bądź po prostu źle napisane i działają prawidłowo wyłącznie wtedy, gdy są używane w specyficzny sposób . Na przykład może się okazać, że używany komponent ignoruje pierwszą próbę wykonania jakiejś czynności i zmuszenie go do prawidłowego działa­ nia wymaga zastosowania dodatkowego, pozornie nadmiarowego wiersza kodu:

3

Jest jednak jeden dziwny wyjątek: w stałych łańcuchowych, takich jak tekst „Witaj, świecie" użyty w naszym przykładzie, znaki odstępu są traktowane dosłownie C# zakłada, że jeśli umieścimy na przykład trzy znaki odstępu w tekście zapisanym w cudzysłowach, to naprawdę chcemy te trzy znaki wyświetlić. -

42

Rozdział 2. Podstawowe techniki programowania

Frobn i cator . SetTarget ( " " ) ; Frobn i cator . SetTarget ( " Sopot " ) ;

Problem z takim kodem polega na tym, że w przyszłości osoby próbujące go zrozumieć będą miały ogromne trudności. Czy ten nadmiarowy wiersz kodu został zastosowany celowo? Czy można go bezpiecznie usunąć? Intryga i niejednoznaczności mogą być dobre dla powieści fantastyczno-naukowych, jednak w przypadku kodu programów nie są pożądane . Potrzebny jest zatem jakiś sposób rozwiązania tej zagadki i właśnie do tego służą komentarze. Można by napisać coś takiego: li W komponencie Frobnicator v2.41 jest błąd, który czasami powoduje jego awarię, jeśli li ustawimy cel na "Sopot ". Testy pokazują, że początkowe ustawienie celu będącego li pustym ła11cuchem znaków eliminuje problemy.

Frobn i cator . SetTarget ( " " ) ; Frobn i cator . SetTarget ( " Sopot " ) ;

Teraz kod jest znacznie mniej tajemniczy. Osoba, która będzie go analizować, od razu dowie się, dlaczego występuje w nim pozornie nadmiarowe wywołanie. Od razu wiadomo, jaki problem ono rozwiązuje oraz kiedy się on pojawia, dzięki czemu można sprawdzić, czy w kolejnych wersjach komponentu problem został wyeliminowany i czy zastosowane rozwiązanie tymcza­ sowe można usunąć. Jeśli weźmiemy pod uwagę pielęgnację kodu w dłuższej perspektywie, taki opis znacznie upraszcza zadanie. Pod względem kodu C# powyższy przykład niczym nie różni się od wcześniejszego, w którym komentarzy nie było. Sekwencja znaków / / nakazuje kompilatorowi pominąć cały tekst aż do końca wiersza . Oznacza to, że komentarze można umieszczać w osobnych wierszach, jak to pokazano powyżej, bądź też na końcu kodu w wierszu: Frobn i cator . SetTarget ( " " ) ;

li Ominięcie błędu występującego w komponencie v2.41

Podobnie jak większość języków wzorowanych na C, także C# udostępnia dwa sposoby two­ rzenia komentarzy. Oprócz tych jednowierszowych zapisywanych za kombinacją znaków / / można także tworzyć komentarze zajmujące wiele wierszy. Rozpoczynają się one od kombinacji znaków /*, a kończą sekwencją */, co pokazaliśmy na poniższym przykładzie . /* To część komentarza. To kolejna część tego samego komentarza. A to już koniec komentarza. *I

N ieprawidłowe komentarze Choć komentarze mogą być bardzo przydatne, to jednak niestety wielu z nich nie można za takie uznać. Istnieje kilka błędów, które programiści wyjątkowo często popełniają przy ich tworzeniu - warto zwrócić na nie uwagę Czytelnika, by wiedział, jak ich unikać. Oto przykład najczęściej spotykanego błędu: li Przypisanie celowi pustego łmicucha znaków li Ustawienie celu na Sopot

Frobn i cator . SetTarget ( " " ) ;

Frobn i cator . SetTarget ( " Sopot " ) ;

Powyższe komentarze powtarzają jedynie to, co zostało już napisane w kodzie. To czysta strata miejsca, jednak, co ciekawe, bardzo często można się na takie komentarze natknąć, zwłaszcza w kodzie tworzonym przez niedoświadczonych programistów. Być może dzieje się tak, ponie­ waż powiedziano im, że komentarze są dobre, jednak zapomniano dodać, co sprawia, że takie są. Komentarz powinien zawierać informację, której nie można zdobyć, analizując kod programu, i która zapewne przyda się osobom próbującym go zrozumieć. Komentarze, regiony oraz czytelność

43

W poniższym przykładzie przedstawiliśmy kolejny popularny błąd związany z komentarzami. li Ustawienie celu na Sopot Frobn i cator . SetTarget ( " M i ędzyzdroj e " ) ;

W tym przypadku komentarz jest sprzeczny z kodem. Można sądzić, że nikomu nie trzeba mówić, iż nie należy dopuszczać do takich sytuacji, zadziwiające jest jednak to, jak często można je znaleźć w rzeczywistym kodzie. Zazwyczaj powstają one, ponieważ ktoś zmienił kod, a nie zadał sobie trudu, by zaktualizować także komentarz. Podczas modyfikacji kodu zawsze warto rzucić okiem na komentarze (tym bardziej że jeśli nie zwracamy na wprowadzane mody­ fikacje wystarczającej uwagi, by dostrzec nieaktualne komentarze, to istnieje duże prawdo­ podobieństwo, że w kodzie pojawią się także inne problemy, których nie zauważyliśmy) .

Komentarze dokumentujące XML Jeśli nadamy naszym komentarzom pewną ściśle określoną strukturę, Visual Studio będzie w stanie prezentować zamieszczane w nich informacje w formie etykiet ekranowych, często wykorzystywanych przez programistów tworzących kod. Jak pokazuje listing 2.4, komentarze dokumentujące są oznaczane przy użyciu trzech znaków ukośnika i zawierają znaczniki XML opisujące element, którego dotyczą - w naszym przypadku jest to metoda, jej parametry oraz zwracane informacje.

Listing 2 .4. Komentarze dokumentujące XML Ili Ili Zwraca przekazaną wartość podniesioną do kwadratu. Ili Ili Wartość, którą należy podnieść do kwadratu. Ili Kwadrat przekazanej wartości. s t at i c doubl e Square (doubl e x) { return x * x ;

Kiedy programista zacznie pisać kod wywołujący tę metodę, Visual Studio wyświetli listę pre­ zentującą wszystkie dostępne składowe pasujące do wpisanego ciągu znaków oraz etykietkę ekranową z informacją podaną w znaczniku aktualnie zaznaczonej metody (jak widać na rysunku 2.4) . Bardzo podobne informacje można zobaczyć w przypadku korzystania z klas należących do biblioteki .NET Framework - dokumentacja tych klas wchodzi w skład .NET Framework SDK dołączanego do Visual Studio. (Kompilator C# potrafi pobrać te informacje z plików źródłowych i umieścić je w osobnych plikach XML, dzięki czemu można przygotować dokumentację tworzonej biblioteki bez udostępniania jej kodów źródłowych) . C on s ole . Hrit eli n e ( � } ;

l�� �Q [

r=-c-.. ..- re -------------.--Squa d... o uble S q uare(d o uhl e x) ----� Program.

I --------------11 Zwraca przekazaną warto ść p o d niesi o n ą do kwadratu.

I

Rysunek 2 .4. Informacje pochodzące z dokumentacji XML Informacje umieszczone w znaczniku są prezentowane po rozpoczęciu wpisywania argumentów metody (jak pokazano na rysunku 2.5) . Informacje podane w znaczniku nie są tu wyświetlane, jednak istnieją narzędzia, które są w stanie je pobrać i dołączyć do

44

Rozdział 2. Podstawowe techniki programowania

Con sole . Hriteline (S qu a re (

�------�

d'o u b l'e Pro-gram, 5q u a re(do11'ble x) Zwraca przekaza ną wa rto-i;ć podn iesio ną do· kwad ratu.

x:

Wartość, którą na leży p odn ieść do kwodro tu.

Rysunek 2 .5. Informacje o parametrach pobrane

z

dokumentacji XML

dokumentacji generowanej w formacie HTML lub w formie plików pomocy systemu Windows. Na przykład firma Microsoft udostępnia narzędzie o nazwie Sandcastle (które można pobrać ze strony http://www.codeplex.com/Sandcastle) potrafiące przygotować dokumentację o strukturze przypominającej informacje o bibliotekach klas tworzonych przez Microsoft. Zakończyliśmy już analizę przykładu „Witaj, świecie", więc nadszedł chyba odpowiedni moment, by utworzyć nowy projekt - oczywiście jeśli tylko Czytelnik podczas lektury książki sprawdza prezentowane w niej przykłady. (W tym celu należy wybrać z menu opcję File/New Project lub nacisnąć kombinację klawiszy Ctrl+Shift+N. Warto zwrócić uwagę, że domyślnie wykonanie tej operacji spowoduje także utworzenie nowej solucji, w której zostanie umiesz­ czony nowy projekt. W oknie dialogowym New Project dostępna jest też opcja pozwalająca dodać go do już istniejącej solucji, w naszym przypadku jednak z niej nie skorzystamy) . Utwórzmy zatem kolejną aplikację konsolową, tym razem o nazwie Racelnfo - będzie ona służyła do przeprowadzania różnego typu analiz wydajności samochodu wyścigowego. Kiedy Visual Studio utworzy na nasze żądanie nowy projekt, jego kod będzie niemal taki sam jak ten przed­ stawiony na listingu 2.1, lecz klasa Program będzie umieszczona w przestrzeni nazw Race l n fo, a nie Hel l oWorl d. Naszym pierwszym zadaniem będzie obliczenie średniej prędkości oraz zuży­ cia paliwa, musimy zatem przedstawić sposób, w jaki C# pozwala przechowywać dane i ope­ rować na nich.

Zm ienne W języku C# metody mogą posiadać nazwane elementy służące do przechowywania informacji. Noszą one nazwę zmiennych, gdyż przechowywane w nich informacje mogą być inne podczas każdego uruchomienia programu, jak również sam program może je zmieniać w trakcie dzia­ łania . Listing 2.5 przedstawia trzy zmienne zdefiniowane w metodzie Mai n; reprezentują one odpowiednio odległość pokonaną przez samochód, czas, jaki mu to zajęło, oraz ilość zużytego paliwa. W tym przykładzie wartości zmiennych nie będą zmieniane - ogólnie można je zmie­ niać, lecz nic nie stoi na przeszkodzie, by tworzyć zmienne, których wartości pozostaną stałe.

Listing 2 .5. Zmienne s t at i c vo i d Mai n (s tri ng O arg s ) { doubl e kmTravel l ed = 5 . 14 ; doubl e el apsedSeconds = 78 . 74 ; doubl e fuel Ki l o s Cons umed = 2 . 7 ;

Warto zwrócić uwagę, że nazwy tych zmiennych ( kmTravel l ed, el apsedSeconds, fuel Ki l osCon s umed) są stosunkowo opisowe . W algebrze zazwyczaj nadaje się zmiennym nazwy jednoliterowe, jednak w przypadku tworzenia kodu programów znacznie lepszą praktyką jest stosowanie nazw, które w zrozumiały sposób opisują ich przeznaczenie.

Zmienne

45

Jeśli mamy problemy z wymyśleniem dobrej opisowej nazwy zmiennej, bardzo często jest to sygnałem potencjalnych problemów. Trudno jest napisać działający kod, jeśli dokładnie nie wiemy, na jakich informacjach on operuje.

Te nazwy informują nie tylko o tym, co reprezentuje dana zmienna, lecz także o tym, w jakich jednostkach dana informacja jest wyrażona . Dla kompilatora nie ma to najmniejszego zna­ czenia - równie dobrze można by nadać tym zmiennym nazwy tomek, j oas i a oraz j anek niemniej jednak opisowe nazwy będą pomocne dla osób przeglądających kod. Niedomówienia związane z tym, czy wartości są wyrażane w układzie metrycznym, czy imperialnym, wielo­ krotnie były powodem bardzo kosztownych problemów takich jak przypadkowe doprowa­ dzenie do zniszczenia statku kosmicznego. Najwyraźniej w tym zespole wyścigowym używany jest system metryczny. (Jeśli Czytelnik zastanawia się, dlaczego paliwo jest wyrażane w kilogra­ mach, a nie, dajmy na to, w litrach, to trzeba wiedzieć, że w świecie wyścigów samochodowych paliwo mierzy się w jednostkach masy, a nie objętości, podobnie zresztą jak w świecie lotnic­ twa. Paliwo kurczy się i rozszerza wraz ze zmianami temperatury - lepiej wykorzystamy nasze pieniądze, kupując je rano w chłodny dzień niż w gorący dzień w południe - a zatem masa jest w tym przypadku lepszą jednostką miary, gdyż jest bardziej stabilna) .

Typy zm iennych Wszystkie trzy deklaracje zmiennych przedstawione na listingu 2.5 rozpoczynają się od słowa kluczowego dou b l e . Informuje ono kompilator, jakiego rodzaju dane są przechowywane w zmiennej . W naszym przykładzie używane są liczby, jednak .NET udostępnia kilka różnych typów liczbowych. Ich pełna lista została przedstawiona w tabeli 2.1 . Może się wydawać, że typów tych jest oszałamiająco dużo, jednak zazwyczaj wybór ogranicza się do trzech spośród nich: i nt, doubl e oraz dec i ma l , używanych odpowiednio do reprezentowania: liczb całkowitych, zmiennoprzecinkowych oraz zmiennoprzecinkowych z miejscami dziesiętnymi.

Liczby całkowite Typ i nt (co jest skrótem nazwy i nteger) reprezentuje liczby całkowite. Bez wątpienia takie liczby nie nadają się do wykorzystania w naszym przykładzie, gdyż musimy mieć możliwość posłu­ giwania się takimi wartościami jak 5,14, a liczbą całkowitą najbliższą tej wartości jest 5. Nie­ mniej jednak programy często operują na wartościach dyskretnych takich jak liczba wierszy zwróconych przez zapytanie do bazy danych bądź liczba pracowników zdających raporty konkretnemu menadżerowi. Główną zaletą liczb całkowitych jest ich dokładność: nie ma żadnej wątpliwości, czy dana liczba faktycznie jest równa 5, czy też jest wartością do niej zbliżoną taką jak 5,000001 . Tabela 2.1 przedstawia dziewięć typów pozwalających na przechowywanie wartości całkowi­ tych. Ostatni z nich, B i t l nteger, jest przypadkiem szczególnym, którym zajmiemy się dokładnie nieco później . Pozostałe osiem typów udostępnia cztery różne wielkości oraz możliwość (lub jej brak) reprezentowania liczb ujemnych. Może się wydawać, że liczby bez znaku są mniej elastyczne, jednak okazują się one szczególnie przydatne w sytuacjach, gdy chcemy reprezentować wartości, które nigdy nie mogą być mniej­ sze od zera. Niemniej jednak liczby całkowite bez znaku są rzadziej stosowane, a w niektórych językach programowania w ogóle nie są dostępne . W .NET Framework można zauważyć, że

46

Rozdział 2. Podstawowe techniki programowania

Tabela 2.1 . Typy liczbowe Nazwa w C#

Nazwa w .NET

Przeznaczenie

fl oat

System . Si ngl e

Liczby całkowite oraz z częściami ułam kowymi o ograniczonym zakresie. Posiadają duży zakres wartości dzięki „zmiennoprzecin kowemu" charakterowi. Zaj m ują 32 bity.

doubl e

System . Doubl e

Wersja wa rtości typu

fl oat o

podwój n ej p recyzji - d z i ałają w ten sam sposób,

Liczba całkowita bez znaku. Zajmuje 8 bitów. Reprezentuje wartości z zakresu od O do 255.

lecz zaj m ują 64 bity.

by te

System. Syte

sbyte

System . SByte

Liczba całkowita ze znakiem. Zaj m uje 8 bitów. Reprezentuje wartości z zakresu od -128 do 1 27.

short

System. Int16

Liczba całkowita ze znakiem. Zajmuje 1 6 bitów. Reprezentuje wartości z zakresu od -32 768 Liczba całkowita bez znaku. Zaj m uje 16 bitów. Reprezentuje wartości z zakresu od O

do 32 767.

u short

System . U l nt16

do 65 5 3 5 .

i nt

System. I nt32

Liczba całkowita ze znakiem . Zaj m uj e 3 2 b ity. Reprezentuj e wartości z zakresu Liczba całkowita bez z n a k u . Zaj m uj e 32 bity. Reprezentuje wartości z zakresu od O

od -2 147 483 648 do 2 1 47 483 647.

u i nt

System . U l nt32

do 4 294 967 295.

l ang

System. I nt64

Liczba całkowita ze znakiem . Zaj m uje 6 4 bity. Repreze ntuj e wartości z zakresu Liczba całkowita bez z n a k u . Zaj m uj e 64 bity. Re prezentuje wartości z zakresu od O

od -9 223 372 036 8 54 775 8 0 8 do 9 223 372 036 8 54 775 807.

ul ong

System . U l nt64

do 1 8 446 744 073 709 551 6 1 5 . ( brak)

deci mal

System . Numeri es . B i g l n teger

ogranicza jedynie pojemność pamięc i .

System. Dec i mal

Obsługuje liczby całkowite z m iejscam i dziesiętnymi. T e n typ jest n ieco m n i ej wydajny

Liczba całkowita z e zna kiem . Jej wielkość roś nie zależnie o d potrzeb. Zakres wartości

od typu

doubl e, jednak gwarantuje nieco bardziej przewidywalne wyn i ki w przypadku

wykonywania działań na liczbach z częściami dziesiętnymi.

zazwyczaj, nawet w miejscach, gdzie użycie liczby bez znaku byłoby bardziej sensowne, sto­ sowane są liczby całkowite ze znakiem. Na przykład właściwość C o u n t dostępna w większości kolekcji jest typu i n t - 32-bitowa liczba ze znakiem - choć nie ma sensu, by kolekcja zawierała ujemną liczbę elementów. '

. .

Warto także zauważyć, ż e liczby całkowite bez znaku mogą przyjmować większe wartości niż liczby ze znakiem. Nie muszą one używać jednego bitu do przechowy­ wania informacji o znaku - zamiast tego wykorzystują go do powiększenia zakresu wartości. Nie jest to jednak cecha, na której bezpiecznie można oprzeć działanie kodu. Jeśli aż w takim stopniu zbliżamy się do granic zakresu używanych typów, że już jeden bit ma znaczenie, to istnieje duże prawdopodobieństwo przekroczenia dopuszczalnego zakresu wartości. Warto się wtedy zastanowić nad wykorzystaniem większego typu.

Oprócz rozróżnienia pomiędzy liczbami bez znaku oraz ze znakiem różne typy liczb całko­ witych mają także różne rozmiary i powiązany z tym różny zakres wartości. Chętnie wybierane są liczby 32-bitowe, gdyż udostępniają użyteczny zakres wartości i zapewniają wydajne działanie na 32-bitowych procesorach. Typy 64-bitowe są używane w (raczej sporadycznych) sytuacjach, gdy sięgający kilku miliardów zakres wartości oferowany przez liczby 32-bitowe okazuje się niewystarczający. Typy 16-bitowe są używane raczej rzadko, choć stosuje się je czasami, gdy trzeba operować na starych interfejsach programistycznych lub formatach plików bądź też obsługiwać protokoły sieciowe. Zmienne

47

Ośmiobitowy typ byte jest ważny, gdyż binarne operacje wejścia-wyjścia (takie jak operacje na plikach lub połączeniach sieciowych) zazwyczaj bazują właśnie na bajtach. Poza tym, ze wzglę­ dów historycznych, ten 8-bitowy typ jest sprzeczny z ogólnymi trendami, gdyż jego wersja bez znaku jest stosowana znacznie częściej niż ta ze znakiem - typ s byt e . Niemniej jednak, pomijając operacje wejścia-wyjścia, typy reprezentujące bajty są zazwyczaj zbyt małe, by były przydatne. A zatem okazuje się, że w praktyce najczęściej stosowanym typem całkowitym jest i nt . Fakt, że C# udostępnia wszystkie inne typy całkowite, może się wydawać archaiczny, nawiązuje bowiem do czasów, gdy komputery miały tak mało pamięci, że stosowanie liczb 32-bitowych było drogą ekstrawagancją. Udostępnianie tych typów wiąże się z pochodzeniem C#, który wywodzi się z rodziny języków C. Okazuje się jednak, że możliwość ich stosowania jest przy­ datna, gdy musimy operować bezpośrednio na interfejsach API systemu Windows (czym zajmiemy się w rozdziale 19.) . Warto zwrócić uwagę, że większość typów przedstawionych w tabeli 2.1 posiada dwie nazwy. C# korzysta z nazw takich jak i nt oraz l ang, jednak w .NET Framework używane są dłuższe Sys t em . I n t32 lub Sys t em . I n t64. Nazwy stosowane w C# są określane jako nazwy zastępcze (ang. alias), które język z radością nam udostępnia. A zatem można użyć deklaracji o następującej postaci: i nt answer

=

42 ;

jak również: Sys tem . I nt32 answer

=

42 ;

Ewentualnie, jeśli w pliku źródłowym znajdzie się dyrektywa us i ng System, tę samą deklarację można także zapisać w następującej formie: I nt32 answer

=

42 ;

Wszystkie te wersje deklaracji mają to samo znaczenie - po ich skompilowaniu wynikowy kod binarny będzie taki sam. Ostatnie dwie wersje są swoimi odpowiednikami ze względu na sposób działania przestrzeni nazw, jednak dlaczego C# udostępnia zbiór dodatkowych nazw zastępczych typów? Okazuje się, że przyczyną są względy historyczne: język C# projektowano w taki sposób, by był on łatwy do nauki dla osób znających języki należące do tak zwanej rodziny języka C, do której zaliczane są C, C ++, Java oraz JavaScript. Większość z tych języków używa tych samych nazw dla określonych typów danych - na przykład i nt jest w nich uży­ wany do określania liczb całkowitych ze znakiem o użytecznym zakresie dostępnych wartości. A zatem C# kontynuuje pewien zwyczaj - pozwala pisać kod, który wygląda tak samo, jak wyglądałby w innych językach należących do rodziny języka C . Jednak .NET Framework obsługuje wiele różnych języków programowania, korzysta zatem z prozaicznie prostej konwencji nadawania typom liczbowym nazw opisowych - na przykład 32-bitowej liczbie całkowitej ze znakiem odpowiada typ Sys t em . I n t 3 2 . Ponieważ C# pozwala stosować oba style nazewnictwa typów, zdania na temat tego, którego z nich należy używać, są podzielone4 . Bardziej popularny wydaje się być styl stosowany w rodzinie języka C (czyli i nt, doub l e itd.) .

4

Kiedy w jakimś systemie programowania istnieje więcej niż jeden sposób na zrobienie czegoś, tworzą się podziały zapewniające możliwość prowadzenia długotrwałej i całkowicie bezcelowej dyskusji o tym, co jest „lepsze" .

48

Rozdział 2. Podstawowe techniki programowania

W czwartej wersji platformy .NET wprowadzony został dodatkowy typ całkowity - Bi g l nteger działający nieco inaczej niż wszystkie pozostałe . Nie ma on swego odpowiednika w nazew­ nictwie rodziny języka C, dlatego też znany jest wyłącznie pod nazwą klasy biblioteki .NET Framework. W odróżnieniu od wszystkich pozostałych typów całkowitych, które zajmują w pamięci komputera ściśle określony obszar determinujący jednocześnie zakres dostępnych wartości, obszar zajmowany przez wartość typu B i g l nteger może się powiększać. Gdy wartość liczby rośnie, zajmuje ona coraz więcej miejsca w pamięci. Jedynym teoretycznym ograniczni­ kiem dostępnego zakresu wartości jest wielkość pamięci, jednak w praktyce czynnikiem ograniczającym wydaje się być raczej koszt przetwarzania związany z operacjami na wielkich liczbach. Gdy są one odpowiednio duże, nawet proste operacje matematyczne takie jak mnoże­ nie mogą być bardzo kosztowne . Na przykład gdybyśmy mieli dwie liczby, z których każda składałaby się z miliona cyfr (i zajmowała ponad 400 kilobajtów pamięci operacyjnej), to pomno­ żenie ich na stosunkowo wydajnym komputerze zajęłoby ponad jedną minutę. Typ B i g l n teger jest przydatny w obliczeniach matematycznych, w których trzeba operować na bardzo wielkich liczbach - w bardziej standardowych sytuacjach najczęściej stosowany jest typ i nt . Przedstawione tu typy danych doskonale nadają się do wyrażania wartości policzalnych. Co jednak można zrobić w przypadku, gdy trzeba operować na czymś innym niż liczby całkowite? W takich sytuacjach na scenę wkraczają typy zmiennoprzecinkowe.

Typy zm iennoprzecinkowe Typy doubl e oraz fl oat zapewniają możliwość reprezentowania liczb posiadających część ułam­ kową. Każdego z nich można użyć na przykład do przechowania wartości 1,5, co nie jest moż­ liwe w przypadku typów całkowitych . Jedyną różnicą pomiędzy tymi dwoma typami jest zapewniana przez nie precyzja . Ponieważ liczby zmiennoprzecinkowe mają ściśle określoną wielkość, oferują ograniczoną precyzję. Znaczy to, że nie zapewniają możliwości reprezentowa­ nia dowolnych części ułamkowych - ograniczona precyzja oznacza, że liczby zmiennoprzecin­ kowe mogą reprezentować większość liczb jedynie w sposób przybliżony.

Liczby zmiennoprzecinkowe Jeśli Czytelnik zastanawia się, dlaczego te typy noszą nazwę zmiennoprzecinkowych (ang. fioating-point), wyjaśniamy, że jest to techniczny opis wewnętrznego sposobu ich działania. Liczby zmiennoprze­ cinkowe składają się z ustalonej ilości cyfr dwójkowych używanych do przechowywania wartości oraz kolejnej liczby określającej położenie przecinka dziesiętnego. A zatem przecinek w nazwie repre­ zentuje przecinek dwójkowy odpowiadający przecinkowi dziesiętnemu. Jest on 11zmienny", gdyż jego położenie może być różne.

Typ fl oat udostępnia siedem miejsc dziesiętnych dokładności, natomiast typ doub l e około 17. (Konkretnie rzecz biorąc, pierwszy z nich udostępnia 23, a drugi 52 dwójkowe miejsca dokład­ ności. Są to formaty dwójkowe, więc ich precyzja nie odpowiada dokładnie liczbie miejsc dzie­ siętnych, jakie mogą reprezentować) . A zatem wykonanie kodu: doubl e x = 1234 . 5678 ; doubl e y = x + 0 . 0001 ; Consol e . Wri teli ne (x) ; Consol e . Wri teli ne (y) ;

Zmienne

49

spowoduje wyświetlenie następujących wyników: 1234 . 5678 1234 . 5679

Jeśli natomiast użyjemy typu fl oat: fl oat x = 1234 . 5678 f ; fl oat y = x + O . OO O l f ; Consol e . Wri teli ne (x) ; Consol e . Wri teli ne (y) ;

wyniki będą następujące: 1234 . 568 1234 . 568

Takie różnice często zaskakują początkujących programistów, jednak są czymś normalnym. Co więcej, nie są one bynajmniej cechą charakterystyczną języka C#. Jeśli mamy do dyspozycji ograniczoną ilość miejsca, to przedstawienie wszystkich możliwych liczb z całkowitą dokładno­ ścią po prostu nie jest możliwe. Liczby zmiennoprzecinkowe, mimo swych niedokładności, są standardowym sposobem reprezentowania liczb z częścią ułamkową w większości języków programowania i dlatego takie niedokładności można spotkać w każdym z nich. ••



.•

.._,..�;



..._.___� ·

Należy zwrócić uwagę, że modyfikując kod, tak by używał on liczb typu fl oat, a nie doubl e, na końcu stałych dodaliśmy literę f - napisaliśmy na przykład O . OOOlf zamiast 0 . 000 1 . Wynika to z faktu, że C# traktuje wartości z częścią ułamkową jako liczby typu doubl e, a próbując zapisać je w zmiennej typu fl oat, ryzykujemy utratę danych ze względu na zmniejszoną precyzję. Taki kod zostałby potraktowany przez kompilator jako błąd i dlatego musimy go jawnie poinformować, że ma operować na liczbach o pojedynczej precyzji. Właśnie do tego celu służy litera f. Jeśli mamy wartość typu doubl e, którą naprawdę chcemy potraktować jako wartość fl oat, i akceptujemy zwią­ zane z tym ryzyko utraty dokładności, musimy o tym powiadomić kompilator, uży­ wając w tym celu operatora rzutowania. Oto przykład: doubl e x 1234 . 5678 ; doubl e y = x + 0 . 0001 ; fl oat i mpreci seSum = ( fl oat) (x + y) ; =

Zapis ( fl oat) jest nazywany rzutowaniem - j awnym rozkazem nakazującym kom­ pilatorowi konwersję typu danej . Ponieważ w tym przypadku konwersji typu zażą­ dano jawnie, kompilator nie potraktuje jej jako błędu.

W bardzo wielu przypadkach ograniczona dokładność nie jest zbyt dużym problemem, jeśli tylko zdajemy sobie z niej sprawę. Niemniej jednak pojawia się nieco bardziej subtelny problem związany z typami doub l e i fl aa t . Są one binarnymi reprezentacjami liczb, co zapewnia moż­ liwość najbardziej efektywnego przechowania informacji o precyzji w dostępnej dla danego typu przestrzeni . Oznacza to jednak także, że w przypadku pracy z liczbami dziesiętnymi można uzyskać zaskakujące wyniki. Na przykład liczby 0,1 nie można precyzyjnie zapisać jako wartości dwójkowej o skończonej długości. (Dzieje się tak z tego samego powodu, dla którego nie można zapisać w postaci ułamka dziesiętnego o skończonej długości ułamka 1 / 9 . W obu tych przypadkach uzyskana liczba będzie okresowa, czyli jej część będzie się powtarzać [a zatem będzie ona nieskończenie długa] . Ułamek 1/9 zapisany w postaci dziesiętnej będzie liczbą okresową 0,1111, natomiast dziesiętny ułamek 0,1 zapisany dwójkowa to okresowa liczba 0,0001100110011001 10011) . Przeanalizujmy następujący przykład:

50

Rozdział 2. Podstawowe techniki programowania

fl oat fl = O . l f ; fl oat f2 = fl + O . l f ; fl oat f3 = f2 + O . l f ; fl oat f4 = f3 + O . l f ; fl oat f5 = f4 + O . l f ; fl oat f 6 = f 5 + O . l f ; fl oat f 7 = f 6 + O . l f ; fl oat f8 = f 7 + O . l f ; fl oat f9 = f8 + O . l f ; Consol e . Wri teli n e ( f l ) ; Consol e . Wri teli ne ( f2) ; Consol e . Wri teli ne ( f3) ; Consol e . Wri teli ne ( f4) ; Consol e . Wri teli ne ( f5) ; Consol e . Wri teli ne ( f6) ; Consol e . Wri teli ne ( f7 ) ; Consol e . Wri teli ne ( f8) ; Consol e . Wri teli ne ( f9) ;

(Kiedy dojdziemy do pętli w dalszej części tego rozdziału, przekonamy się, w jaki sposób można uniknąć tworzenia takiego powtarzającego się kodu) . A oto wynik wykonania powyż­ szego przykładu: 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0 . 800000 1 0 . 9000001

Brak możliwości precyzyjnego reprezentowania wartości 0,1 nie od razu staje się widoczny, gdyż .NET zaokrągla liczby podczas ich wyświetlania, co maskuje problem. Kiedy jednak kilkukrotnie dodamy te wartości do siebie, niedokładności także się skumulują i w końcu staną się widoczne. Jak można sobie wyobrazić, księgowym by się to nie spodobało - gdyby liczby te wyrażały wartość przekazywanych pieniędzy mierzoną w miliardach, to niespodziewany brak 0,0000001 miliarda (czyli 100 zł) występujący co każde osiem transakcji zostałby niewąt­ pliwie uznany za błąd w sztuce. I właśnie z tego powodu istnieje specjalny typ liczbowy prze­ znaczony do wykonywania operacji na liczbach z częściami ułamkowymi .

Zmiennoprzecinkowe l iczby dz iesiętne Typ dec i mal (bądź Sys tern . Dec i mal ) jest na pierwszy rzut oka bardzo podobny do typów fl aa t oraz doub l e . Różnica polega jednak na tym, że wewnętrzny sposób reprezentowania wartości został w nim dostosowany do liczb w systemie dziesiętnym. Liczby w tym typie mogą być zapisywane z dokładnością do 28 miejsc po przecinku i, w odróżnieniu od typów fl oat i doubl e, każdą liczbę, którą da się zapisać przy użyciu tych 28 miejsc (lub ich mniejszej ilości), można przechowywać w zmiennej typu dec i ma l z zachowaniem całkowitej dokładności. Wartość 0,1 mieści się w tych granicach, a zatem zastosowanie typu dec i ma l w ostatnim przykładzie roz­ wiązałoby nasz problem. Jednak także dokładność tego typu ma pewne ograniczenia - jego działanie daje tylko mniej niespodziewane efekty, gdy rozważamy wartości zapisane jako liczby dziesiętne.

Zmienne

51

A zatem jeśli wykonujemy obliczenia na sumach pieniężnych, to zastosowanie typu deci mal da najprawdopodobniej lepsze efekty niż użycie typów doubl e i fl oat. Jest to jednak możliwe kosztem nieznacznego pogorszenia wydajności - komputery znacznie lepiej radzą sobie z liczbami reprezentowanymi w systemie dwójkowym niż w dziesiętnym. W naszej przykła­ dowej aplikacji obliczającej statystyki samochodu wyścigowego nie jest nam potrzebna dokład­ ność, jaką zapewnia typ d e c i m a l , i właśnie z tego powodu w kodzie przedstawionym na listingu 2.5 zastosowany został typ doub l e . Wracając do naszego przykładu, Czytelnik zapewne pamięta, że zostały w nim zdefiniowane trzy zmienne przechowujące odpowiednio informacje o: odległości, jaką samochód pokonał, czasie, jaki mu to zajęło, oraz masie zużytego w tym celu paliwa. Poniżej jeszcze raz przedsta­ wiliśmy kod przykładu. s t at i c vo i d Mai n (s tri ng O arg s ) { doubl e kmTravel l ed = 5 . 14 ; doubl e el apsedSeconds = 78 . 73 ; doubl e fuel Ki l o s Cons umed = 2 . 7 ;

Teraz, kiedy przedstawiliśmy już liczbowe typy danych, struktura i znaczenie tego kodu są oczywiste. Zaczynamy od typu danych, na jakim chcemy operować, następnie podajemy nazwę danej i w końcu używamy znaku równości, by przypisać zmiennej określoną wartość. Jednak przypisywanie wartości stałym nie jest szczególnie pasjonujące . Komputery można wykorzy­ stać do wykonywania znacznie bardziej użytecznych operacji - można bowiem zapisać w zmiennej wartość wyrażenia.

Wyrażenia i instru kcje Wyrażenie to fragment kodu, który generuje pewną wartość. W przedstawionych do tej pory przykładach można znaleźć kilka wyrażeń - w większości przypadków są to liczby przypi­ sywane zmiennym. A zatem w naszym przykładzie liczba taka jak 5 . 14

jest wyrażeniem. Wyrażenia, w których podawana jest konkretna wartość, są nazywane literałami. Co ciekawe, wyrażenia mogą wykonywać obliczenia. Na przykład przy użyciu wyrażenia przed­ stawionego na listingu 2.6 można obliczyć odległość pokonaną na jednym kilogramie paliwa.

Listing 2.6. Dzielenie jednej zmiennej przez drugą kmTravel l ed / fuel Ki l os Cons umed

Symbol / reprezentuje operację dzielenia. Kolejne operacje - mnożenie, dodawanie i odejmo­ wanie - są zapisywane przy użyciu następujących operatorów: *, + oraz - . Oprócz tego istnieje możliwość łączenia wyrażeń. Operator / wymaga przekazania dwóch danych wejściowych - dzielnika oraz dzielnej - a każda z nich może być kolejnym wyraże­ niem. W powyższym przykładzie mogliśmy użyć nazw zmiennych, gdyż są one prawidłowymi wyrażeniami - w razie ich zastosowania wartością wyrażenia jest po prostu wartość zmiennej . Jednak równie dobrze możemy zastosować literały, co pokazuje listing 2.7. (Nieuważni pro­ gramiści mogą tu wpaść w pewną pułapkę, która została opisana w poniższej ramce) .

52

Rozdział 2. Podstawowe techniki programowania

Dzielenie całkowite i zmiennoprzecinkowe Istnieje pewna drobna różnica pomiędzy tym, jak działa dzielenie w kodach przedstawionych na listingu 2.6 oraz 2.7. Ponieważ dwa literały z listingu 2.7 nie zawierają miejsc dziesiętnych, kom­ pilator potraktuje je jako liczby całkowite i wykona operacj ę dzielenia całkowitego. Jednak obie zmienne: kmTravel l ed oraz fuel Ki l osConsumed, są zadeklarowane jako zmiennoprzecinkowe, dlatego w ich przypadku zostanie wykonane dzielenie zmiennoprzecinkowe. W tym konkretnym przykładzie nie ma to wielkiego znaczenia, gdyż podzielenie liczby 60 przez 10 daje w wyniku kolejną liczbę całkowitą 6. Co by się jednak stało, gdyby wynik nie był liczbą całkowitą? Na przykład gdyby wyrażenie miało postać: -

3 / 4 to w wyniku uzyskalibyśmy wartość o, gdyż liczba 4 nie mieści się ani razu w liczbie 3. Gdyby jednak kod miał następującą postać: doubl e doubl e

x

y

=

=

3; 4;

to wyrażenie x / y zwróciłoby wartość 0 . 75, gdyż w tym przypadku kompilator C# zastosowałby dzielenie zmiennoprzecinkowe, które doskonale radzi sobie z wynikami zawierającymi części ułamkowe. Aby użyć działań zmiennoprzecinkowych na literałach, należałoby je zapisać w nastę­ pujący sposób: 3.o / 4.o Dodanie do literału cyfry po kropce dziesiętnej oznacza, że chcemy operować na liczbach zmien­ noprzecinkowych, czyli wykonać operację dzielenia zmiennoprzecinkowego. Dlatego w rezultacie otrzymamy liczbę o . 75.

Listing 2 .7. Dzielenie dwóch literałów 60 ; 10

Nic także nie stoi na przeszkodzie, by zastosować kombinację zmiennej i literału w celu obli­ czenia czasu jazdy wyrażonego w minutach: el apsedSeconds / 60

Ewentualnie, używając wyrażenia będącego iloczynem dwóch literałów jako argumentu operacji dzielenia, możemy obliczyć czas jazdy wyrażony w godzinach: el apsedSeconds / ( 60

*

60)

(Nawiasy gwarantują, że wartość zmiennej zostanie podzielona przez iloczyn dwóch liczb 60. Gdybyśmy ich nie zastosowali, to zostałaby ona najpierw podzielona przez 60, a następnie pomnożona przez tę samą wartość; w efekcie obliczenie byłoby całkowicie bezużyteczne . Więcej informacji na ten temat można znaleźć w ramce zamieszczonej na następnej stronie) . Wynik tego wyrażenia mógłby nam posłużyć do obliczenia szybkości samochodu wyrażonej w kilo­ metrach na godzinę: kmTravel l ed / (el apsedSeconds / (60

*

60) )

W rzeczywistości same wyrażenia niczego nie robią. Opisują one obliczenia, jednak to kom­ pilator C# musi wiedzieć, co należy zrobić ze zwróconym wynikiem. Wyrażenia można sto­ sować w różnych celach. Mogą one posłużyć na przykład do inicjalizacji wartości jakiejś zmiennej: doubl e kmPerHour

=

kmTravel l ed / (el apsedSeconds / (60

*

60) ) ;

Wyrażenia i instrukcje

53

Równie dobrze można wyświetlić wynik wyrażenia w oknie konsoli: Consol e . Wri teli ne ( kmTravel l ed / (el apsedSeconds / (60

*

60) ) ) ;

Oba przedstawione powyżej wiersze kodu są przykładami instrukcji. Podczas gdy wyrażenia opisują obliczenia, instrukcje określają operacje, jakie należy wykonać. W dwóch ostatnich przykładach zastosowaliśmy to samo wyrażenie - obliczenie szybkości samochodu wyścigowego - jednak obie instrukcje użyły jego wyniku w różnych celach: pierw­ sza obliczyła jego wynik i zapisała go w nowej zmiennej, natomiast druga przekazała obliczony wynik w wywołaniu metody Wri tel i n e klasy Con sol e.

Kolejność przetwarzania Język C# posiada zbiór reguł służących do określania kolejności, w jakiej należy przetwarzać poszcze­ gólne elementy wyrażeń. Wcale nie jest powiedziane, że będą one wykonywane od strony lewej do prawej - wynika to z faktu, że niektóre operatory mają wyższy priorytet niż inne. Wyobraźmy sobie, że wyrażenie i .o + 3.o / 4.o jest przetwarzane od strony lewej do prawej . W takim przypadku zaczęlibyśmy od wartości 1 . 0, do której zostałaby dodana wartość 3 . 0, co dałoby wynik 4 . 0. Z kolei ta wartość zostałaby podzielona przez 4 . 0, dając ostatecznie 1 . 0. Jednak zachowując zgodność ze standardowymi regułami wyko­ nywania obliczeń arytmetycznych, powinniśmy uzyskać w wyniku wartość jeden i trzy czwarte. I dokładnie taką wartość - 1 . 75 - zwróci C#. Dzielenie zostanie wykonane przed dodawaniem, gdyż ma ono wyższy priorytet. Niektóre grupy operatorów mają jednak takie same priorytety. Dotyczy to na przykład operatorów mnożenia i dzielenia. Jeśli wyrażenie zawiera kilka operacji o tym samym priorytecie, to będą one wykonywane w kolejności zapisu, od lewej do prawej . A zatem 1 0 . 0 / 2 . 0 * 5 . 0 da w wyniku 25 . 0. Jednak nawiasy pozwalają zmieniać kolejność wykonywania operacji, dlatego obliczenie wyrażenia 1 0 . 0 / (2 . 0 * 5 . 0) zwróci wynik równy 1 . 0. Niektóre książki programistyczne bardzo szczegółowo opisują priorytety operatorów, co znacznie utrudnia ich lekturę - w C# istnieje aż 15 różnych poziomów priorytetów. Szczegółowe informacje na ich temat mogą się przydać twórcom kompilatorów, lecz dla zwykłych programistów będą miały znikomą wartość, gdyż kod, który bazuje na hierarchii operatorów, jest trudny do przeanalizowania i zrozumienia. Stosując nawiasy w celu wymuszenia i jawnego określenia kolejności wykonywania działań, można w dużym stopniu poprawić czytelność kodu. Niemniej jednak jeśli Czytelnik chce przejrzeć szczegółowe informacje dotyczące hierarchii operatorów, może je znaleźć na stronie

http://msdn.microsoft.com/en-us/library/aa691323 . ••



.

·

'-"�,'

,

L-------11.J"o '

Typ wyrażenia ma duże znaczenie . W przykładach przedstawionych do tej pory używane były liczby oraz zmienne zawierające liczby typu doubl e lub i nt. Wyrażenia mogą jednak być dowolnego typu. Na przykład ( "Wi taj , " + " świ eci e " ) to wyrażenie typu stri ng. Gdybyśmy spróbowali napisać instrukcję, która chciałaby zapisać wartość takiego wyrażenia w zmiennej typu doubl e, kompilator zgłosiłby błąd . Wymaga on bowiem, by wyrażenia zwracały wartość bądź to tego samego typu, jakiego jest zmienna, w której chcemy je zapisać, bądź też typu, który można w niejawny sposób prze­ konwertować na typ zmiennej . Niejawna konwersja pomiędzy typami numerycznymi jest możliwa, jeśli jej wykonanie nie spowoduje utraty danych. Na przykład typ doubl e może reprezentować dowolną wartość typu i nt, dlatego też nic nie stoi na przeszkodzie, by zapisać wartość wyraże­ nia tego typu w zmiennej typu doubl e. Jednak próba niejawnej konwersji wartości doubl e

54

Rozdział 2. Podstawowe techniki programowania

na typ i nt spowodowałaby wystąpienie błędu, gdyż wartości typu doubl e mogą być znacznie większe od wartości i nt, a co więcej mogą zawierać część ułamkową, która w wyniku konwersji zostałaby bezpowrotnie utracona. Jeśli nie przejmujemy się moż­ liwością utraty danych, to przed wyrażeniem możemy umieścić operator rzutowania: i nt approxKmPerHour = ( i nt) kmPerHou r ;

W tej instrukcji wartość zmiennej kmPerHour (która wcześniej została zadeklarowana jako zmienna typu doubl e) jest rzutowana na typ i nt . Oznacza to, że zostanie ona przekształcona na liczbę całkowitą, co może się wiązać z utratą informacji.

Zmienne nie muszą zawierać tej samej wartości przez cały okres swojego istnienia. Ich wartość można zmodyfikować w dowolnym momencie.

I nstrukcje przypisania W poprzedniej części rozdziału pokazaliśmy, w jaki sposób można przypisać wartość wyrażenia do nowo utworzonej zmiennej: doubl e kmPerHour = kmTravel l ed / (el apsedSeconds / (60

*

60) ) ;

Jeśli podczas któregoś z późniejszych etapów działania programu pojawią się nowe informacje, będzie można zapisać je w zmiennej kmPerHo ur. Instrukcje przypisania nie muszą operować wyłącznie na nowych, właśnie zadeklarowanych zmiennych - nic nie stoi na przeszkodzie, by przypisywać wartości zmiennym już istniejącym: kmPerHour = updatedKmTravel l ed / (updatedEl apsedSeconds / (60

*

60) ) ;

Powyższa instrukcja nadpisuje wartość przechowywaną w zmiennej kmPerHo ur. C# udostępnia wyspecjalizowane instrukcje przypisania, które pozwalają tworzyć nieco krótszy kod. Załóżmy, że chcemy dodać czas, w jakim samochód wykonał ostatnie okrążenie toru, do sumarycznego czasu jazdy. Można to zrobić w następujący sposób: el apsedSeconds = e l apsedSeconds + l atest lapT i me ;

Instrukcja ta przetwarza wyrażenie zapisane z prawej strony operatora przypisania i zapisuje jego wartość w zmiennej podanej z lewej strony. Jednak operacja polegająca na dodaniu wartości do zmiennej jest wykonywana tak często, że utworzono specjalną składnię ułatwiającą jej wykonywanie: el apsedSeconds += l atest lapT i me ;

Powyższa instrukcja daje dokładnie takie same wyniki jak poprzednia. Istnieją także jej inne wersje wykorzystujące pozostałe działania arytmetyczne; a zatem -= oznacza odjęcie od zmien­ nej wartości wyrażenia podanego z prawej strony operatora, *= - pomnożenie zmiennej przez wartość, i tak dalej .

Operatory i n krementacji i dekrementacji Skoro już zajmujemy się zagadnieniem zmieniania wartości, warto także przyjrzeć się ope­ ratorom inkrementacji i dekrementacji . Jeśli chcemy rejestrować liczbę przejechanych okrążeń toru, moglibyśmy zwiększać wartość jakiejś zmiennej o 1 za każdym razem, gdy auto zakończy okrążenie: l apCount += 1 ;

Wyrażenia i instrukcje

SS

Twórcy języka C uznali, iż operacja zwiększania o jeden ma na tyle duże znaczenie, że należy wymyślić dla niej specjalną składnię nazywaną operatorem inkrementacji; C# również ją udostępnia: l apCount++ ;

Istnieje także operator dekrementacji, --, który zmniejsza wartość o jeden. Kod przedstawiony powyżej jest instrukcją, jednak operatory inkrementacji oraz dekrementacji można także stoso­ wać w wyrażeniach: i nt current lap

=

l apCount++ ;

Używając tych operatorów, trzeba jednak zachować ostrożność. Wyrażenie umieszczone w powyższym przykładzie po prawej stronie operatora przypisania oznacza „zastosuj bieżącą wartość zmiennej l a pCount, a następnie, kiedy zostanie już użyta, zwiększ ją o jeden" . A zatem jeśli przed wykonaniem instrukcji wartość zmiennej l apCount wynosiła 3, to po jej wykonaniu wartość zmiennej c urren t lap będzie wynosić 3, a zmiennej l apCount - 4 . Gdybyśmy chcieli użyć zaktualizowanej wartości, to operator inkrementacji (lub dekrementacji) należałoby umie­ ścić przed zmienną (lub wartością, na jakiej ma operować): i nt current lap

=

++l apCount ;

Można by napisać program składający się w całości z deklaracji zmiennych, instrukcji przypi­ sania, inkrementacji oraz wywołań metod, jednak nie byłby on szczególnie interesujący - zawsze wykonywałby tę samą sekwencję operacji w dokładnie tej samej kolejności . Na szczęście C# udostępnia kilka bardziej interesujących instrukcji zapewniających programom możliwość podej­ mowania decyzji, które zmieniają przebieg realizacji kodu. Czasami są one nazywane instruk­ cjami sterowania przepływem.

I nstru kcje sterowania przepływem i wyboru Instrukcje wyboru określają, która z dostępnych ścieżek kodu zostanie następnie wykonana. Wybór ten dokonywany jest na podstawie wyrażenia. Używając instrukcji wyboru, można by określić, czy na następnym okrążeniu toru w naszym samochodzie wyścigowym może zabrak­ nąć paliwa, i wyświetlić odpowiednie ostrzeżenie . Język C# udostępnia dwie instrukcje tego typu: i f oraz swi t c h . Aby przedstawić działanie wyboru na przykładzie, musimy wprowadzić w naszym programie niewielkie zmiany. Aktualnie wszystkie używane w nim informacje - przejechany dystans, zużyte paliwo oraz czas, jaki upłynął od startu - są podane na stałe w formie literałów bez­ pośrednio w kodzie . Z tego względu umieszczenie instrukcji wyboru w tym przykładzie nie byłoby interesujące - program za każdym razem podejmowałby taką samą decyzję, gdyż używane przez niego informacje cały czas są takie same. A zatem żeby umożliwienie wyboru miało jakikolwiek sens, musimy zmienić program w taki sposób, by pozwalał użytkownikowi na podawanie danych wejściowych. Ponieważ nasz program jest aplikacją konsolową, niezbędne informacje mogą być przekazywane w formie argumentów umieszczanych w wierszu wywo­ łania. W takim przypadku podczas uruchamiania programu byłby podawany całkowity przejechany dystans, czas, jaki jego pokonanie zajęło, oraz ilość zużytego paliwa, co pokazano poniżej .

56

Rozdział 2. Podstawowe techniki programowania

Racel n fo 20 , 6 3 12 , 8 1 0 , 8 5

Zmodyfikowana wersja programu, w której używane informacje są pobierane z wiersza pole­ ceń, a nie zakodowane na stałe, została przedstawiona na listingu 2.8 .

Listing 2 .8. Odczyt danych z wiersza polece1i s t at i c vo i d Mai n (s tri ng O arg s ) { doubl e kmTravel l ed = doubl e . Parse (args [O] ) ; doubl e el apsedSeconds = doubl e . Parse (args [l] ) ; doubl e fuel Ki l o s Cons umed = doubl e . Parse (args [2] ) ;

Zanim dodamy do przykładu instrukcję wyboru, warto zwrócić uwagę na kilka interesujących zagadnień. Przede wszystkim trzeba sobie przypomnieć, że do metody Ma i n, stanowiącej punkt wejścia do naszego programu, przekazywana jest sekwencja łańcuchów znaków - a rg s reprezentująca argumenty podane w wierszu wywołania. Sekwencja ta jest tablicą - konstruk­ cją używaną w .NET do przechowywania wielu elementów tego samego typu. (Można utwo­ rzyć tablicę dowolnego typu - liczb, łańcuchów znaków, obiektów o jakimś określonym typie. Zapis stri ng [] oznacza, że do metody należy przekazać tablicę łańcuchów znaków) . W kodzie, wewnątrz jakiegoś wyrażenia, możemy pobrać konkretny element takiej tablicy, umieszczając za nazwą zmiennej parę nawiasów kwadratowych, a wewnątrz nich wpisując liczbę. Jak widać, w trzech pierwszych wierszach ostatniego przykładu zostały zastosowane wyrażenia arg s [OJ , args [1] oraz args [2] pobierające odpowiednio pierwszy, drugi i trzeci element tablicy, czyli trzy argumenty przekazane w wierszu wywołania programu. '

.' '

'

W j ęzykach należących do rodziny C elementy są zazwyczaj liczone od O. To samo dotyczy języka C#. Może się to wydawać nieco dziwne, lecz dla komputera ma to sens. Można to sobie wyobrazić w ten sposób, że określamy, jak daleko względem początku tablicy jest przesunięty interesujący nas element, a zatem przesunięcie o wartość zero pozwala pobrać pierwszy z jej elementów. Odpowiada to logice stosowanej przy numerowaniu kondygnacji - pierwsze piętro nie jest kondygnacją znajdującą się na poziomie ziemi, lecz położoną na pierwszym poziomie ponad nią.

Trzeba także zwrócić uwagę na zastosowanie metody doubl e . Parse. Argumenty z wiersza pole­ ceń są przekazywane do programu jako łańcuchy znaków, ponieważ użytkownik może wpisać cokolwiek, na przykład: Racel n fo Jenson Button Roc ks

Jednak nasz program oczekuje liczb. Musimy zatem zrobić coś, by przekonwertować te łańcuchy znaków na postać liczbową. I właśnie do tego służy metoda doubl e . Parse: oczekuje ona, że przekazany do niej tekst jest liczbą zapisaną w systemie dziesiętnym, i konwertuje ją na liczbę zmiennoprzecinkową o podwójnej precyzji. (Jeśli Czytelnik zastanawia się, co by się stało, gdyby przekazany do tej metody łańcuch znaków nie zawierał liczby, to informujemy, że metoda

5

Zwróćmy uwagę, że w odróżnieniu od sposobu zapisywania liczb w kodzie źródłowym C#, w którym część dziesiętna liczby jest poprzedzona kropką, w przypadku podawania liczb w wierszu wywołania programu należy miejsca dziesiętne poprzedzić przecinkiem. W przeciwnym przypadku próba wykonania powyższego programu spowoduje zgłoszenie wyjątku FormatException. Różnica ta wynika z polskich ustawień regional­ nych, w których separatorem dziesiętnym jest przecinek - przyp. tłum. Instrukcje sterowania przepływem i wyboru

57

zgłosiłaby wyjątek. Dokładniejsze informacje o tym, co to znaczy oraz co należy w takim przypadku zrobić, zostały podane w rozdziale 6 .; na razie oznacza to dla nas tylko tyle, że nasz program przestałby działać i zgłosiłby wystąpienie błędu) . Powyższy przykład pokazuje również, że wywołania metod mogą być wyrażeniami - metoda Parse typu doubl e zwraca wartość typu doubl e, co oznacza, że można jej użyć do zainicjalizowa­ nia zmiennej tego typu. Wszystko to są jednak szczegóły - najważniejsze jest to, że aktualnie możemy przekazywać do naszego programu informacje, które za każdym razem mogą być inne. Na przykład inżynier pracujący w pit stopie może uruchamiać program za każdym razem, gdy samochód wyścigowy zakończy okrążenie. A zatem nasza aplikacja może już podejmować sensowne decyzje na pod­ stawie przekazywanych danych wejściowych, używając w tym celu instrukcji wyboru. Jedną z tych instrukcji jest i f.

I nstrukcje if Instrukcja i f jest instrukcją wyboru, która na podstawie wartości wyrażenia podejmuje decyzję, jaki blok kodu należy wykonać. Można jej użyć w naszym programie do wyświetlenia ostrze­ żenia o niebezpiecznie niskim poziomie paliwa. W tym przypadku instrukcję i f dodamy na końcu kodu metody Mai n, jak pokazano na listingu 2 .9 . Większość kodu wykonuje obliczenia, przygotowując dane do podjęcia decyzji. Sama decyzja jest podejmowana przez instrukcję i f umieszczoną na końcu przykładu - określa ona, czy należy wykonać blok kodu umieszczony w nawiasach klamrowych.

Listing 2 9 Instrukcja if .

.

doubl e fuel Tan kCapaci tyKi l os doubl e l aplength = 5 . 14 1 ; doubl e doubl e doubl e doubl e

=

80 ;

fuel Ki l os PerKm fuel Ki l osCons umed / kmTravel l ed ; fuel Ki l os Rema i n i ng = fuel Tan kCapac i tyKi l os - fuel Ki l os Cons umed ; pred i ctedDi s tanceUnt i l OutOfFuel = fuel Ki l os Rema i n i ng / fuel Ki l os PerKm ; pred i ctedlapsUnt i l OutOfFuel = pred i ctedD i stanceUn t i l OutOfFuel / l ap length ; =

i f (predi ctedlapsUnti l OutOfFuel < 4)

{ }

Consol e . Wri teli ne ("Mało pal i wa . Moż n a j eszcze przej echać " + predi ctedlapsUnti l OutOfFuel + " okrąż eni a . " ) ;

Aby przetestować nową wersję programu, musimy uruchomić go, przekazując do niego argu­ menty z poziomu wiersza poleceń. By to zrobić, można otworzyć okno wiersza poleceń, a następnie przejść do folderu zawierającego skompilowany projekt i uruchomić program, przekazując do niego odpowiednie argumenty. (Program będzie się znajdował w katalogu bin/Debug utworzonym przez Visual Studio w katalogu projektu) . Jednak wygodniejszym roz­ wiązaniem będzie poinstruowanie Visual Studio, by przekazało odpowiednie argumenty pod­ czas uruchamiania programu. W tym celu należy przejść do panelu Solution Explorer i dwu­ krotnie kliknąć opcję Properties . W efekcie zostanie wyświetlone okno dialogowe właściwości zawierające po lewej stronie serię kart. W tym oknie należy wybrać kartę Debug - w jej środ­ kowej części zobaczymy wielowierszowe pole tekstowe o nazwie Command line arguments przedstawione na rysunku 2.6.

58

Rozdział 2. Podstawowe techniki programowania

Sta rt Opti om

-------

C o m m a n d l i ne argu men ts:

1

1 41 .95 2156 .2 75 .6

Rysunek 2.6. Określanie w Visual Studio argumentów przekazywanych do programu z poziomu wiersza polemi Jeśli uruchomimy program z argumentami odpowiadającymi przejechaniu jedynie kilku okrą­ żeń toru (na przykład: 1 5 238 8), to na ekranie nic się nie pojawi. Spróbujmy jednak wykonać go z argumentami o następujących wartościach: 1 4 1 . 95 2 1 56 . 2 7 5 . 6 . W tym przypadku program oszacuje, że na posiadanym paliwie samochód może jeszcze przejechać około 1,6 okrążenia . Instrukcja i f z listingu 2.9 testuje następujące wyrażenie: predi cted laps Unt i l OutOfFuel < 4

Symbol < oznacza „mniejszy niż" . A zatem kod zapisany w nawiasach klamrowych zaczyna­ jących się za instrukcją i f zostanie wykonany wyłącznie w przypadku, gdy liczba przewidy­ wanych okrążeń będzie mniejsza od czterech. Oczywiście 1,6 jest mniejsze od 4, więc kod zo­ stanie wykonany i wyświetli poniższy komunikat. Mało pal i wa . Można j es zcze przej echać 1 . 6070 1 035044548 o krążen i a .

W instrukcjach i f należy stosować wyrażenia odpowiedniego typu. W tym przypadku doko­ naliśmy porównania - sprawdza ono, czy wartość zmiennej jest mniejsza od 4. Istnieją tylko dwa możliwe wyniki takiego porównania: albo wartość zmiennej jest mniejsza od 4, albo nie jest. A zatem wyrażenie to ma zupełnie inną naturę niż wyrażenia wykonujące operacje matema­ tyczne. Gdyby zmodyfikować nasz program tak, by wyświetlał wartość tego wyrażenia: Consol e . Wri teli ne (predi cted laps Unt i l OutOfFuel < 4) ;

to wyświetlane byłyby wyniki True lub Fal se. W .NET Framework dostępny jest specjalny typ służący właśnie do reprezentowania takich wyborów z dwoma opcjami. Jest to System . Baal ea n . Podobnie jak typy liczbowe, także ten posiada w języku C# nazwę zastępczą - boa 1 6 • Instrukcja i f wymaga zastosowania wyrażenia logicznego (boole'owskiego) . Gdybyśmy spróbowali użyć wyrażenia zwracającego wartość liczbową, takiego jak: i f (fuel Tan kCapaci tyKi l os - fuel Ki l os Cons umed)

kompilator zgłosiłby błąd: „Cannot implicitly convert type 'double' to 'bool'." 7 . W ten sposób poinformowałby, że oczekuje wartości typu bool - wartości t rue lub fal s e - a my przeka­ zaliśmy mu liczbę. W rzeczywistości nasz kod należałoby wówczas rozumieć w następujący sposób: „Jeśli czternaście i pół, to zrób to i to" . Co miałoby to znaczyć? C# udostępnia kilka operatorów, które, podobnie jak przedstawiony na listingu 2.9 operator cl ocked i nStaff = new Li st () ; publ i c vo i d Cl ockln ( I NamedPerson s taffMember) { i f ( ! cl ocked i nStaff . Conta i ns ( s t affMember) ) { cl ockedi nStaff . Add ( s t affMember) ; Consol e . Wri teli ne ( " { O } s i ę zarej estrował . " , s t affMember . Name) ;

publ i c vo i d Rol l Cal l () { foreach ( I NamedPerson staffMember i n cl ockedinStaff) { Consol e . Wri teli ne (staffMember . Name) ;

•• ._.,.„,;

• .·

.

L-------11.J"o '

Jeśli Czytelnik na bieżąco wpisywał i testował przykładowe fragmenty kodu w Visual Studio (do czego gorąco zachęcamy), to przypominamy, że trzeba także zmienić inicja, l"1zator ob"1ektu z powrotem na postac: Fi refi ghter j oe

=

new Fi refi ghter { Name = " Józek" } ;

Po skompilowaniu i wykonaniu tej najnowszej wersji naszego programu w końcu uzyskamy takie wyniki, o jakie nam chodziło - listę wszystkich osób, które przyszły i zarejestrowały się na posterunku: Józek s i ę zarej estrował . Wi l he l m s i ę zarej estrował . Hen i e k s i ę zarej e s t rował .

C# obsługuje wielokrotne dziedziczenie interfejsów

151

P . Artur Pytal s ki s i ę zarej es trował . Józek Wi l he l m Hen i e k P . Artur Pytal s k i

Tworzenie jednych interfejsów na baz ie innych Także interfejsy udostępniają możliwość dziedziczenia . Gdyby tylko naszła nas taka ochota, moglibyśmy utworzyć interfejs opisujący osobę posiadającą imię i pobierającą jakieś wyna­ grodzenie: i nterface I N amedSal ari edPerson : I N amedPerson , I S a l ari edPerson { }

A co by się stało w przypadku wystąpienia konfliktu nazw? Wyobraźmy sobie kolejny interfejs,

I Set tabl eNamed Person, o następującej postaci: i nterface I Settabl eNamedPerson { s t r i ng Name { get ; set ;

Co się stanie, gdy w klasie F i refi g h t er B a s e spróbujemy zaimplementować oba interfejsy: I Named Person oraz I Settab l eNamed Person? abstract cl ass Fi refi ghterBase : I N amedPerson , I Settabl eNamed Person , ISal ari edPerson { li „.

Otóż okaże się, że wszystko jest w wyśmienitym porządku! Każdy z tych dwóch interfejsów wymaga od nas zaimplementowania właściwości Name typu stri ng. Ponadto jeden z nich wymaga implementacji co najmniej akcesom get, a drugi obu akcesorów - get i s et . Odwołując się do właściwości za pośrednictwem odpowiedniego interfejsu, obiekt jest w stanie prawidłowo określić, o jaką składową nam chodziło. Nie ma jednak żadnego wymogu, by tworzyć odrębne implementacje dla każdego z interfejsów. A co w przypadku, gdyby taka interpretacja była błędna? Innymi słowy, co w sytuacji, gdy

właściwość Name interfejsu I N amed Person ma całkowicie inne znaczenie od analogicznej właściwości interfejsu I Settabl e N amedPerson? Załóżmy, że jedna z nich pozwala na stosowanie wyłącznie liter i cyfr, bez żadnych znaków odstępu, natomiast druga dopuszcza dowolną postać tekstu taką, do jakiej jesteśmy przyzwyczajeni. Zawsze gdy kod używający naszej klasy oczekuje obiektu I NamedPerson, musimy dostarczyć drugą implementację, natomiast gdy oczekuje on obiektu I Settabl eNamed Person - pierwszą z nich. Można to zrobić, implementując interfejsy w sposób jawny.

152

Rozdział 4. Rozszerzalność i polimorfizm

Jawna implementacja interfejsów Aby w jawny sposób zaimplementować konkretną składową interfejsu, należy pominąć mody­ fikator dostępności i poprzedzić nazwę składowej nazwą interfejsu, tak jak pokazuje to kod przedstawiony na listingu 4.24.

Listing 4.24. Jawna implementacja interfejsu cl ass AFoot i nBothCamps : I N amedPerson , I Settabl eNamedPerson { pri vate s t r i ng settabl eName ; stri ng INamedPerson . Name

{ get { Consol e . Wri teli ne ( " Odwo ł an i e real i zowane przy użyc i u i nterfej s u I N amedPerson . " ) ; return settabl eName ;

stri ng I Settabl eNamedPerson . Name

{ get { return settabl eName ; set { Consol e . Wri teli ne ( "Odwołan i e real i zowane przy użyc i u i nterfej s u " + " I Settabl eNamedPerson . " ) ; i f (settabl eName ! = n u l l && settabl eName . Conta i ns ( " " ) ) { li Nie można ustawić wartości, jeśli zawiera ona znak li odstępu. ret u rn ; settabl eName

val ue ;

Listing 4.25 pokazuje, w jaki sposób będziemy się odwoływać do tych właściwości w metodzie Mai n naszego programu.

Listing 4.25. Zastosowanie jednego obiektu do wywoływania składowej o tej samej nazwie zaimplementowanej w dwóch interfejsach cl ass Program { s t at i c vo i d Ma i n (stri ng O args) { AFoo t i n BothCamps both = new AFoo t i n BothCamps () ; I Settabl eNamedPerson settabl e Person = both ; I NamedPerson namedPerson = both ;

settabl ePerson . Name = " s i ema " ;

Tworzenie jednych interfejsów na bazie innych

153

Consol e . Wr i teli ne (settabl ePerson . Name) ; Consol e . Wr i teli ne (namedPerson . Name) ; Consol e . ReadKey () ;

Należy zwrócić uwagę na sposób utworzenia obiektu oraz dwóch dodatkowych referencji do niego. Jedna z nich jest zapisana w zmiennej typu I Settabl eNamed Person, a druga - w zmiennej typu I Named Person . Następnie program odwołuje się do właściwości Name za pośrednictwem każdego z tych dwóch interfejsów, generując przy tym następujące wyniki: Odwołan i e real i zowane przy użyc i u i nterfej s u I Settabl eNamedPerson . s i ema Odwołan i e real i zowane przy użyc i u i nterfej s u I N amedPerson . s i ema

Co by się stało, gdybyśmy spróbowali odwołać się do właściwości Name, korzystając z pierwszej ze zmiennych, czyli z tej, której typem jest klasa, a nie interfejs? Consol e . Wri teli ne (bot h . Name) ;

Dodajmy powyższy wiersz kodu do metody Mai n i skompilujmy ją. Okaże się, że kompilator wyświetli następujący komunikat o błędzie: ' AFoot i n BothCamps ' does not conta i n a defi n i t i on for ' Name ' and no extens i on method ' Name ' accept i ng a fi rs t argument of type ' AFoot i n Both Camps ' cou l d be found (are you mi s s i ng a us i ng d i rect i ve or an assemb l y reference ? ) 0

Spotkaliśmy się z tym błędem już wcześniej . Oznacza on, ż e staramy się skorzystać ze skła­ dowej, która nie istnieje. Okazuje się, że składowe zaimplementowane jawnie istnieją wyłącznie w przypadku, gdy odwołujemy się do nich za pośrednictwem odpowiedniego interfejsu. Niemniej jednak, o ile tylko zaimplementowaliśmy jawnie jedną spośród dwóch składowych (dwie spośród trzech lub ile tylko nam było potrzebnych), to zawsze możemy wybrać jeden z interfejsów jako „domyślny" i zaimplementować go, używając zwyczajnej składni (co poka­ zuje listing 4.26) .

Listing 4.26. Jawne zaimplementowanie jednego spośród interfejsów cl ass AFoot i nBothCamps : I N amedPerson , I Settabl eNamedPerson { pri vate s t r i ng settabl eName ;

li Standardowa składnia implementacji publ i c s t r i ng Name { get { Consol e . Wri teli ne ( " Odwo ł an i e real i zowane przy użyc i u i nterfej s u I N amedPerson . " ) ; return settabl eName ;

s t r i ng I Settab l eNamedPerson . Name 11 AFootlnBothCamps nie zawiera definicji Name ani nie udało się znaleźć metody rozszerzającej Name pobierającej pierwszy argument typu AFootlnBothCamps (czy nie pominięto dyrektywy using lub referencji do podzespołu?) przyp. tłum.

-

154

Rozdział 4. Rozszerzalność i polimorfizm

get { return settabl eName ; set { Consol e . Wri teli ne ( " Odwo ł an i e real i zowane przy użyc i u i nterfej s u " + + " I Settabl eNamedPerson . " ) ; i f (settabl eName ! = n u l l && settabl eName . Conta i ns ( " " ) ) { li Nie można ustawić wartości, jeśli zawiera ona znak li odstępu. ret u rn ; settabl eName

val ue ;

Teraz możemy skompilować i uruchomić kod, a domyślną implementacją właściwości używaną w klasie będzie implementacja z interfejsu I Named Person . Odwołan i e real i zowane przy użyc i u i nterfej s u I Settabl eNamedPerson . s i ema Odwołan i e real i zowane przy użyc i u i nterfej s u I N amedPerson . s i ema Odwołan i e real i zowane przy użyc i u i nterfej s u I N amedPerson . s i ema

'

..

W codziennej praktyce konieczność jawnego implementowania interfejsów pojawia się raczej sporadycznie. Jeśli posiadamy kontrolę nad całym kodem aplikacji, powin­ niśmy unikać sytuacji, w których występują konflikty pomiędzy składowymi o tych samych nazwach, lecz innym znaczeniu. Takie rozwiązania zaskakują jedynie innych programistów podobnie jak przeciążanie i przesłanianie metod połączone ze zmianą ich znaczenia. W platformie .NET Framework można znaleźć kilka przykładów jawnej implementacji interfejsów, które zostały zastosowane do ukrycia składowych publicznego interfejsu API klasy, i to nawet w przypadkach, gdy nie występuje jawny konflikt nazw. Twórcy platformy nie są jednak przekonani co do tego, czy takie rozwiązanie faktycznie daje jakąś poprawę. Znacznie częściej zdarzają się sytuacje, w których nie mamy pełnej kontroli nad kodem, na przykład gdy używane są dwie biblioteki napisane przez firmy trzecie deklarujące interfejsy o całkowicie odmiennym znaczeniu, lecz posiadające składowe o tych samych nazwach. Nawet to nie jest jednak problemem, chyba że konieczne jest zaimplemen­ towanie obu tych interfejsów w tej samej klasie. A to zdarza się zdecydowanie rzadziej !

No dobrze, wróćmy na chwilę do naszej przykładowej klasy F i reStat i on i wyobraźmy sobie interfejs, który można by utworzyć w celu sformalizowania kontraktu rejestracji osoby przy­ chodzącej do pracy na posterunku straży pożarnej . Taki kontrakt mógłby definiować system rozliczeniowy. W naszym przypadku to klasa F i reStati on udostępnia implementację pozwalającą na rejestro­ wanie przychodzących pracowników (przy użyciu metody C l oc k l n ) , jednak kontrakt I Cl ock l n systemu rozliczeniowego jest znacznie bardziej ogólny - pozwala na zarejestrowanie dowol­ nego obiektu, jak to było możliwe w naszej początkowej implementacji: Tworzenie jednych interfejsów na bazie innych

155

(Abstrakcyj ne) klasy bazowe a i nterfejsy Bez wątpienia wprowadzenie interfejsów pozwoliło uprościć nasz model. Czy w takim razie kiedy­ kolwiek będziemy preferowali stosowanie zamiast nich abstrakcyjnych klas bazowych? Cóż, spotkaliśmy się już z przykładem, w którym zastosowanie abstrakcyjnej klasy bazowej jest dobrym rozwiązaniem - ma to miejsce, gdy istnieje jakaś dodatkowa funkcjonalność bazowa, którą chcemy przekazać do klas pochodnych, oraz jeśli w grę wchodzą składowe abstrakcyjne. Wprowa­ dzanie interfejsu tylko dla wprowadzenia składowych abstrakcyjnych byłoby niepotrzebne. Ogólnie rzecz biorąc, interfejs jest doskonałym sposobem zdefiniowania samego kontraktu, bez dostar­ czania jakiejkolwiek jego implementacji, zwłaszcza jeśli kontrakt ten ma być wykorzystywany w róż­ nych miejscach hierarchii klas. Interfejsy podlegają także ścisłym zasadom wersjonowania. Jeśli dodamy lub usuniemy z interfejsu jakąkolwiek składową, a następnie udostępnimy podzespół, to będzie to inny interfejs, który nie będzie już binarnie zgodny z kodem implementującym jego wcześniejszą wersję (choć kod wywołujący składowe za pośrednictwem interfejsu wciąż będzie działał dobrze przy założeniu, że nie usunięto czegoś, od czego jest on uzależniony) . W przypadku abstrakcyjnych klas bazowych zazwyczaj ist­ nieje możliwość dodawania składowych z zachowaniem binarnej zgodności względem wcześniej­ szego kodu (choć oczywiście dodanie nowej składowej abstrakcyjnej spowoduje utracenie zgodności z wszelkimi klasami dziedziczącymi). i nterface I C l ockin { vo i d Cl ockln (obj ect i tem) ;

Teraz możemy zaimplementować interfejs I C l o c k i n w klasie F i reS t a t i on, jak pokazano na listingu 4.27. Listing 4.27. Implementacja interfejsu IClockln cl ass Fi reStati on : I Cl ockin { Li s t < I N amedPerson> cl ockedinStaff = new Li s t < I N amedPerson> () ; publ i c vo i d Cl ockln ( I NamedPerson s taffMember) { i f ( ! cl ocked i nStaff . Conta i ns ( s t affMember) ) { cl ockedi nStaff . Add ( s t affMember) ; Consol e . Wri teli ne ( " { O } s i ę zarej estrował' . " , s t affMember . Name) ;

publ i c vo i d Rol l Cal l () { foreach ( I N amedPerson staffMember i n cl ockedinStaff) { Consol e . Wri teli ne (staffMember . Name) ;

publ i c vo i d Cl ockln (obj ect i tem) { li Hm„. Co tu zrobić?

156

Rozdział 4. Rozszerzalność i polimorfizm

Stosowana wcześniej metoda Cl oc k l n nie została zmieniona. Ponadto dodaliśmy metodę prze­ ciążoną pobierającą dowolny obiekt, która spełnia wymagania interfejsu. Ale w jaki sposób mamy zaimplementować tę metodę? Chcemy sprawdzić, czy rejestrująca się osoba jest typu I NamedPerson, a jeśli ten warunek będzie spełniony - wykonać standardowe czynności odnotowania przybycia pracownika. W przeciwnym przypadku musimy poinformować użytkownika, że nie można go zarejestrować. Innymi słowy, musimy jakoś sprawdzić typ przekazanego obiektu.

Ostateczne rozwiązan ie: sprawdzan ie typów podczas wykonywan ia programu Język C# udostępnia dwa słowa kluczowe służące do sprawdzania typu obiektu: a s oraz i s . Oto, w jaki sposób możemy ich użyć w implementacji metody C l oc k l n: publ i c vo i d Cl ockln (obj ect i tem) { i f (i tem i s INamedPerson) { C l o c k l n ( i tem as INamedPerson) ;

el se { Conso l e . Wri te l i ne ( " N i e można zarej estrować ' { O } ' . " , i tem. GetType O ) ;

Należy zwrócić uwagę na zastosowanie nazwy typu w sprawdzeniu, czy dany obiekt jest ( i s ) tego typu. Po sprawdzeniu dzięki jawnemu przekonwertowaniu referencji na typ I N amed Person przy użyciu operatora as wywoływana jest druga z przeciążonych metod Cl o c k l n . Warunek zastosowany w powyższej metodzie sprawdza, czy wskazany obiekt może być dostępny za pośrednictwem referencji określonego typu. Operator i s przegląda całą hierarchię dziedzi­ czenia obiektu (zarówno w górę, jak i w dół), próbując znaleźć wskazany typ, a jeśli mu się to uda, zwraca referencję tego typu. A co by się stało, gdybyśmy nie zawracali sobie głowy sprawdzaniem typu przy użyciu ope­ ratora i s, tylko od razu użyli operatora a s ? Na szczęście jeśli operator a s nie może znaleźć odpowiedniego typu, zwraca referencję n ul l : publ i c vo i d Cl ockln (obj ect i tem) { I NamedPerson n amedPerson = i tem as INamedPerson ; i f (namedPerson ! = nul l )

{ Cl ockln (namedPerson) ; el se { Conso l e . Wri te l i ne ( " N i e można zarej estrować ' { O } ' . " , i tern . GetType O ) ;

Ostateczne rozwiązanie: sprawdzanie typów podczas wykonywania programu

157

Testowanie typów zazwyczaj jest wykonywane właśnie w takiej postaci, gdyż jest ona nie­ znacznie szybsza od sposobu przedstawionego w poprzednim fragmencie kodu. W pierwszym z przykładów środowisko wykonawcze musi dwukrotnie przeprowadzić kosztowne spraw­ dzenie typu: raz w warunku instrukcji i f, a drugi raz, by sprawdzić, czy można dokonać kon­ wersji, czy trzeba zwrócić wartość n ul l . W drugim przykładzie to sprawdzenie wykonywane jest tylko raz - dopiero potem jego wynik jest porównywany z wartością n ul l .

Podsu mowan ie Do tej pory dowiedzieliśmy się już, jak tworzyć klasy, jak modelować relacje pomiędzy poszcze­ gólnymi instancjami tych klas przy wykorzystaniu asocjacji, kompozycji oraz agregacji, a także w jaki sposób, korzystając z dziedziczenia, można tworzyć związki pomiędzy samymi klasami. Dowiedzieliśmy się też, jak można stosować modyfikatory protected oraz p rotected i n t erna l w celu określania dostępności składowych w klasach pochodnych. Następnie poznaliśmy spo­ soby stosowania abstrakcyjnych klas i metod bądź interfejsów w celu definiowania publicznego kontraktu tworzonych klas . Na samym końcu rozdziału nauczyliśmy się samodzielnie badać hierarchię dziedziczenia i sprawdzać, czy obiekt, do którego odwołujemy się poprzez referencję klasy bazowej, jest w rzeczywistości instancją klasy pochodnej . W następnym rozdziale przedstawione zostały kolejne techniki wielokrotnego wykorzystywa­ nia tego samego kodu oraz jego rozszerzania bez użycia mechanizmu dziedziczenia .

158

I

Rozdział 4. Rozszerzalność i polimorfizm

ROZDZIAŁ 5.

Delegacje - łatwość komponowania i rozszerzalność

W dwóch poprzednich rozdziałach dowiedzieliśmy się, w jaki sposób można hermetyzować zachowania i informacje przy użyciu klas. Korzystając z pojęć asocjacji, kompozycji oraz agre­ gacji, modelowaliśmy relacje pomiędzy klasami i poznawaliśmy korzyści, jakie zapewnia nam polimorfizm oraz zastosowanie (czasami niewłaściwe) metod wirtualnych i ich domyślnego kontraktu narzucanego klasom pochodnym. W tym rozdziale także będziemy się zajmować zagadnieniami kompozycji i rozszerzalności, jednak nie z perspektywy klas - zastosujemy podejście funkcyjne. Przekonamy się, w jaki sposób możemy je wykorzystać do zaimplementowania pewnych wzorców, które wcześniej wymagały od nas porzucenia naszej jednej jedynej klasy bazowej i przesłonięcia metod wirtualnych. Co więcej, jednocześnie będziemy mogli uzyskać dodatkową korzyść, jaką są luźne powiązania pomiędzy klasami. Zacznijmy od kolejnego przykładu. Tym razem naszym celem będzie stworzenie systemu prze­ twarzającego dokumenty nadsyłane drogą elektroniczną przed ich opublikowaniem. Być może będziemy chcieli automatycznie sprawdzić pisownię dokumentu, ponownie podzielić go na strony, dokonać komputerowego tłumaczenia na jakiś inny język, by opublikować go w witry­ nie w innym kraju, bądź też wykonać jedną z wielu innych czynności, które nasi redaktorzy na pewno wymyślą w trakcie procesu tworzenia aplikacji lub nawet po jego zakończeniu. Po przeprowadzeniu pewnych analiz biznesowych zespół pracujący nad tworzeniem systemu przekazał nam klasę Document, której kod przedstawiono na listingu 5 . 1 .

Listing 5. 1 . Klasa Document publ i c seal ed cl as s Document { li Pobranie/ustawienie tekstu dokumentu publ i c s t r i ng Text { get ; set ;

li Data dokumentu publ i c DateT i me DocumentDate

159

get ; set ; publ i c s t r i ng Author { get ; set ;

Jak widać, klasa posiada trzy proste właściwości - Text, DocumentDate oraz Aut hor - i nie ma żadnych dodatkowych metod.

Czym są powiązania? Mówimy, że dwie klasy są ze sobą powiązane, jeśli zmiana jednej z nich pociąga za sobą koniecz­ ność wprowadzenia zmian w drugiej . Z przykładami takiej sytuacji spotkaliśmy się w poprzednim rozdziale. Utworzenie klasy NamedPerson zmusiło nas do wprowadzenia zmian w klasach Fi ref i ghter 4Base oraz Admi ni strator. Oznacza to, że klasy Fi refi ghterBase oraz Admi ni strator były powiązane z klasą NamedPerson. Oczywiście każda klasa lub metoda odwołująca się do innej klasy lub metody jest z nią powiązana nie da się tego uniknąć (zresztą jest to zamierzone i pożądane). Jednak by uprościć testowanie sys­ temu oraz zapewnić jego większą niezawodność, należy starać się minimalizować ilość innych typów, z którymi nasze klasy i metody są powiązane, oraz minimalizować liczbę powiązań pomiędzy dowol­ nymi dwoma typami. Dzięki temu zmiany wprowadzane w klasie będą miały możliwie mały wpływ na pozostałe elementy tworzonego programu. Oprócz tego staramy się logicznie organizować klasy w tak zwanych warstwach, tak by klasy silniej ze sobą powiązane znalazły się w tej samej warstwie oraz by pomiędzy poszczególnymi warstwami występowała minimalna liczba ściśle kontrolowanych powiązań. Jednym z elementów takiego podejścia jest próba sprawienia, by większość z występujących powiązań była jednokierunkowa klasy wchodzące w skład niższej warstwy nie powinny być uzależnione od klas należących do warstwy wyższej . Dzięki temu można w dodatkowy sposób ograniczyć (i zrozumieć) propagację zmian w tworzonym systemie. Warstwy można by porównać z zaporami sieciowymi blokującymi wpływ wprowadzanych zmian. Jak to zazwyczaj jest z projektowaniem oprogramowania, nie są to żadne żelazne reguły ani ogra­ niczenia wymuszane przez używaną platformę lub język programowania - są to powszechnie stosowane praktyki wspierane zarówno przez platformę, jak i przez język.

Teraz chcemy mieć możliwość przetworzenia dokumentu. W najprostszym przypadku chcemy móc: sprawdzić pisownię ( Spel l c h e c k ) , ponownie dokonać podziału dokumentu na strony ( Repagi nate) oraz dokonać jego tłumaczenia (Tran s l ate; na przykład na francuski) . Ponieważ nie możemy zmienić klasy Document, wszystkie te metody zaimplementujemy w jednej statycznej klasie pomocniczej zgodnie z informacjami podanymi w rozdziale 3 . Kod tej klasy został przedstawiony na listingu 5.2, choć zaprezentowane tam implementacje są raczej tymczasowe chodzi nam o to, by pokazać, w jaki sposób należy określać strukturę kodu, a zaimplemen­ towanie prawdziwego narzędzia do sprawdzania pisowni mogłoby skutecznie rozproszyć Czytelnika .

160

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Listing 5.2 . Przykładowe metody do przetwarzania dokumentu s t at i c cl ass Document Processes publ i c s t at i c v o i d Spel l check (Document doc) { Consol e . Wr i teli ne ( " Sprawdzono p i sown i ę w do kumenc i e . " ) ; publ i c s t at i c v o i d Repag i nate (Document doc) { Consol e . Wr i teli ne ( " Do konano podz i ał u do kumentu na s t rony . " ) ; publ i c s t at i c v o i d Tran s l ateintoFrench (Document doc) { Consol e . Wr i teli ne ( " Document tradu i t 1 • " ) ;

li

„.

Dysponując taką klasą możemy już stworzyć prosty mechanizm przetwarzania dokumentów, który będzie je tłumaczył, sprawdzał ich pisownię, a następnie dokonywał ich podziału na strony (patrz listing 5.3) .

Listing 5.3. Przetwarzanie dokumentu s t at i c cl ass Document Processor publ i c s t at i c v o i d Proces s (Document doc) { Document Processes . Trans l ate i n t o French (doc) ; Document Processes . Spel l check (doc) ; Document Processes . Repag i nate (doc) ;

Metodę tę możemy wykorzystać w naszej metodzie Ma i n, by przetworzyć kilka dokumentów (co pokazano na listingu 5 .4) .

Listing 5.4. Program sprawdzający klasy obsługujące przetwarzanie dokumentów cl ass Program { s t at i c vo i d Ma i n (stri ng O args) { Document docl = new Document Author = " J an Kowal s ki " , DocumentDate = new DateT i me (2000 , O l , O l ) , Text = " Czy przybyłem za wcześn i e? " }; Document doc2 = new Document { Author = "Wi e s ł aw Zators k i " , DocumentDate = new DateT i me (200 1 , O l , O l ) , 1 Co po francusku oznacza „Dokument został przetłumaczony''.

-

przyp. tłum.

Kompozycja funkcyjna wykorzystująca delegacje

161

Text = "Wi erz c i e mi ! Nadchodz i nowe mi l en i um . " }; Consol e . Wr i teli ne ( " Przetwarzan i e do kumentu nr 1 . " ) ; Document Processor . Proces s (doc l ) ; Consol e . Wr i teli ne () ; Consol e . Wr i teli ne ( " Przetwarzan i e do kumentu nr 2 . " ) ; Document Process o r . Process (doc2) ; Consol e . ReadKey () ;

Po skompilowaniu i uruchomieniu powyższy program wygeneruje następujące wyniki: Przetwarzan i e do kumentu nr 1 . Document tradu i t . Sprawdzono p i sown i ę w do kumen c i e . Do konano podz i ał u do kumentu n a s t rony . Przetwarzan i e do kumentu nr 2 . Document tradu i t . Sprawdzono p i sown i ę w do kumen c i e . Do konano podz i ał u do kumentu n a s t rony .

A zatem w (statycznej) klasie Document Proces sor umieściliśmy konkretną grupę instrukcji przetwa­ rzania wykonywanych w ściśle określonej kolejności, dzięki czemu możemy z nich wielokrot­ nie korzystać w różnych aplikacjach potrzebujących solidnego i godnego zaufania narzędzia wykonującego proces tłumaczenia dokumentu na język francuski. Jak na razie wszystko powinno być znane i zrozumiałe. A w jaki sposób można by było skorzystać z innego zbioru operacji? Na przykład co w przy­ padku, gdybyśmy chcieli pozostawić dokument w oryginalnym języku, lecz sprawdzić jego pisownię i ponownie podzielić go na strony? Oczywiście można by utworzyć kolejną klasę podobną do Document Proces sor i w niej umieścić odpowiednie wywołania: s t at i c cl ass Document ProcessorStandard publ i c s t at i c v o i d Proces s (Document doc) { Document Processes . Spel l check (doc) ; Document Processes . Repag i nate (doc) ;

I ponownie moglibyśmy umieścić odwołania do tego procesora w naszej metodzie Ma i n: Consol e . Wri teli ne ( ) ; Consol e . Wri teli ne ( " Przetwarzan i e do kumentu nr 1 ( s t andardowe) . " ) ; Document Proces sorS t andard . Process (doc l ) ; Consol e . Wri teli ne ( ) ; Consol e . Wri teli ne ( " Przetwarzan i e do kumentu nr 2 ( s t andardowe) . " ) ; Document Proces sorS t andard . Process (doc2) ;

W zasadzie w takim rozwiązaniu nie można się doszukać niczego niewłaściwego: działa dosko­ nale, a przy okazji udało się utworzyć elegancki projekt inteligentnie hermetyzujący operacje przetwarzania dokumentu.

162

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Można zauważyć, że każda klasa Document Proces sor jest powiązana z klasą Document oraz z każdą z wywoływanych metod klasy Documen t Proces s e s . A zatem nasz klient jest powiązany z klasą Document oraz każdą używaną klasą Document Proces sor. Jeśli ponownie zajrzymy do przedstawionej wcześniej dokumentacji, okaże się, że najpraw­ dopodobniej będziemy musieli zaimplementować wiele różnych funkcji modyfikujących doku­ ment w procesie jego przetwarzania . Będą one stosowane lub nie zależnie od typu przetwa­ rzanego dokumentu, innych systemów, z których być może będziemy musieli korzystać, oraz aktualnie wybranego procesu obróbki . Zamiast implementować te wszystkie procesy w kolejnych klasach procesorów (i tworzyć coraz to nowsze powiązania pomiędzy nimi i kolejnymi możliwościami klasy Doc umen t Proc e s s e s ) , znacznie lepiej byłoby, gdybyśmy mogli przekazać te obowiązki innym programistom z zespołu produkcyjnego. Mogliby oni określić porządkowy zbiór procesów (jakiegoś typu) i przekazać go do jednej jedynej klasy Doc umen t Processor, która następnie by je wykonała. Dzięki takiemu rozwiązaniu my moglibyśmy się skoncentrować na zapewnieniu jak najwięk­ szej efektywności i niezawodności mechanizmu przetwarzającego, z kolei zespół produkcyjny mógłby się zająć opracowywaniem sekwencji procesów (tworzonych bądź to przez nas, bądź to przez nich samych, innych podwykonawców lub przez kogokolwiek innego) bez konieczności ciągłego proszenia nas o wprowadzanie najnowszych aktualizacji . Rysunek 5.1 przedstawia diagram tego rozwiązania. P roceso r d oku meni tów Dokument



„ • • • „ • • • • • „ ••

ę ę

ę

Qo ...

Proces

�� Ł

Proces

) )

)„.„„..... „

•••

Dokument

Rysunek 5.1 . Architektura mechanizmu przetwarzania dokumentów Dokument jest przekazywany do procesora, który z kolei wykonuje ściśle określoną sekwencję procesów, przekazując dokument do każdego z nich. W efekcie wykonania tych wszystkich operacji uzyskujemy kolejny dokument. No dobrze, utwórzmy zatem klasę Documen t Pro ces sor, która zapewni nam takie możliwości (przedstawiono ją na listingu 5.5) .

Kompozycja funkcyjna wykorzystująca delegacje

I

163

Listing 5.5. Elastyczny procesor dokumentów cl ass Document Processor pri vate readon l y Li s t processes new Li s t () ; publ i c Li s t Proces ses { get { return proces ses ;

publ i c vo i d Process (Document doc) { foreach (Document Process process i n Processes) { proces s . Proces s (doc) ;

Nasz procesor dokumentów zawiera listę (Li st) obiektów Document Process (hipotetycznego typu, którego jeszcze nie utworzyliśmy) . L i st jest kolekcją uporządkowaną, co oznacza, że element, który został do niej dodany (przy użyciu metody Add) i trafił na miejsce o indeksie O, pozostanie na tym samym miejscu i będzie także pierwszym elementem pobieranym podczas przetwa­ rzania całej zawartości kolekcji. Tak więc nasza metoda Process może przechodzić przez całą kolekcję obiektów Document Process i dla każdego z nich wywoływać pewną metodę Process, która wykona odpowiednie operacje na dokumencie. Ale jaki powinien być typ owych hipotetycznych obiektów Document Process? Cóż, znamy już rozwiązanie, które można by tu zastosować - klasa Document Proces s mogłaby być abstrakcyjną klasą bazową deklarującą abstrakcyjną metodę Process: abstract cl ass Document Proces s publ i c abs tract voi d Process (Document doc) ;

W tym przypadku musimy utworzyć klasę pochodną dla każdej operacji przetwarzania, jak to pokazano na listingu 5 .6.

Listing 5 . 6 . Implementacja abstrakcyjnej klasy DocumentProcess cl ass Spel l chec kProcess : DocumentProces s { publ i c overri de voi d Process (Document doc)

{ Document Processes . Spel l check (doc) ;

cl ass Repag i nateProcess : DocumentProces s publ i c overri de voi d Process (Document doc)

{ Document Processes . Repag i nate (doc) ;

164

I

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

cl ass Trans l ateintoFrench Process : Document Process publ i c overri de voi d Process (Document doc)

{ Document Processes . Trans l ate i n t o French (doc) ;

Teraz możemy skonfigurować procesor, dodając do listy odpowiednie obiekty procesów (patrz listing 5 .7) .

Listing 5.7. Konfigurowanie procesora dokumentów poprzez dodawanie procesów s t at i c Document Proces sor Confi gure () { Document Proces sor re = new Document Proces s o r () ; rc . Processes .Add (new Tran s l atei nto FrenchProces s () ) ; rc . Proces ses . Add (new Spel l checkProces s () ) ; rc . Proces ses . Add (new Repag i nateProces s () ) ; return re ;

Czy Czytelnik zwrócił uwagę na to, że procesy są dodawane do procesora w takiej samej kolejności, w jakiej wcześniej były wykonywane wywołania funkcji? Używane obiekty procesów są pod względem logicznym podobne do wywołań funkcji, a ich kolejność jest podobna do kolejności operacji wykonywanych przez program. Jedyna różnica polega na tym, że tworzymy je w trakcie wykonywania aplikacji, a nie podczas kompilacji . Tej metody konfiguracyjnej możemy użyć w kodzie naszego programu, by przetworzyć doku­ ment za pomocą procesora (patrz listing 5 .8) :

Listing 5.8. Stosowanie dynamicznie skonfigurowanego procesora s t at i c vo i d Mai n (s tri ng O arg s ) { Document docl = new Document Author = " J an Kowal s k i " , DocumentDate = new DateT i me (200 0 , O l , O l ) , Text = " Czy przybyłem za wcześ n i e ? " }; Document doc2 = new Document Author = " W i e s ł aw Zators ki " , DocumentDate = new DateT i me (200 1 , O l , O l ) , Text = "Wi erze i e mi ! Nadchod z i nowe mi l en i urn . " }; DocumentProcessor processor = Confi gure () ;

Consol e . Wri teli ne ( " Przetwarzan i e do kumentu nr 1 . " ) ; proces sor . Proces s (doc l ) ; Consol e . Wri teli ne () ; Consol e . Wri teli ne ( " Przetwarzan i e do kumentu nr 2 . " ) ; proces sor . Proces s (doc2) ; Consol e . ReadKey ( ) ;

Kompozycja funkcyjna wykorzystująca delegacje

165

Po skompilowaniu i wykonaniu powyższego przykładu uzyskamy następujące rezultaty: Przetwarzan i e do kumentu nr 1 . Document tradu i t . Sprawdzono p i sown i ę w do kumen c i e . Do konano podz i ał u do kumentu n a s t rony . Przetwarzan i e do kumentu nr 2 . Document tradu i t . Sprawdzono p i sown i ę w do kumen c i e . Do konano podz i ał u do kumentu n a s t rony .

Taki wzorzec, polegający na hermetyzacji metody w obiekcie i (lub) procesu w sekwencji obiektów, jest bardzo często spotykany w projektowaniu obiektowym. Zastosowane rozwiązanie jest dobre, gdyż dzięki niemu klasa Doc umen t Proces sor jest powiąza­ na wyłącznie z klasą Document oraz z abstrakcyjną klasą bazową, której dokument używa jako kontraktu dla poszczególnych procesów. Klasa ta nie jest już powiązana z żadną z klas repre­ zentujących poszczególne procesy - te mogą się dowolnie zmieniać, nie wymagając wprowa­ dzania jakichkolwiek modyfikacji w samym procesorze, gdyż implementują kontrakt określany przez abstrakcyjną klasę bazową. W końcu, określenie sekwencji przetwarzania (czyli „programu" dla obiektu Document Proces sor) należy teraz do obowiązków aplikacji klienckiej, a nie biblioteki procesora. Dzięki temu nasz niezależny zespół produkcyjny może opracowywać swoje własne sekwencje (a nawet zupeł­ nie nowe procesy) bez konieczności korzystania z pomocy głównego zespołu programistów i w ten sposób zmieniać działanie procesora dokumentów. W rzeczywistości jedyną rzeczą, którą można by uznać za bolączkę tego rozwiązania, jest konieczność definiowania nowej klasy za każdym razem, gdy chcemy jedynie wywołać metodę. Czy nie byłoby łatwiej odwołać się do tej metody bezpośrednio? C# udostępnia narzędzie, które na to pozwala. Są nim delegacje (ang. delegate) .

Kompozycja funkcyjna wykorzystująca delegacje Właśnie napisaliśmy kod, który ukrywa wywołanie metody wewnątrz obiektu. Faktyczne wywołanie jest dodatkowo ukryte wewnątrz kolejnej metody o znanej sygnaturze. Delegację można sobie wyobrazić jako rozwiązanie bardzo podobnego problemu: jest to obiekt zawierający wywołanie metody innego obiektu (lub klasy) . Jednak w odróżnieniu od naszych klas Document Process, w których docelowe operacje są podane na stałe w formie metod wirtualnych przesłaniających metody klasy bazowej, delegacje pozwalają odwoływać się do konkretnych funkcji (konkretnej klasy lub obiektu) w trakcie działania pro­ gramu, a następnie je wykonywać. A zatem można sobie wyobrazić, że analogicznie do zmiennych, które mogą zawierać refe­ rencje do obiektów, delegacje zawierają referencje do funkcji (patrz rysunek 5 .2) . Zanim przedstawimy konkretną składnię używaną w C# do tworzenia i korzystania z delegacji, chcielibyśmy pokazać, że nie ma w nich nic mistycznego - w rzeczywistości w bibliotece klas .NET Framework istnieje klasa Del ega t e, która zawiera wszystkie niezbędne możliwości .

166

I

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Zmie n na (typu MyObje " J aki i tekst do dz i enni ka . . . " ) ;

li

„.

W powyższym kodzie wyrażenie lambda zostało wykorzystane do utworzenia delegacji posiadającej parametr typu Document o nazwie doc i zwracającej łańcuch znaków ( stri ng) . Nieco później w bardzo podobny sposób zapewnimy możliwość generowania znacznie bardziej przy­ datnego komunikatu. Warto jeszcze raz zwrócić uwagę na to, jak zwarte są wyrażenia lambda, oraz na fakt, że kompilator automatycznie określa typy parametrów. Czy Czytelnik pamięta jeszcze, jak wiele pracy wymagało uzyskanie tych samych efektów wcześniej, gdy obracaliśmy się w świecie abstrakcyjnych klas bazowych? Spróbujmy zatem skompilować i uruchomić nową wersję programu. Uzyskamy następujące wyniki: Przetwarzan i e do kumentu nr 1 . Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . J a k i ś tekst do d z i enn i ka . . . Przetwarzan i e do kumentu nr 2 . Document tradu i t .

Delegacje we właściwościach

181

J a k i ś tekst do d z i enn i ka . . . Sprawdzono p i sown i ę w do kumen c i e . J a k i ś tekst do d z i enn i ka . . . Do konano podz i ał u do kumentu na s t rony . J a k i ś tekst do dz i enn i ka . . . Wyróżn i ono s ł owo ' mi l en i um ' . J a k i ś tekst do d z i enn i ka . . .

To przykład delegacji do funkcji, która zwraca coś innego niż wartość voi d lub wartość typu bool . Przypuszczalnie Czytelnik już odgadł, że .NET Framework udostępnia ogólny typ, dzięki któremu nie trzeba będzie deklarować takich delegacji samodzielnie .

Ogól ne delegacje do fun kcj i Biblioteka .NET Framework udostępnia ogólną klasę F u n c, której nazwę można rozumieć jako „funkcja pobierająca parametr typu T i zwracająca wartość typu T Resul t " . Podobnie jak w przypadku typów Pred i cate oraz Act i on, także i teraz pierwszy parametr typu określa typ pierwszego parametru funkcji, do której odwołuje się delegacja . Jednak w odróżnieniu od dwóch wymienionych wcześniej typów w tym przypadku konieczne jest także określenie typu wartości wynikowej; do tego cel u służy drugi parametr typu T Resul t .

-

'

Biblioteka .NET Framework zawiera całą grupę typów Func pobierających jeden, dwa, trzy i więcej parametrów, analogicznie jak w przypadku typu Acti on. We wcześniejszych wersjach platformy .NET maksymalną dostępną liczbą parametrów były cztery w .NET 4 jest ich aż 16.

. '

A zatem możemy zastąpić nasz własny typ delegacji typem Fun c . Usuńmy więc deklarację

delegacji: del egate stri ng LogText Prov i de r ( Document doc) ;

i zaktualizujmy deklarację właściwości: publ i c Func LogTextProvi der { get ; set ;

Tę nową wersję programu można zbudować i uruchomić bez wprowadzania jakichkolwiek zmian w kodzie klienta, gdyż nowa deklaracja właściwości wciąż oczekuje delegacji do funkcji o takiej samej sygnaturze. Wyniki wygenerowane przez program będą identyczne: Przetwarzan i e do kumentu nr 1 . Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . J a k i ś tekst do d z i enn i ka . . . Przetwarzan i e do kumentu nr 2 . Document tradu i t . J a k i ś tekst do d z i enn i ka . . . Sprawdzono p i sown i ę w do kumen c i e . J a k i ś tekst do d z i enn i ka . . . Do konano podz i ał u do kumentu na s t rony .

182

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

J a k i ś tekst do d z i enn i ka . . . Wyróżn i ono s ł owo ' mi l en i um ' . J a k i ś tekst do d z i enn i ka . . .

No dobrze, wróćmy zatem na chwilę i przyjrzyjmy się funkcji rejestrującej komunikaty. Jak już wcześniej zaznaczyliśmy, aktualnie nie jest ona zbyt użyteczna. Można by ją jednak poprawić, rejestrując po zakończeniu każdego etapu procesu nazwę przetwarzanego pliku. W ten sposób ułatwilibyśmy zespołowi produkcyjnemu diagnozowanie problemów. Listing 5 .19 przedstawia wersję metody Ma i n, w której wprowadzono niezbędne zmiany.

Listing 5.19. Rozbudowana fankcja do rejestrowania komunikatów w dzienniku s t at i c vo i d Mai n (s tri ng O arg s ) { Document docl = new Document Author = " J an Kowal s k i " , DocumentDate = new DateT i me (200 0 , O l , O l ) , Text = " Czy przybyłem za wcześ n i e ? " }; Document doc2 = new Document Author = " W i e s ł aw Zators ki " , DocumentDate = new DateT i me (200 1 , O l , O l ) , Text = "Wi erzci e mi ! Nadchod z i nowe mi l en i urn . " };

Document doc3 = new Document { Author = " J an Kowal ski " , DocumentDate = new DateT i me (200 2 , 0 1 , Ol) , Text = " In ny rok , i nny dokument . " }; stri ng documentBei ngProcessed = n ul l ;

Document Proces sor processor = Confi gure () ; proces sor . LogTextProvi der = (doc => documentBei ngProcessed) ; documentBei ngProcessed = " (Dokument 1 . ) " ;

process o r . Proces s (doc l ) ; Consol e . Wri tel i ne () ; documentBei ngProcessed = " (Dokument 2 . ) " ;

process o r . Proces s (doc2) ; Consol e . Wri tel i ne () ; documentBei ngProcessed = " (Dokument 3 . ) " ;

process o r . Proces s (doc3) ; Consol e . ReadKey () ;

Do zbioru dokumentów dodaliśmy trzeci, tak by było ich więcej do przetwarzania. Następnie zdefiniowaliśmy zmienną lokalną o nazwie document Bei ng Processed. Modyfikując przetwarzane dokumenty, aktualizujemy także wartość tej zmiennej, by odpowiadała ona aktualnemu stanowi procesu. A w jaki sposób można przekazać te informacje do wyrażenia lambda? W bardzo prosty wystarczy ich użyć!

Ogólne delegacje do funkcji

I

183

Skompilujmy nasz program i zobaczmy, jakie wyniki wygeneruje . Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . (Do kument 1 . ) Document tradu i t . (Do kument 2 . ) Sprawdzono p i sown i ę w do kumen c i e . (Do kument 2 . ) Do konano podz i af u do kumentu n a s t rony . (Do kument 2 . ) Wyróżn i ono s f owo ' mi l en i um ' . (Do kument 2 . ) Document tradu i t . (Do kument 3 . ) Sprawdzono p i sown i ę w do kumen c i e . (Do kument 3 . ) Do konano podz i af u do kumentu n a s t rony . (Do kument 3 . )

W powyższym kodzie wykorzystaliśmy fakt, że metody anonimowe mają dostęp do zmiennych zadeklarowanych w zakresie nadrzędnym, a nie tylko do tych zadeklarowanych wewnątrz nich samych. Więcej informacji na ten temat można znaleźć w ramce zamieszczonej poniżej .

Domknięcia Ogólnie rzecz biorąc, konkretną instancję funkcji oraz zbiór zmiennych, na których ona operuje, nazywamy domknięciem (ang. closure) . W czystych językach funkcyjnych domknięcie jest zazwyczaj implementowane poprzez skopio­ wanie wartości zmiennych w momencie jego tworzenia oraz pobranie referencji do odpowiedniej funkcji; wartości te pozostają następnie niezmienne. W języku C# zostało wykorzystane podobne rozwiązanie, j ednak pozwala on na modyfikowanie zmiennych nawet po utworzeniu domknięcia. Jak pokazaliśmy w tym rozdziale, tę cechę C# można wykorzystać do własnych celów, jednak należy przy tym doskonale znać zakresy zmiennych używanych w domknięciach i doskonale nimi zarządzać, by uniknąć dziwacznych efektów ubocznych.

Dowiedzieliśmy się już, jak odczytywać wartości zmiennych zadeklarowanych w zakresie zewnętrznym domknięcia. A co z określaniem ich wartości? To także jest możliwe. Utwórzmy zatem licznik procesów, którego wartość będzie inkrementowana za każdym razem, gdy wykonamy kolejny proces, i dodajmy go do funkcji rejestrującej (patrz listing 5.20) .

Listing 5.20. Modyfikacja zmiennych zewnętrznych w metodzie zagnieżdżonej s t at i c vo i d Mai n (s tri ng O arg s ) { li (konfiguracja) „.

Document Proces sor processor i nt processCount = O ;

=

s t r i ng documentBei ng Processed

Confi gure () ; =

" (Brak zestawu do kumentów) " ;

processor . LogTextProvi der = (doc =>

184

{

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

proces s Count += 1 ; return documentBei ngProcessed; }) ;

documentBei ngProces sed = " (Do kument 1 . ) " ; process o r . Proces s (doc l ) ; Consol e . Wri tel i ne () ; documentBei ngProces sed = " (Do kument 2 . ) " ; process o r . Proces s (doc2) ; Consol e . Wri tel i ne () ; documentBei ngProces sed = " (Do kument 3 . ) " ; process o r . Proces s (doc3) ; Consol e . Wri tel i ne () ; Consol e . Wri teli ne (" Li czba wykonanych procesów : " + proces s Count + " . " ) ;

Consol e . ReadKey () ;

Wewnątrz metody Mai n dodaliśmy nową zmienną proces s Count i przypisaliśmy jej wartość O . Zmieniliśmy także używane wyrażenie lambda, nadając mu postać instrukcji z nawiasami klamrowymi, dzięki czemu możemy wewnątrz niego umieścić więcej kodu. Aktualnie oprócz zwracania nazwy przetwarzanego dokumentu nasze wyrażenie lambda inkrementuje wartość zmiennej proces s Coun t . W końcu, na zakończenie przetwarzania, możemy wyświetlić informację o liczbie wykonanych procesów. A zatem po wprowadzeniu powyższych zmian wyniki generowane przez program będą miały następującą postać: Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . (Do kument 1 . ) Document tradu i t . (Do kument 2 . ) Sprawdzono p i sown i ę w do kumen c i e . (Do kument 2 . ) Do konano podz i ał u do kumentu n a s t rony . (Do kument 2 . ) Wyróżn i ono s ł owo ' mi l en i um ' . (Do kument 2 . ) Document tradu i t . (Do kument 3 . ) Sprawdzono p i sown i ę w do kumen c i e . (Do kument 3 . ) Do konano podz i ał u do kumentu n a s t rony . (Do kument 3 . ) (Do kument 3 . ) Li czba wykonanych procesów : 9 .

Nasz zespół produkcyjny bardzo się cieszy z tych wszystkich zmian, jednak ma dodatkowe wymagania . Okazuje się, że jeden z jego podzespołów pracuje nad komponentem diagno­ stycznym, który będzie mierzył czas wykonywania niektórych procesów, podczas gdy inny podzespół implementuje obsługę monitora, który będzie na bieżąco prezentował informacje o wszystkich procesach wykonywanych w systemie. Dlatego członkowie zespołu produkcyj­ nego chcieliby wiedzieć, kiedy proces zaczyna być wykonywany oraz kiedy zostaje zakoń­ czony, dzięki czemu byłoby wiadomo, kiedy mają być wykonywane kody pozostałych kom­ ponentów.

Ogólne delegacje do funkcji

I

185

Pierwszą myślą, jaka mogłaby nam przyjść do głowy, mogłoby być utworzenie dwóch dodat­ kowych funkcji zwrotnych: jednej dla rozpoczęcia przetwarzania, drugiej dla jego zakończenia ... Jednak to nie do końca rozwiązałoby problem zespołu produkcyjnego - dwa pracujące w jego ramach podzespoły chcą mieć możliwość wykonywania swoich kodów niezależnie do siebie. Potrzebne jest nam zatem rozwiązanie, które pozwoli poinformować o zajściu pewnego zdarze­ nia kilku klientów. Platforma .NET Framework dostarcza taki rozwiązanie - są nim zdarzenia.

I nformowan ie kl ientów za pomocą zdarzeń Zdarzenie (ang. event) jest zgłaszane (ewentualnie wysyłane) przez wydawcę (lub nadawcę), kiedy stanie się coś interesującego (na przykład gdy zostanie wykonana jakaś czynność lub ulegnie zmianie wartość jakiejś właściwości) . Klienci zgłaszają chęć odbierania zdarzeń (albo je sub­ skrybują), dostarczając odpowiednią delegację; nieco przypomina to stosowane wcześniej metody zwrotne. Metoda, do której odwołuje się delegacja, nosi nazwę procedury obsługi zdarzenia. Naj­ ciekawsze w tym rozwiązaniu jest to, że więcej niż jeden klient może subskrybować to samo zdarzenie . Poniżej przedstawiony został przykład dwóch zdarzeń, które dodaliśmy do klasy Doc ument "+ Processor, by wyjść naprzeciw potrzebom zespołu produkcyjnego. cl ass Document Processor { publ i c event EventHandl er Process i ng; publ i c event EventHandl er Processed;

li

„.

Należy zwrócić uwagę na zastosowanie słowa kluczowego event, które informuje, że dalej nastąpi deklaracja zdarzenia. Następnie podawany jest typ delegacji zdarzenia ( EventHandl er) oraz nazwa zdarzenia (zapisywana zgodnie z konwencją PascalCasing) . Jak widać, deklaracja zdarzenia bardzo przypomina deklarację pola publicznego typu EventHandl er z dodanym sło­ wem kluczowym even t . A jak wygląda delegacja EventHandl er? .NET Framework definiuje j ą następująco: del egate vo i d Event Handl er (obj ect sender , EventArgs e) ;

Należy zauważyć, że posiada ona dwa parametry. Pierwszy z nich jest referencją do wydawcy zdarzenia, dzięki której jego odbiorca może się zorientować, kto zdarzenie opublikował. Nato­ miast drugim parametrem są pewne dane skojarzone ze zdarzeniem. Klasa EventArgs została zdefiniowana w bibliotece klas .NET i jest używana w zdarzeniach, które nie potrzebują żad­ nych dodatkowych informacji. Już niebawem zobaczymy, jak można ją zmienić. ' ,' . ,.,.

._,..�; -

,

.ii. ,

Niemal wszystkie zdarzenia są zgodne z tym dwuargumentowym wzorcem. Z technicznego punktu widzenia nie musi tak być - w zdarzeniu można bowiem zastosować delegację dowolnego typu - jednak w praktyce ten wzorzec jest stosowany niemal zawsze.

A zatem w jaki sposób można zgłosić zdarzenie? Cóż . . . tak naprawdę przypomina ono delegację, można więc zastosować składnię wywołania delegacji taką jak użyta w metodach On Proces s i ng oraz on Processed przedstawionych na listingu 5 .21 .

186

I

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Listing 5.2 1 . Zgłaszanie zdarzeri publ i c vo i d Proces s (Document doc) { On Processi ng (EventArgs . Empty) ;

li Najpierw przeprowadzamy szybkq weryfikację dokumentu. foreach (Act i onCheckPa i r proces s i n processes) { i f (proces s . Qu i c kCheck ! = n u l l && ! proces s . Qu i c kChe c k (doc) ) { Consol e . Wri teli ne ( " Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . " ) ; i f ( LogText Prov i der ! = n u l l ) { Consol e . Wri te l i ne ( LogText Prov i der (doc) ) ; } On Proces sed ( EventArgs . Empty) ;

return ;

li Teraz wykonujemy akcję. foreach (Act i onCheckPa i r proces s i n processes) { proces s . Act i on (doc) ; i f ( LogText Prov i der ! = n u l l ) { Consol e . Wri teli ne ( LogTextProv i der (doc) ) ; } On Proces sed ( EventArgs . Empty) ;

pri vate voi d On Processi ng ( EventArgs e)

{

i f ( Processi ng ! = nul l )

{

Process i ng (thi s , e) ;

} } pri vate voi d OnPro cessed ( EventArgs e)

{

i f ( Processed ! = nul l )

{

Processed (thi s , e) ;

} }

Należy zwrócić uwagę na to, że sprawdzamy, czy delegacja jest różna od n u l l , a jej wywołania zostały zapisane w funkcjach o nazwach OnXXX. Choć takie rozwiązanie nie jest konieczne, to jednak jest powszechną praktyką. '

.' '

'

Jeśli nasze klasy projektujemy jako bazowe, to zazwyczaj będą one deklarowane jako chronione klasy wirtualne (protected v i rtual ) . Dzięki temu klasy pochodne mogą przesłaniać metody zgłaszające zdarzenia, zamiast rejestrować się jako ich odbiorcy. Takie rozwiązanie jest bardziej efektywne niż przechodzenie całego procesu obsługi zdarzenia i pozwala nam (ewentualnie) odmówić jego zgłoszenia, co łatwo zrobić, rezygnując z wywołania metody zaimplementowanej w klasie bazowej . Koniecznie należy jednak umieścić w dokumentacji informację, czy klasy pochodne mogą nie wywoływać implementacji metody z klasy bazowej !

Informowanie kl ientów za pomocą zdarzeń

187

Teraz musimy zasubskrybować interesujące nas zdarzenia . Utwórzmy zatem klasy, które będą udawały czynności, jakie ma wykonywać zespół produkcyjny (patrz listing 5 .22) .

Listing 5.22 . Rejestracja oraz rezygnacja z subskrypcji zdarzeń cl ass Produc t i onDeptTool l { publ i c vo i d Subscri be (DocumentProcessor proces sor) { processor . Process i ng += processor_Processi ng; processor . Processed += processor_Processed;

publ i c vo i d Unsubscri be (Document Processor processor) { processor . Process i ng - = processor_Processi ng; processor . Processed - = processor_Processed;

voi d processor- Processi ng (obj ect sender , EventArgs e)

{

Con sol e . Wri teli ne ( " Narzędzi e 1 . - zarej estrowano przetwarzani e . " ) ;

}

vo i d proces sor- Processed (obj ect sende r , EventArgs e) { Consol e . Wr i teli ne ( " Narzędz i e 1 . - zarej es t rowano zakończen i e przetwarzan i a . " ) ;

cl ass Produc t i onDeptTool 2 publ i c vo i d Subscri be (DocumentProcessor proces sor) { proces sor. Proces s i ng += (sende r , e) => Consol e . Wri teli ne ( " Narzędz i e 2 . - zarej estrowano przetwarzan i e . " ) ; processor. Processed += ( sende r , e) => Consol e . Wri teli ne ( " Narzędz i e 2 . - zarej estrowano z a kończen i e przetwarzan i a . " ) ;

Aby zasubskrybować zdarzenie, należy użyć operatora += wraz z odpowiednią delegacją. Jak widać w kodzie metody Producti onDeptTool 1 . Subs cri be, zastosowaliśmy w tym celu standardową składnię delegacji, jednak jak pokazuje metoda Product i onDeptT oo 1 2 . Subs cri be, można także sko­ rzystać z wyrażenia lambda . ••

• .·

._,..�;

.

L-------11.J"' '

Oczywiście czynności związane z rejestrowaniem procedur obsługi zdarzeń nie muszą być wykonywane w metodach o nazwie Subscri be - można je wykonywać w dowolnym miejscu kodu.

Jeśli zdarzenie przestało nas już interesować (z jakichkolwiek powodów), można zrezygnować z jego subskrypcji, używając operatora -= oraz delegacji do tej samej metody (kod, który to robi, można znaleźć w metodzie Prod u c t i onDeptTool 1 . U n s u b s cri be) . Zgłaszając subskrypcję zdarzenia, subskrybent niejawnie przechowuje referencję do wydawcy. Oznacza to, że mechanizm odzyskiwania pamięci nie będzie w stanie usunąć z niej obiektu

188

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

wydawcy zdarzenia, jeśli wciąż będzie on zawierał referencję do subskrybenta. Z tego powodu warto zapewnić możliwość usuwania subskrypcji zdarzeń, które nie są już aktywnie używane, by uniknąć niepotrzebnego przechowywania zależności pomiędzy obiektami. Dodajmy zatem do naszej metody Mai n kod, który będzie korzystał z dwóch nowych narzędzi (patrz listing 5.23) .

Listing 5.23. Zaktualizowana metoda Main s t at i c vo i d Mai n (s tri ng O arg s ) { li „.

Producti on DeptTool l tool l = new Producti onDeptTool l () ; tool l . Subs cri be (processor) ; Producti on DeptTool 2 tool 2 = new Producti onDeptTool 2 () ; tool 2 . Subs cri be (processor) ;

documentBei ngProces sed

li

=

" (Do kument 1 . ) " ;

„.

Consol e . ReadKey () ;

Po skompilowaniu i uruchomieniu nowa wersja programu wygeneruje następujące wyniki: Narzędz i e 1 . - zarej estrowano przetwarzani e . Narzędz i e 2 . - zarej estrowano przetwarzani e .

Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . (Do kument 1 . ) Narzędz i e 1 . - zarej estrowano zakończen i e przetwarzani a . Narzędz i e 2 . - zarej estrowano zakończen i e przetwarzani a . Narzędz i e 1 . - zarej estrowano przetwarzani e . Narzędz i e 2 . - zarej estrowano przetwarzani e .

Document tradu i t . (Do kument 2 . ) Sprawdzono p i sown i ę w do kumen c i e . (Do kument 2 . ) Do konano podz i ał u do kumentu n a s t rony . (Do kument 2 . ) Wyróżn i ono s ł owo ' mi l en i um ' . (Do kument 2 . ) Narzędz i e 1 . - zarej estrowano zakończen i e przetwarzani a . Narzędz i e 2 . - zarej estrowano zakończen i e przetwarzani a . Narzędz i e 1 . - zarej estrowano przetwarzani e . Narzędz i e 2 . - zarej estrowano przetwarzani e .

Document tradu i t . (Do kument 3 . ) Sprawdzono p i sown i ę w do kumen c i e . (Do kument 3 . ) Do konano podz i ał u do kumentu n a s t rony . (Do kument 3 . ) (Do kument 3 . ) Narzędz i e 1 . - zarej estrowano zakończen i e przetwarzani a . Narzędz i e 2 . - zarej estrowano zakończen i e przetwarzani a .

Li czba wykonanych procesów : 9 .

Informowanie kl ientów za pomocą zdarzeń

189

Czytelnik mógł zauważyć, że procedury obsługi zdarzeń były wykonywane w takiej kolejności, w jakiej je zarejestrowano, jednak nie ma gwarancji wykonywania ich właśnie w tym porządku i nigdy nie należy tego oczekiwać ani uzależniać od tego prawidło­ wego działania kodu. Jeśli konieczne jest zachowanie ściśle określonej kolejności wykonywania operacji (takiej jak kolejność procesów wykonywanych w naszej przykładowej aplikacji), to nie należy korzystać ze zdarzeń.

Wcześniej wspominaliśmy, że istnieje możliwość dostosowania danych przesyłanych wraz ze zdarzeniem. Można to zrobić, tworząc nową klasę pochodną dziedziczącą po EventArgs i dodając do niej odpowiednie właściwości i metody. Załóżmy, że chcielibyśmy przesyłać wraz ze zda­ rzeniem dokument w jego aktualnej postaci. W tym celu możemy utworzyć klasę przedsta­ wioną na listingu 5.24.

Listing 5.24. Niestandardowa klasa argumentów zdarzenia cl ass Process EventArgs : EventArgs { li Wygodny konstruktor publ i c Process EventArgs (Document document) { Document = document ;

li Dodatkowa właściwość li Nie chcemy, by subskrybenci mieli możliwość li modyfikowania jej wartości, zatem zastosujemy li prywatny akcesor set. li (Oczywiście nie przeszkodzi to subskrybentom li modyfikować samego dokumentu). publ i c Document Document { get ; pri vate set ;

Musimy także utworzyć odpowiednią delegację dla zdarzenia, taką, która będzie akceptować drugi argument typu Proces s Even tArg s, a nie EventArg s . Możemy to zrobić od ręki, trzymając się konwencji, że pierwszy parametr ma nazwę sender, a drugi - zawierający dane zdarzenia nazwę e: del egate vo i d Proces s EventHandl er (obj ect sender , Process EventArgs e) ;

Także w tym przypadku tworzenie takich delegacji jest tak częstym zadaniem, że .NET udo­ stępnia nam ogólny typ EventHandl er, który pozwala nam uniknąć pisania szablonowego kodu. Możemy zatem zastąpić nasz typ Process EventHandl er typem EventHandl er. Zaktualizujmy deklarację naszego zdarzenia (patrz listing 5 .25) .

Listing 5.25. Zaktualizowane składowe zdarze1i publ i c event EventHandl er Proces s i ng ; publ i c event EventHandl er Processed ;

Zmieńmy też metodę pomocniczą generującą zdarzenie, która aktualnie musi akceptować argument typu Process Even tArg s (patrz listing 5.26) .

190

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Listing 5.26. Zaktualizowany kod generujący zdarzenia pri vate voi d OnProces s i ng ( Process EventArgs e) { i f ( Process i ng ! = n u l l ) { Proces s i ng (th i s , e) ;

pri vate voi d OnProces sed ( Process EventArgs e) { i f ( Proces sed ! = nul l ) { Proces sed ( t h i s , e) ;

I w końcu kod wywołujący te metody musi tworzyć obiekty typu Proces s Even tArg s, tak jak to pokazano na listingu 5 .27.

Listing 5.27. Tworzenie obiektu argumentu zdarzenia publ i c vo i d Proces s (Document doc) { ProcessEventArgs e = new Process EventArgs (doc) ; On Processi ng (e) ;

li Najpierw przeprowadzamy szybką weryfikację dokumentu. foreach (Act i onCheckPa i r proces s i n processes) { i f (proces s . Qu i c kCheck ! = n u l l && ! proces s . Qu i c kChe c k (doc) ) { Consol e . Wri teli ne ( " Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . " ) ; i f ( LogText Prov i der ! = n u l l ) { Consol e . Wri te l i ne ( LogText Prov i der (doc) ) ; } On Proces sed (e) ;

return ;

li Teraz wykonujemy akcję. foreach (Act i onCheckPa i r proces s i n processes) { proces s . Act i on (doc) ; i f ( LogText Prov i der ! = n u l l ) { Consol e . Wri teli ne ( LogTextProv i der (doc) ) ; } On Processed (e) ;

Warto zwrócić uwagę, że podczas generowania poszczególnych zdarzeń używane są te same dane. Rozwiązanie to jest bezpieczne, gdyż nie ma możliwości wprowadzenia zmian w obiek­ cie argumentu - jego akcesor set jest prywatny. Gdyby jednak procedury obsługi zdarzeń miały możliwość modyfikowania obiektu argumentu, to stosowanie tego samego obiektu w obu generowanych zdarzeniach byłoby ryzykowne.

Informowanie kl ientów za pomocą zdarzeń

191

Korzystając z tych zdarzeń, możemy zaoferować kolegom z działu produkcyjnego jeszcze jedno ułatwienie. Widzieliśmy już, że muszą oni przeprowadzać szybką weryfikację dokumentu przed rozpoczęciem każdego procesu, by sprawdzić, czy należy przerwać przetwarzanie. Możemy skorzystać ze zdarzenia Process i ng, by zapewnić im możliwość przerwania całego procesu, zanim w ogóle zostanie on rozpoczęty . Biblioteka .NET Framework zawiera klasę o nazwie Cancel EventArgs, która do podstawowej klasy E ven tArg s dodaje właściwość Cancel typu bool . Subskrybent może przypisać tej właściwości wartość t rue, co powinno spowodować przerwanie operacji wykonywanej przez wydawcę zdarzenia . Dodajmy zatem do programu nową klasę argumentów (patrz listing 5.28) .

Listing 5.28. Klasa argumentów umożliwiajqca przerwanie operacji cl ass Process Cancel EventArgs : Cancel EventArgs { publ i c Process Cancel EventArgs (Document document) { Document = document ; publ i c Document Document { get ; pri vate set ;

Teraz zaktualizujemy deklarację naszego zdarzenia Proces s i ng oraz metody pomocniczej uży­ wanej do generowania go. Zmodyfikowaną wersję kodu przedstawiono na listingu 5.29 (warto przy tym zauważyć, że zdarzenie Processed nie zostanie zmienione - skoro dokument został już przetworzony, to jest za późno, by odwoływać całą operację) .

Listing 5.29. Zdarzenie, które można odwołać publ i c event EventHandl er Process i ng ; pri vate voi d OnProces s i ng ( ProcessCancel EventArgs e) { i f ( Process i ng ! = n u l l ) { Proces s i ng (th i s , e) ;

Powinniśmy także zmodyfikować metodę Pro c e s s - musi ona tworzyć obiekt argumentu zdarzenia odpowiedniego typu oraz obsługiwać możliwość anulowania przetwarzania (patrz listing 5 .30) .

Listing 5.30. Obsługa odwołania operacji publ i c vo i d Proces s (Document doc) { Process EventArgs e = new Proces s EventArgs (doc) ; Proces s Cancel EventArgs ce

=

new ProcessCancel EventArgs (doc) ;

OnProces s i ng (ce) ; i f (ce . Cancel )

192

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Consol e . Wr i teli ne ( " Proces został anul owany . " ) ; i f ( LogText Prov i der ! = n u l l ) { Consol e . Wri teli ne ( LogTextProv i der (doc) ) ; return ;

} li

„.

Teraz wykorzystamy te wszystkie możliwości w narzędziach produkcyjnych, jak to pokazano na listingu 5 .31 .

Listing 5.3 1 . Wykorzystanie możliwości anulowania operacji cl ass Produc t i onDeptTool l { publ i c vo i d Subscri be (DocumentProcessor proces sor) { proces sor. Proces s i ng += processor_Proces s i ng ; processor. Processed += processor_Processed ; publ i c vo i d Unsubscri be (Document Processor processor) { proces sor. Proces s i ng - = processor_Proces s i ng ; processor. Processed - = processor_Processed ; vo i d processor_Process i ng (obj ect sende r , ProcessCan cel EventArgs e) { Consol e . Wr i teli ne ( " Narzędz i e 1 . - zarej es t rowano przetwarzan i e , wykonan i e n i e zostało '-+anul owane . " ) ;

vo i d processor_Processed (obj ect sende r , EventArgs e) { Consol e . Wr i teli ne ( " Narzędz i e 1 . - zarej es t rowano zakończen i e przetwarzan i a . " ) ;

cl ass Produc t i onDeptTool 2 publ i c vo i d Subscri be (DocumentProcessor proces sor) { proces sor. Proces s i ng += (sende r , e) => { Consol e . Wri teli ne ( " Narzędz i e 2 . - zarej estrowano przetwarzan i e i f ( e . Document . Text . Con t a i ns ( " do kument " ) ) {

anul owano j e . " ) ;

e . Cancel = true ;

}; processor. Processed += (sende r , e) => Consol e . Wri teli ne ( " Narzędz i e 1 . - zarej estrowano z a kończen i e przetwarzan i a . " ) ;

Należy zwrócić uwagę, że nie musimy aktualizować parametru danych zdarzenia - możemy wykorzystać polimorfizm i odwoływać się do niego jako do obiektu klasy bazowej, chyba że chcemy skorzystać z jego nowych możliwości . W przypadku zastosowania wyrażeń lambda Informowanie kl ientów za pomocą zdarzeń

193

nowy typ parametru zostanie określony automatycznie, w związku z czym nic nie musimy zmieniać. Wystarczy jedynie zaktualizować procedurę obsługi zdarzenia w klasie Prod uct i on '"+DeptTool 2, tak by anulowała przetwarzanie w przypadku odnalezienia słowa dokumen t . Po skompilowaniu i uruchomieniu tego programu wygeneruje on następujące wyniki: Narzędz i e 1 . - zarej estrowano przetwarz an i e , wykonan i e n i e zostało anu l owane . Narzędz i e 2 . - zarej es trowano przetwarzan i e , wykonan i e n i e zostało anu l owane . Przetwarzan i e n i e z a kończy s i ę pomyś l n i e . (Do kument 1 . ) Narzędz i e 1 . - zarej estrowano z a kończen i e przetwarzan i a . Narzędz i e 2 . - zarej estrowano z a kończen i e przetwarzan i a . Narzędz i e 1 . - zarej estrowano przetwarz an i e , wykonan i e n i e zostało anu l owane . Narzędz i e 2 . - zarej estrowano przetwarz an i e , wykonan i e n i e zostało anu l owane . Document tradu i t . (Do kument 2 . ) Sprawdzono p i sown i ę w do kumen c i e . (Do kument 2 . ) Do konano podz i ał u do kumentu n a s t rony . (Do kument 2 . ) Wyróżn i ono s ł owo ' mi l en i um ' . (Do kument 2 . ) Narzędz i e 1 . - zarej estrowano z a kończen i e przetwarzan i a . Narzędz i e 2 . - zarej estrowano z a kończen i e przetwarzan i a . Narzędz i e 1 . zarej estrowano przetwarz an i e , wykonan i e ni e zostało an ul owane . Narzędz i e 2 . zarej estrowano przetwarzani e i anul owano j e . Proces został anul owany . -

-

(Do kument 3 . ) Li czba wykonanych procesów : 6 .

Jak widać, zaimplementowaliśmy mechanizm anulowania przetwarzania dokumentu, musimy jednak zachować dużą ostrożność. Zwróćmy uwagę, że narzędzie 1 . odebrało zdarzenie jako pierwsze i radośnie wykonało jego procedurę obsługi, zanim narzędzie 2. zdążyło anulować cały proces. W przypadku pisania procedur obsługi zdarzeń obsługujących możliwość anulo­ wania trzeba upewnić się, że niewykonanie niektórych (lub nawet wszystkich) z tych procedur nie będzie miało znaczenia oraz że będą one działały prawidłowo, jeśli czynność, której ocze­ kiwały, nigdy nie zostanie wykonana. Zdarzenia zapewniające możliwość anulowania należy bardzo dokładnie dokumentować ze szczególnym uwzględnieniem zdarzeń i akcji, z jakimi są one powiązane, oraz samej składni anulowania operacji. Z tego względu zastosowane przez nas rozwiązanie - polegające na zmianie zdarzenia, którego nie można anulować, w zdarzenie zapewniające taką możliwość - jest bardzo złe. Gdyby wcześniejszy kod został już udostęp­ niony, to wprowadzenie takiej modyfikacji mogłoby doprowadzić do problemów w działa­ niu wszystkich klientów, które zostałyby ponownie skompilowane z wykorzystaniem nowej wersji kodu.

Udostępnianie dużej l iczby zdarzeń Niektóre klasy (w szczególności te związane z interakcjami z użytkownikiem) muszą udo­ stępniać bardzo wiele zdarzeń. Gdybyśmy zastosowali zwyczajną składnię przedstawioną w poprzednich przykładach, to dla każdego zadeklarowanego zdarzenia zostałoby przydzielone miejsce w pamięci, nawet gdyby konkretne zdarzenie nie miało żadnych subskrybentów. Ozna­ czałoby to, że obiekty tego typu bardzo szybko mogłyby się rozrosnąć do wielkich rozmiarów.

194

I

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Aby można było uniknąć takich niepożądanych sytuacji, C# zapewnia nam możliwość samo­ dzielnego zarządzania pamięcią przydzielaną na potrzeby obsługi zdarzeń. Wykorzystywana przy tym składnia przypomina nieco akcesory get i s et właściwości z dodatkowym polem do przechowywania niezbędnych danych: publ i c event EventHandl er MyEvent { acid { li Kod dodający procedurę obslugi zdarzenia remove

li Kod usuwający procedurę obslugi zdarzenia

Zazwyczaj do przechowywania informacji o zdarzeniach używane jest pole typu D i et i o nary "+< Key , Val >, przy czym obiekt tego typu jest tworzony dopiero w momencie pojawienia się pierwszego subskrybenta. (Słowniki, czyli klasy takie jak Di et i on ary, zostały opisane w roz­ dziale 9.) . Listing 5 .32 przedstawia kod klasy Doeument Proeessor, w którym informacje o zdarzeniach są przechowywane w obiekcie słownika.

Listing 5.32 . Własny sposób przechowywania informacji o zdarzeniach cl ass Document Processor pri vate D i ct i onary events ; publ i c event EventHandl er Proces s i ng { acid { Del egate theDel egate = Ens u reEvent ( 11 Proces s i ng 11 ) ; events [ 11 Process i ng 11] = ( ( EventHandl er) theDel egate) + val u e ; remove Del egate theDel egate = Ens u reEvent ( 11 Proces s i ng 11 ) ; events [ 11 Process i ng 11] = ( ( EventHandl er) t heDel egate) - val ue ;

publ i c event EventHandl er Processed { acid { Del egate theDel egate = Ens u reEvent ( 11 Processed 11 ) ; events [ 11 Processed 11] ( ( EventHandl er) theDel egate) + val u e ; =

Informowanie kl ientów z a pomocą zdarzeń

I

195

remove Del egate theDel egate = Ens u reEvent ( " Processed " ) ; events [ " Processed "] = ( ( EventHandl er) theDel egate) - val ue ;

pri vate Del egate Ens u reEvent (stri ng eventName) { li Tworzymy słownik, jeśli jeszcze nie istnieje. i f (events == n u l l ) { events = new D i ct i onary () ;

li Dodajemy puste miejsce na delegację, jeśli jeszcze li go nie mamy. Del egate theDel egate = nu l l ; i f ( ! event s . TryGetVal ue ( eventName , out theDel egate) ) events . Add (eventName , nu l l ) ; return theDel egate ; pri vate vo i d OnProces s i ng ( Process Cancel EventArgs e) { Del egate eh = nu l l ; i f ( events ! = n u l l && events . TryGetVal ue ( " Process i ng " , out eh) ) EventHandl er pceh = eh as EventHandl er ; i f (pceh ! = n u l l ) { pceh (th i s , e) ;

pri vate vo i d OnProcessed ( Proces s EventArgs e) { Del egate eh = nu l l ; i f (events ! = n u l l && events . TryGetVal ue ( " Proce s s ed " , out eh) ) EventHandl er pceh eh as EventHandl er ; i f (pceh ! = n u l l ) { pceh (th i s , e) ;

li

196

„.

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

Oczywiście takie rozwiązanie jest znacznie bardziej skomplikowane od klasycznego sposobu przedstawionego we wcześniejszej części rozdziału i zazwyczaj nie będzie ono stosowane do udostępniania jedynie kilku zdarzeń, niemniej jednak pomoże ono zaoszczędzić wiele czasu w przypadku klas, których jest dużo albo które publikują dużo zdarzeń, choć mają mało sub­ skrybentów.

Podsu mowan ie W tym rozdziale przekonaliśmy się, jak duże możliwości wielokrotnego stosowania kodu zapewniają techniki funkcyjne, oraz poznaliśmy mechanizmy pozwalające na rozszerzanie programów, które są przy tym zarówno bardziej elastyczne, jak i prostsze od rozwiązań opie­ rających się na wykorzystaniu klas. Poznaliśmy także zdarzenia pozwalające nawiązywać relacje jednego wydawcy z wieloma subskrybentami. W następnym rozdziale dowiemy się, jak można reagować na nieoczekiwane sytuacje: błędy, awarie oraz wyjątki.

Podsumowanie

I

197

198

Rozdział 5. Delegacje - łatwość komponowania i rozszerzalność

ROZDZIAŁ 6.

Obsługa błędów

Błędy przytrafiają się cały czas . Ich wystąpienie to pewnik. •

Bez względu na najlepsze starania programu Microsoft Word, armii wykwalifikowanych korektorów i redaktorów, jak również samych autorów byłoby dużym zaskoczeniem, gdyby w książce takiej wielkości nie pojawił się chocby jeden błąd typograficzny.



Choć jest ich stosunkowo niewiele, to jednak pojawiają się błędy także w .NET Frame­ work - z tego względu od czasu do czasu potrzebne jest wydawanie dodatków Service Pack.



Można wpisywać numer karty kredytowej, by kupić coś w internecie, i przypadkowo zamienić kolejność dwóch cyfr lub zapomnieć o wpisaniu daty wygaśnięcia ważności karty.

Może się to nam p odobać lub nie, lecz będziemy musieli pogodzić się z faktem, że także w naszym oprogramowaniu będą pojawiać się wszelkiego typu błędy, a wraz z nimi koniecz­ ność ich naprawy. W tym rozdziale przedstawimy różne typy błędów, narzędzia udostępniane przez C# oraz .NET Framework do obsługi tych błędów oraz wybrane strategie stosowania tych narzędzi. Jednak w pierwszej kolejności należy zauważyć, że nie wszystkie błędy są takie same. Kilka kategorii najczęściej pojawiających się błędów przedstawia tabela 6 . 1 . Choć defekty (ang. bug) s ą zdecydowanie najczęściej występującym typem błędów, to jednak w tym rozdziale nie będziemy się zajmowali nimi bezpośrednio. Dowiemy się natomiast, w jaki sposób techniki obsługi błędów mogą ułatwić (lub utrudnić!) wykrywanie defektów, które nie­ jednokrotnie są przyczyną innych, lepiej zdefiniowanych problemów. Zacznijmy od przykładu, którego będziemy mogli używać do przedstawiania technik obsługi błędów. Tym razem wejdziemy w świat robotyki i spróbujemy utworzyć aplikację do obsługi robota żółwia. W rzeczywistości robot żółw to prostokątna płytka, do której są przymontowane dwa silniczki mogące obracać dwoma kołami umieszczonymi na środku lewej i prawej krawędzi. Oprócz tego z przodu i z tyłu płytki są zamontowane dwa obrotowe nienapędzane kółeczka, których zadaniem jest zapewnienie robotowi stabilności . Oba silniczki mogą być sterowane niezależnie i mogą się kręcić do przodu lub do tyłu; można je też zatrzymywać. Poprzez krę­ cenie silniczkami w przeciwnych kierunkach lub kręcenie tylko jednym z nich możemy stero­ wać robotem w sposób przypominający jazdę czołgiem. Utwórzmy zatem klasę, która będzie modelować naszego robota (patrz listing 6.1) .

199

Tabela 6.1 . Bardzo niekompletna lista najczęściej występujących błędów Błąd

Opis lub przykład

Błąd , defekt

Zaimplementowanie kontraktu niezgod ne z jego dokumentacją.

N ieoczekiwane działanie

Kontrakt nie został zaimplementowany prawidłowo d la wszystkich oczekiwanych danych wejściowych .

Nieoczekiwane dane wejściowe

Klient przekazał d o metody dane leżące poza dozwolonym zakresem.

N ieoczekiwany typ danych

Klient przekazał do metody dane niewłaściwego typu.

Nieoczekiwany format danych

Klient przekazał do metody dane zapisane w niewłaściwym formacie.

N ieoczekiwany wyn i k

Klient otrzymał z m etody i nformacje, których nie oczekiwał dla d anego zestawu d anych wejściowych .

Nieoczekiwane wywołanie metody

Klasa n i e oczekiwała wywoła nia kon k retnej metody w danym czasie - przykładowo nie zostały wykonane niezbęd ne czynności i n icjalizacyjne.

Zasób niedostępny

Metoda próbowała uzyskać dostęp do jakiegoś zasobu, który nie był dostępny - na przykład nie podłączo n o niezbędnego u rządzenia zewnętrznego.

Rywalizacja o zasób

Metoda próbowała uzyskać dostęp do j akiegoś rzadkiego zasobu ( pam ięci lub u rządzenia sprzętowego, które nie może być współdzielo n e ) , który nie był d ostępny, gdyż w d anej chwili używał go ktoś inny.

Listing 6 . 1 . Klasa Turtle cl ass Turtl e { li Szerokość platformy robota publ i c doubl e Pl atformWi dth { get ; set ;

li Wysokość platformy robota publ i c doubl e Pl atformHei ght { get ; set ; li Szybkość, z jaką silniczki kręcq kółkami, li wyrażona w metrach na sekundę. Dla ułatwienia zakladamy, li że została ona określona na podstawie dystansu, jaki pokonały li kółka robota na powierzchni, po której się porusza li (i ewentualnych poślizgów). publ i c doubl e MotorSpeed { get ; set ; li Stan lewego silniczka publ i c MotorState LeftMotorState { get ; set ; li Stan prawego silniczka publ i c MotorState Ri ghtMotorState { get ; set ;

200

I

Rozdział 6. Obsługa błędów

li Aktualne położenie robota publ i c Po i nt Curren t Pos i t i on { get ; pri vate set ; li Aktualna orientacja robota publ i c doubl e CurrentOri entat i on { get ; pri vate set ;

li Aktualny stan silniczka enum MotorState { Stopped , Runn i ng , Reversed

Oprócz sterowania silniczkami możemy także zdefiniować wymiary platformy oraz szybkość, z jaką silniczki kręcą kółkami. Dodaliśmy także parę właściwości pozwalających nam określić, gdzie robot znajduje się w danej chwili (względem swojego położenia początkowego) oraz w jakim kierunku jest obrócony. Aby nasz symulator robota potrafił coś zrobić, dodamy do niego metodę symulującą upływ czasu. Będzie ona analizowała stan obu silniczków i wykorzystywała odpowiedni algorytm do wyliczania nowego położenia robota . Listing 6.2 przedstawia naszą pierwszą, bardzo uprosz­ czoną wersję tej metody.

Listing 6.2 . Symulacja ruchu robota li Robot się porusza przez zadany okres czasu. publ i c vo i d Run For (doubl e durat i on) { i f ( LeftMotorSt ate == MotorState . Stopped && Ri ghtMotorState == MotorState . Stopped) { li Jeśli robot byl calkowicie zatrzymany, nic się nie stanie. return ; li Jeśli oba silniczki pracowaly, kręcqc się w tym samym kierunku, li to mogliśmy jechać. i f ( ( LeftMotorState == MotorState . Runn i ng && Ri ghtMotorState == MotorState . Runn i ng) I I ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Reversed) ) Dri ve (durat i on) ; return ;

li Silniczki kręcq się w przeciwnych kierunkach, li zatem robot nie jedzie do przodu, a jedynie kręci li się wokól swego środka. i f ( ( LeftMotorState == MotorState . Runn i ng && Ri ghtMotorState == MotorState . Reversed) I I ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Runn i ng) ) {

Kiedy i jak uznać niepowodzenie

I

201

Rotate (durat i on) ; return ;

Jeśli oba kółka robota kręcą się w tym samym kierunku (do przodu lub do tyłu), to jedzie on w stronę zgodną z ruchem kółek (w przód lub w tył) . Jeśli kółka kręcą się w przeciwnych kierunkach, to robot kręci się wokół własnego środka. Jeśli kółka się nie kręcą, robot pozostaje w spoczynku. Listing 6.3 przedstawia implementacje metod Dri ve oraz Rota te. Aby wykonywać swe zadania, wykorzystują one obliczenia trygonometryczne.

Listing 6.3. Symulacja obrotów oraz ruchu pri vate voi d Rotate (doubl e durat i on) { li Pelna dlugość okręgu zataczanego przez obracajqcego się robota doubl e c i rcum = Math . P I * Pl atformWi dth ; li Całkowity przebyty dystans doubl e d = durat i on * MotorSpeed ; i f ( LeftMotorSt ate == MotorState . Reversed) { li Jeśli silniczki kręcq się w tył, to jedziemy do tylu. d *= - 1 . 0 ;

li Stosunek przebytego dystansu do obwodu pelnego obrotu doubl e proport i onOfWhol eC i rcl e d / c i rcum ; li Gdy obrót wyniesie 360 stopni (lub 2pi radianów), to przebyta li odleglość wyniesie: CurrentOri entati on = CurrentOri entat i on + (Math . P I * 2 . 0 * proport i onOfWho l eCi rcl e) ; =

pri vate voi d Dri ve (doubl e durat i on) { li Całkowity przejechany dystans doubl e d = durat i on * MotorSpeed ; i f ( LeftMotorSt ate == MotorState . Reversed) { li Jeśli silniczki kręcq się w tył, to jedziemy do tylu. d *= - 1 . 0 ;

li Nieco oblicze1i trygonometrycznych w celu określenia zmiany li wspólrzędnych x i y doubl e del taX = d * Math . S i n (CurrentOri entat i on) ; doubl e del taY = d * Mat h . Cos (CurrentOri entat i on) ; li Zaktualizowane polożenie robota Current Pos i t i on = new Po i nt (Current Pos i t i on . X + del taX , Current Pos i t i on . Y + del taY) ;

Napiszmy zatem krótki program testowy, by sprawdzić, czy napisany kod faktycznie robi to, o co nam chodziło (patrz listing 6 .4) .

Listing 6.4. Testowanie robota żółwia s t at i c vo i d Mai n (s tri ng O arg s ) { 11 Oto nasz robot.

202

I

Rozdział 6. Obsługa błędów

Turt l e arthurTheTurt l e = new Turt l e { Pl atformWi dth

10 . 0 , Pl atformHe i ght

1 0 . 0 , MotorSpeed

=

5.0 } ;

ShowPos i t i on (arthurTheTurt l e) ;

li Chcemy jechać prosto „ . arthurTheTurtl e . LeftMotorState = MotorState . Runn i ng ; arthurTheTurtl e . Ri ghtMotorState = MotorState . Runn i ng ; li „ .przez dwie sekundy. arthurTheTurtl e . Run For (2 . 0) ; ShowPos i t i on (arthurTheTurt l e) ;

li A teraz obrócimy trochę robota zgodnie z ruchem wskazówek zegara. arthurTheTurtl e . Ri ghtMotorState = MotorState . Reversed ; li Pil2 sekundy powinno wystarczyć. arthurTheTurtl e . Run For (Math . P I / 2 . 0) ; ShowPos i t i on (arthurTheTurt l e) ;

li A teraz jedziemy do tylu„. arthurTheTurtl e . Ri ghtMotorState = MotorState . Reversed ; arthurTheTurtl e . LeftMotorState = MotorState . Reversed ; li „ .przez pięć sekund„. arthurTheTurtl e . Run For (5) ; ShowPos i t i on (arthurTheTurt l e) ;

li „ .po czym obracamy się w przeciwnym kierunku „ . arthurTheTurtl e . Ri ghtMotorState = MotorState . Runn i ng ; li „ .przez pil4 sekundy, co da nam 45 stopni. arthurTheTurtl e . Run For (Math . P I / 4 . 0) ; ShowPos i t i on (arthurTheTurt l e) ;

li I znowu pojedziemy trochę do tylu. arthurTheTurtl e . Ri ghtMotorState = MotorState . Reversed ; arthurTheTurtl e . LeftMotorState = MotorState . Reversed ; arthurTheTurtl e . Run For (Math . Cos (Mat h . P I / 4 . 0) ) ; ShowPos i t i on (arthurTheTurt l e) ; Consol e . ReadKey () ; pri vate stat i c voi d ShowPos i t i on (Turt l e arthurTheTurt l e) { Consol e . Wri tel i ne ( "Artur znaj duje s i ę w m1 eJ s cu ( { O } ) i j es t obrócony w ki eru n ku { 1 : 0 . 00} radi anów . " , arthurTheTurt l e . Current Pos i t i on , arthurTheTurt l e . CurrentOri entat i on) ;

Bardzo uważnie dobraliśmy czasy ruchów robota, tak by uzyskane współrzędne i kąt obrotu były czytelne. (Hej, czy ktoś z Czytelników mógłby wymyślić jakiś interfejs API, który byłby łatwiejszy w użyciu!?) Kiedy skompilujemy program i wykonamy go, uzyskamy następujące wyniki: Artur znaj duj e s i ę w mi ej s cu (O ; O) i j es t obrócony w ki erun ku 0 , 00 radi anów . Artur znaj duj e s i ę w mi ej s cu (0 ; 10) i j es t obrócony w ki erun ku 0 , 00 rad i anów . Artur znaj duj e s i ę w mi ej s cu (0 ; 10) i j es t obrócony w ki erun ku 1 , 57 rad i anów .

Kiedy i jak uznać niepowodzenie

I

203

Artur znaj duj e s i ę w mi ej s cu (-25 ; 10) i j es t obrócony w ki erun ku 1 , 57 rad i anów . Artur znaj duj e s i ę w mi ej s cu (-25 ; 10) i j es t obrócony w ki erun ku 0 , 79 rad i anów . Artur znaj duj e s i ę w mi ej s cu ( - 2 7 , 5 ; 7 , 5) i j est obrócony w ki erun ku 0 , 79 radi anów .

No dobrze. Sądząc po tych podstawowych operacjach, można uznać, że wszystko jest w porządku. Ale co się stanie, gdy szerokości platformy przypiszemy wartość O? Turtl e arthurTheTurtl e = new Turt l e { P l atformWi dth = O . O , Pl atformHei ght = 10 . 0 , MotorSpeed = 5 . 0 } ;

Taki przypadek nie tylko nie będzie miał żadnego sensu, lecz co więcej także uzyskiwane wyniki nie będą szczególnie użyteczne . Najwyraźniej występują tu problemy z dzieleniem przez zero. Artur Artur Artur Artur

znaj duj e znaj duj e znaj duj e znaj duj e

się się się się

w mi ej s cu w mi ej s cu w mi ej s cu w mi ej s cu -.+n i eskoficzonośc rad i anów . Artur znaj duj e s i ę w mi ej s cu -.l i czbą rad i anów . Artur znaj duj e s i ę w mi ej s cu -.l i czbą rad i anów .

(O ; O) i j es t obrócony w ki erun ku 0 , 00 radi anów . (0 ; 10) i j es t obrócony w ki erun ku 0 , 00 rad i anów . (0 ; 10) i j es t obrócony w ki erun ku +ni eskoficzonośc rad i anów . (ni e j est l i czbą ; n i e j est l i czbą) j es t obrócony w ki erun ku (ni e j est l i czbą ; n i e j est l i czbą)

j es t obrócony w ki erun ku n i e j est

(ni e j est l i czbą ; n i e j est l i czbą)

j es t obrócony w ki erun ku n i e j est

Nasz rzeczywisty robot mógłby wpaść w poważne tarapaty, gdybyśmy kazali mu obrócić się o nieskończony kąt. W najlepszym wypadku bardzo byśmy się znudzili, czekając, aż skończy wykonywać takie polecenie. A zatem powinniśmy zabronić uruchamiania robota, jeśli szerokość jego platformy (właściwość Pl atfonnWi dth) jest mniejsza lub równa zero. Moglibyśmy zastosować następujący kod: li Robot się porusza przez zadany okres czasu. publ i c vo i d Run For (doubl e durat i on) { i f ( P l atformWi dth 10 . 0)

{

Pl atformWi dth 10 . 0 ; } i f (Pl atformHei ght < 1 . 0)

{

Pl atformHei ght = 1 . 0 ; } i f (Pl atformHei ght > 10 . 0)

{

Pl atformHei ght = 10 . 0 ;

} }

li

„.

W powyższym przykładzie opisaliśmy ograniczenia wprowadzane w kontrakcie klasy, a następ­ nie wymusiliśmy podporządkowanie się im podczas tworzenia obiektu oraz zmieniania wartości. Zdecydowaliśmy się narzucać ograniczenie w momencie zmieniania wartości, gdyż w ten spo­ sób nasz kontrakt staje się bezpośrednio widoczny. Jeśli użytkownik zastosuje wartość wykra­ czającą poza dopuszczalny zakres, a następnie spróbuje ją odczytać, będzie mógł od razu się

206

I

Rozdział 6. Obsługa błędów

przekonać, że ograniczenie jest respektowane. Oczywiście nie jest to jedyne dostępne rozwią­ zanie . Można by sprawdzać i zmieniać wartość właściwości bezpośrednio przed jej użyciem. W takim przypadku w razie zmiany implementacji lub wprowadzenia nowych możliwości mogłoby się jednak okazać, że musimy dodać wiele wywołań metody E n s urePl at formS i z e, i niemal na pewno gdzieś byśmy o nich zapomnieli. Jeśli teraz spróbujemy uruchomić aplikację, wygeneruje ona następujące wyniki: Artur Artur Artur Artur Artur Artur

znaj duj e znaj duj e znaj duj e znaj duj e znaj duj e znaj duj e

się się się się się się

w w w w w w

mi ej s cu mi ej s cu mi ej s cu mi ej s cu mi ej s cu mi ej s cu

(O ; O) i j es t obrócony w ki erun ku 0 , 00 radi anów . (0 ; 10) i j es t obrócony w ki erun ku 0 , 00 rad i anów . (0 ; 10) i j es t obrócony w ki erun ku 15 , 7 1 rad i anów . ( - l , 530757 94227797 E - 1 4 ; 35) i j e s t obrócony w ki erun ku 1 5 , 7 1 rad i anów . ( - l , 530757 94227797 E - 1 4 ; 35) i j e s t obrócony w ki erun ku 7 , 85 rad i anów . ( - 3 , 53553390593275 ; 35) i j es t obrócony w ki erunku 7 , 85 rad i anów .

Choć metoda ta jest całkiem przydatna i, co wyraźnie widać, pozwoliła nam rozwiązać problem zupełnie nieprzydatnych tekstów n i e j est l i czbą, to musimy się zastanowić, czy jest to właściwe rozwiązanie dla konkretnego problemu? Wróćmy do przykładu zastosowania robota do ryso­ wania linii na korcie tenisowym. Czy naprawdę byśmy chcieli, by rysował on linie, zakładając, że jest robotem o szerokości jednego metra, tylko dlatego, że zapomnieliśmy go prawidłowo zainicjować? Analizując przebyte odległości oraz kąty, o jakie się obracał, mamy pewność, że absolutnie tego nie chcemy! '

Ograniczenia takie jak przedstawione w powyższym przykładzie są przydatne i uży­ teczne w bardzo wielu przypadkach. Na przykład możemy chcieć zagwarantować, że pewien element interfejsu użytkownika nie będzie wystawał poza okno. Z drugiej strony internetowy system bankowy, który nie pozwala na przeprowadzanie transakcji o wartości mniejszej niż 10 złotych, nie powinien zwiększać wpisanej przez użytkow­ nika kwoty 1 złotego do 10 i radośnie kontynuować operacji.

. .

A zatem cofnijmy się nieco i skorzystajmy z innego rozwiązania: zwracania wartości infor­ mującej o błędzie.

Zwracanie kod u błędu Już od wielu lat programiści piszą metody, które wykrywają błędy i przekazują informacje o nich w postaci kodu błędu . Zazwyczaj jest to jakaś wartość logiczna, przy czym t rue oznacza powodzenie, a fal s e - niepowodzenie w wykonywaniu operacji . W przypadkach gdy trzeba rozróżniać wiele rodzajów błędów, można także zastosować liczby całkowite lub typ wyli­ czeniowy. .

. ·

.„

.„.„ .

'

Zanim dodamy do naszego projektu wykorzystanie wartości błędów, należy usunąć z niego cały dodany wcześniej kod, który w niewidoczny dla klienta sposób wymu­ szał spełnienie zadanych ograniczeń. Możemy zatem usunąć metodę EnsurePl atformSi ze oraz wszelkie jej wywołania. Qeśli Czytelnik na bieżąco wprowadza zmiany w Visual Studio, to może także umieścić tę metodę oraz jej wywołania w komentarzach) .

A zatem który fragment naszego kodu będzie zwracał informacje o błędzie? W pierwszym odruchu można by zdecydować się na robienie tego w metodzie Run For zgodnie z tym, co suge­ rowaliśmy wcześniej . Spójrzmy jednak na kod tej metody - nie ma w nim nic istotnego. Tak naprawdę, problemy występują w metodzie Rotate. Co by się stało, gdyby na późniejszym etapie

Zwracanie kodu błędu

I 207

rozwoju aplikacji metoda ta została zmieniona, a jej działanie zostało uzależnione od innych właściwości? Czy musielibyśmy zmienić także metodę Run For i uwzględnić w niej nowe ograni­ czenia? I czy byśmy o tym pamiętali? To metoda Rot a t e korzysta z właściwości, a zatem z założenia właśnie w niej powinniśmy sprawdzać ich wartości. Takie rozwiązanie ułatwi nam w przyszłości debugowanie aplikacji pozwoli nam umieścić punkt wstrzymania w okolicy miejsca występowania błędu i zobaczyć, co się tam dzieje . Zmodyfikujmy zatem kod tej metody i zobaczmy, co się stanie (patrz listing 6.6) .

Listing 6.6. Przekazywanie informacji o błędach przy wykorzystaniu wartości zwracanej pri vate bool Rotate (doubl e durat i on) { i f (Pl atformWi dth

{

O . O . " ) ;

li Pelna dlugość okręgu zataczanego przez obracajqcego się robota. doubl e c i rcum = Math . P I * Pl atformWi dth ; li Calkowity przebyty dystans doubl e d = durat i on * MotorSpeed ; i f ( LeftMotorSt ate == MotorState . Reversed)

214

I

Rozdział 6. Obsługa błędów

li Jeśli silniczki kręcq się w tył, to jedziemy do tyłu. d *= - 1 . 0 ; li Stosunek przebytego dystansu do obwodu pełnego obrotu doubl e proport i onOfWhol eC i rcl e = d / c i rcum ; li Gdy obrót wyniesie 360 stopni (lub 2pi radianów), to przebyta li odleglość wyniesie: CurrentOri entati on = CurrentOri entati on + (Math . P I * 2 . 0 * proport i onOfWhol eCi rcl e) ; li return true; (Teraz zwracanie wartości jest niepotrzebne, li więc można tę instrukcję usunqć).

Proszę zwrócić uwagę, że zmieniliśmy typ wynikowy z powrotem na voi d i usunęliśmy nie­ potrzebną już instrukcję ret urn umieszczoną na samym końcu metody. Aktualnie najbardziej interesuje nas instrukcja warunkowa umieszczona na samym początku jej kodu.

Warunki wstępne i końcowe: projektowanie według kontraktu Krótkie testy umieszczone na początku metody s ą czasami nazywane klauzulami strażniczymi (ang.

guard clauses) lub strażnikami (ang. guards). Jeśli wydajność nie ma w przypadku naszej aplikacji większego znaczenia od poprawności jej dzia­ łania (a zazwyczaj nie ma), to sprawdzanie warunków wstępnych przed próbą wykonania metody jest wspaniałym pomysłem. Czasami będziemy także chcieli wykonać podobny zbiór testów bezpośrednio przed zakończeniem jej działania, by upewnić się, że po przeprowadzeniu operacji stan systemu wciąż jest prawidłowy. Filozofia projektowania według kontraktu wymaga, by takie warunki wstępne oraz końcowe były określane w kontrakcie metody, a niektóre języki programowania, takie jak Eiffel, udostępniają mechanizmy pozwalające robić to w sposób deklaratywny. Zespół badawczy firmy Microsoft pracuje nad rozszerzeniem języka C# o nazwie Spec# zawierają­ cym pewne mechanizmy związane z projektowaniem według kontraktu. Informacje na jego temat można znaleźć na stronie http://research. microsoft.com/en-us/projects/specsharp/.

Zamiast zwracać wartość typu wyliczeniowego, zgłaszamy obiekt klasy I n v a l i dOpera t i on "+ Except i on . I n v a l i dOpera t i on Except i o n to jedna z kilku klas pochodnych klasy Except i on . Jest ona przezna­ czona do użycia w sytuacji, gdy operacji nie udało się wykonać, gdyż nie pozwolił na to aktu­ alny stan samego obiektu (w odróżnieniu, na przykład, od sytuacji, w której przekazano do metody nieprawidłowe argumenty) . To całkiem dobrze pasuje do naszego przypadku, a więc możemy skorzystać z tej klasy. Przed udostępnieniem C# 3.0 istniała możliwość zgłaszania obiektu dowolnego typu (na przykład łańcucha znaków). W C# 3.0 dodano jednak ograniczenie, zgodnie z którym instrukcja throw może zgłaszać wyłącznie obiekty klas dziedziczących po Excepti on .

Jeśli przejrzymy dokumentację klasy Except i on (którą można znaleźć na stronie http://msdn. microsoft.com/library/system.exception), przekonamy się, że udostępnia ona właściwość Mes sage.

Wyjątki

215

To właśnie jej wartość określamy, przekazując w konstruktorze obiektu łańcuch znaków. Może to być dowolny tekst, a najlepiej informacja, która mogłaby nam (lub naszym klientom) w jakiś sposób pomóc w określeniu i zlokalizowaniu przyczyny problemów. Kolejną właściwością udostępnianą przez klasę Except i on jest Dat a . Jest to słownik par klucz-wartość, dzięki któremu można skojarzyć z wyjątkiem dodatkowe informacje i który może być niezwykle użyteczny podczas debugowania programu oraz rejestrowania informacji o jego działaniu. Chcąc zastąpić zwracane kody błędów wyjątkami, trzeba wprowadzić całkiem sporo zmian w kodzie aplikacji, zanim uda się ją ponownie skompilować. W pierwszej kolejności zmienimy metodę T urt l e . Run For, tak by nie zwracała już żadnej war­ tości, i usuniemy typ wyliczeniowy Turtl eError (patrz listing 6.12) .

Listing 6.12. Zwracanie informacji o błędzie nie jest już potrzebne li Robot się porusza przez zadany okres czasu. publ i c vo i d Run For (doubl e durat i on) { i f ( LeftMotorSt ate == MotorState . Stopped && Ri ghtMotorState == MotorState . Stopped) li Jeśli robot był całkowicie zatrzymany, nic się nie stanie. return ; li Jeśli oba silniczki pracowały, kręcąc się w tym samym kierunku, li to mogliśmy jechać. i f ( ( LeftMotorState == MotorState . Runn i ng && Ri ghtMotorState == MotorState . Runn i ng) I I ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Reversed) ) Dri ve (durat i on) ;

li Silniczki kręcq się w przeciwnych kierunkach, li zatem robot nie jedzie do przodu, a jedynie kręci li się wokół swego środka. i f ( ( LeftMotorState == MotorState . Runn i ng && Ri ghtMotorState == MotorState . Reversed) I I ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Runn i ng) ) Rotate (durat i on) ;

W drugiej kolejności możemy zająć się kodem wywołującym i usunąć z niego wszystkie fragmenty związane z obsługą zwracanych informacji o błędach (patrz listing 6.13) .

Listing 6.13. W metodzie Main błędy nie sq już sprawdzane jawnie s t at i c vo i d Mai n (s tri ng O arg s ) { Turt l e arthurTheTurt l e = new Turt l e { Pl atformWi dth = O . O , Pl atformHe i ght ShowPos i t i on (arthurTheTurt l e) ;

li Chcemy jechać prosto „ .

216

Rozdział 6. Obsługa błędów

=

10 . 0 , MotorSpeed

=

5.0 } ;

arthurTheTurtl e . LeftMotorState = MotorState . Runn i ng ; arthurTheTurtl e . Ri ghtMotorState = MotorState . Runn i ng ; li „ .przez dwie sekundy. arthurTheTurtl e . Run For (2 . 0) ; ShowPos i t i on (arthurTheTurt l e) ;

li A teraz obrócimy trochę robota zgodnie z ruchem wskazówek zegara. arthurTheTurtl e . Ri ghtMotorState = MotorState . Reversed ; li Pil2 sekundy powinno wystarczyć. arthurTheTurtl e . Run For (Math . P I / 2 . 0) ; ShowPos i t i on (arthurTheTurt l e) ;

li A teraz jedziemy do tylu„. arthurTheTurtl e . Ri ghtMotorState = MotorState . Reversed ; arthurTheTurtl e . LeftMotorState = MotorState . Reversed ; li „ .przez pięć sekund„. arthurTheTurtl e . Run For (5) ; ShowPos i t i on (arthurTheTurt l e) ;

li „ .po czym obracamy się w przeciwnym kierunku „ . arthurTheTurtl e . Ri ghtMotorState = MotorState . Runn i ng ; li przez pil4 sekundy, co da nam 45 stopni. arthurTheTurtl e . Run For (Math . P I / 4 . 0) ; ShowPos i t i on (arthurTheTurt l e) ;

li I znowu pojedziemy trochę do tylu. arthurTheTurtl e . Ri ghtMotorState = MotorState . Reversed ; arthurTheTurtl e . LeftMotorState = MotorState . Reversed ; arthurTheTurtl e . Run For (Math . Cos (Mat h . P I / 4 . 0) ) ; ShowPos i t i on (arthurTheTurt l e) ; Consol e . ReadKey () ;

Możemy także usunąć metodę Handl e Error. No dobrze. Co się stanie, kiedy skompilujemy i uruchomimy ten program (koniecznie należy nacisnąć klawisz F5 lub wybrać z menu opcję Debug/Start Debugging, gdyż w przeciwnym razie nie uruchomimy debuggera)? Cóż, bardzo szybko pojawi się okno debuggera przedstawione na rysunku 6.3. Zgodnie z tym, co sugerują informacje wyświetlone w debuggerze, wykonywanie programu zostało zatrzymane w tym miejscu, gdyż pojawił się nieobsłużony wyjątek. Warto przy tym zauważyć, że aplikacja została zatrzymana dokładnie tam, gdzie wyjątek został zgłoszony. Choć nie podaliśmy żadnych opisowych informacji o błędzie, wyraźnie widać, co go spowodowało. To znacząca różnica w stosunku do aplikacji wykorzystującej kody błędów, w której w momencie wyświetlania kodu w debuggerze traciliśmy informacje o prawdziwej przyczynie problemu. Jeśli interesują nas jeszcze bardziej szczegółowe informacje, wystarczy kliknąć łącze View Detail widoczne u dołu okienka. Spowoduje to wyświetlenie okna dialogowego zawierającego siatkę właściwości zgłoszonego obiektu wyjątku. Zawarte w niej informacje można przejrzeć i wyko­ rzystać jako pomoc przy rozwiązywaniu problemu. (W siatce można zobaczyć także właściwości Message oraz Data, które poznaliśmy wcześniej; na rysunku 6.4 została wyświetlona właściwość S t a c kTrace prezentująca stos wywołań) . Wyjątki

217

Rotate(double duration) J J Przykład 6-11 . Sygnalizowanie błędc),,._. :za pollXlc ą wyjątkó w. private void Rotate (double duration )

{

if (Platforml.�idth O.O. Get general help for thos exception.

S.earch for more Help Online„.

Actions:

Vi ew Detail...

Copy e: O . O . Cze kamy w bl oku fi nal l y . . .

Proszę zwrócić uwagę, że kod służący do obsługi błędów jest aktualnie zgrupowany i umiesz­ czony w przejrzyście zdefiniowanych blokach, a nie rozsiany po całej aplikacji . Poza tym udało się nam znacząco ograniczyć liczbę punktów wyjścia z metody. Aktualnie nie obsługujemy żadnych wyjątków w samej klasie Turtl e . Wyobraźmy sobie, że nasza klasa T urt l e została udostępniona klientom w formie biblioteki, a my - jako czołowy dostawca symulatorów robotów tego typu - chcielibyśmy, by dysponowała ona jakimś wewnętrznym mechanizmem rejestracji pojawiających się błędów; być może prowadzimy dobrowolny program polegający na badaniu wrażeń użytkowników, który przesyła telemetrię robota do naszego zespołu. Wciąż chcemy, by błędy były propagowane z naszej biblioteki do kodu klientów, by ci mogli je obsłużyć. Nam jedynie zależy na tym, byśmy byli informowani o ich wystąpieniach. C# zapewnia możliwość przechwytywania i niezauważalnego ponownego zgłaszania wyjątków, co pokazano na listingu 6.17.

Listing 6.17. Ponowne zgłaszanie wyjątku li Robot się porusza przez zadany okres czasu. publ i c vo i d Run For (doubl e durat i on) { try

{

i f ( LeftMotorState == MotorState. Stopped && Ri ghtMotorState == MotorState . Stopped)

li Jeśli robot był całkowicie zatrzymany, nic się nie stanie. return ; li Jeśli oba silniczki pracowały, kręcąc się w tym samym kierunku, li to mogliśmy jechać. i f ( ( LeftMotorState MotorState . Runn i ng && Ri ghtMotorState == MotorState . Runni ng) 1 1 ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Reversed) ) ==

Dri ve (durat i on) ;

li Silniczki kręcą się w przeciwnych kierunkach, li zatem robot nie jedzie do przodu, a jedynie kręci li się wokół swego środka. i f ( ( LeftMotorState == MotorState . Runn i ng && Ri ghtMotorState == MotorState . Reversed) I I ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Runn i ng) ) Rotate (durat i on) ;

catch ( Excepti on ex)

Wyjątki

I 223

Consol e . Wr i teli ne ( " Informacj a do d z i enn i ka : " + ex . Me s s age) ; li Ponowne zgłoszenie wyjqtku throw ;

Pierwszą rzeczą, na jaką należy zwrócić uwagę w tym przykładzie, jest fakt przechwytywania wyjątków typu Except i on, choć wcześniej zaznaczaliśmy, że niemal nigdy się tego nie robi . Jednak w tym przypadku chcemy rejestrować wszystkie wyjątki, a ponieważ nie ignorujemy ich, lecz ponownie zgłaszamy, nie będziemy pomijali tych wyjątków, których się nie spodziewamy. Po wykonaniu kodu procedury obsługi (w którym w tym przykładzie jedynie zapisujemy infor­ mację w dzienniku) używamy słowa kluczowego t h row (bez żadnego obiektu), by ponownie zgłosić przechwycony wyjątek. Po skompilowaniu tego kodu i wykonaniu naszego przykładowego programu wygeneruje on następujące wyniki: Artur znaj duj e s i ę w mi ej s cu (O ; O) i j es t obrócony w ki erun ku 0 , 00 radi anów . Artur znaj duj e s i ę w mi ej s cu (0 ; 10) i j es t obrócony w ki erun ku 0 , 00 rad i anów . I n formacj a od dz i en n i ka : Właści wości Pl atformWi dth nal eży przypi sać wartość > O . O Błąd dz i ałan i a robota : Właści wości Pl atformWi dth nal eży przypi s ać wartość > O . O

Cze kamy w bl o ku fi nal l y . . .

Warto zwrócić uwagę, że w wynikach są widoczne informacje wygenerowane przez obie procedury obsługi wyjątków. Nie jest to jedyny sposób zgłaszania wyjątków z wnętrza bloków cat c h . Całkowicie sensowne i uzasadnione może także być zgłaszanie z procedury obsługi wyjątków dowolnego innego typu! Rozwiązanie to jest często wykorzystywane w celu zmiany wyjątku stosowanego w naszej implementacji na wyjątki innego typu, bardziej odpowiednie dla kontekstu, w jakim nasz kod jest używany. W takim przypadku oryginalny wyjątek nie jest zgłaszany ponownie, lecz jest umieszczany we właściwości I n n erExcept i on nowego obiektu wyjątku, jak to pokazano na listingu 6 .18.

Listing 6.18. Umieszczanie jednego wyjątku wewnątrz innego li Robot się porusza przez zadany okres czasu. publ i c vo i d Run For (doubl e durat i on) { try

{

i f ( LeftMotorState == MotorStat e . Stopped && Ri ghtMotorState == MotorState . Stopped)

li Jeśli robot był całkowicie zatrzymany, nic się nie stanie. return ; li Jeśli oba silniczki pracowały, kręcqc się w tym samym kierunku, li to mogliśmy jechać. i f ( ( LeftMotorState == MotorState . Runn i ng && Ri ghtMotorState == MotorState . Runni ng) 1 1 ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Reversed) ) Dri ve (durat i on) ;

224 I

Rozdział 6. Obsługa błędów

li Silniczki kręcq się w przeciwnych kierunkach, li zatem robot nie jedzie do przodu, a jedynie kręci li się wokół swego środka. i f ( ( LeftMotorState == MotorState . Runn i ng && Ri ghtMotorState == MotorState . Reversed) I I ( LeftMotorState == MotorState . Reversed && Ri ghtMotorState == MotorState . Runn i ng) ) Rotate (durat i on) ; catch ( I nval i dOperat i on Except i on i ox) { throw new Except i on ( " J a k i ś probl em z robotem . . . " , i ox) ; catch ( Except i on ex) { li Tu rejestrujemy informację o wyjqtku w dzienniku. Consol e . Wr i teli ne ( " Informacj a od d z i enn i ka : " + ex . Me s s age) ; li Ponowne zgłoszenie wyjqtku throw ;

Należy zwrócić uwagę na sposób, w jaki przekazaliśmy obsługiwany wyjątek jako parametr podczas tworzenia nowego wyjątku. Wprowadźmy teraz szybką modyfikację w kodzie naszej procedury obsługi wyjątków w metodzie Ma i n, tak by korzystała z nowej możliwości (patrz listing 6 .19) .

Listing 6.19. Wykorzystanie właściwości InnerException s t at i c vo i d Mai n (s tri ng O arg s ) { Turt l e arthurTheTurt l e = new Turt l e { Pl atformWi dth = O . O , Pl atformHe i ght };

10 . 0 , MotorSpeed

=

5.0

ShowPos i t i on (arthurTheTurt l e) ; try {

li

„.

catch ( I nval i dOperat i on Except i on e) { Consol e . Wr i teli ne ( " Bł ąd dz i ał an i a robota : " ) ; Consol e . Wr i teli ne (e . Me s s age) ; catch ( Excepti on el)

{

li Pętla przeglqdajqca wszystkie wewnętrzne wyjqtki li i wyświetlajqca ich komunikaty Excepti on c urrent = e l ; whi l e (current ! = n ul l )

{

Consol e . Wri teli ne (current . Message) ; current = current . I nnerExcepti on ;

} }

fi nal l y

Wyjątki

I 225

Consol e . Wr i teli ne ( " Cze kamy w bl oku fi nal l y . . . " ) ; Consol e . ReadKey () ;

Jeśli skompilujemy i wykonamy tę nową wersję aplikacji, wygeneruje ona następujące wyniki zawierające komunikaty wszystkich wyjątków - zewnętrznych oraz wewnętrznych: Artur znaj duj e s i ę w mi ej s cu (O ; O) i j es t obrócony w ki erun ku 0 , 00 radi anów . Artur znaj duj e s i ę w mi ej s cu (0 ; 10) i j es t obrócony w ki erun ku 0 , 00 rad i anów . J a k i ś probl em z robotem . . . Właśc i wo ś c i Pl atformWi dth nal eży przyp i s a ć wartość > O . O Cze kamy w bl o ku fi nal l y . . .

Bez wątpienia taki sposób wykorzystania wyjątku związanego ze szczegółami implementa­ cyjnymi w połączeniu z odpowiednimi informacjami jawnie podanymi w publicznym kontrakcie może pomóc w uproszczeniu i zmniejszeniu liczby wymaganych procedur obsługi wyjątków. Dodatkowo pozwala on także ukrywać szczegóły implementacyjne, gdyż zgłaszane wyjątki także można uznać za element kontraktu. Z drugiej strony, czy taka technika zgłaszania jednego wyjątku umieszczonego w innym (bądź też jawnego powtórnego zgłaszania tego samego wyjątku) ma jakieś wady? Ponieważ pro­ gramiści bardzo poważnie traktują wszelkiego typu kompromisy, trzeba zgodnie z oczeki­ waniami stwierdzić, iż rozwiązanie to ma swoje słabe strony. Jeśli wyjątek zostanie jawnie (ponownie) zgłoszony, to stos wywołań w nowej procedurze obsługi wyjątków będzie się zaczynał od nowej instrukcji t hrow, co oznacza, że w debuggerze zostanie utracony oryginalny kontekst (choć wciąż będzie go można poznać, sprawdzając zawartość wewnętrznego wyjątku) . Prowadzi to do znacznego zmniejszenia wydajności loka­ lizowania i naprawiania problemów. Z tego powodu decyzję o zapisywaniu jednego wyjątku w innym należy podejmować bardzo rozważnie i zawsze pamiętać, by niejawnie (a nie jawnie) ponownie zgłaszać wyjątki, które zostały przechwycone, a mają zostać przekazane dalej .

Kiedy są wykonywane bloki final ly? Warto dokładnie wyjaśnić, kiedy, w razie występowania kilku warunków brzegowych, są wykonywane bloki fi n a l l y . Przede wszystkim sprawdźmy, co się stanie, kiedy uruchomimy naszą przykładową aplikację poza środowiskiem debuggera. Gdy to zrobimy (naciskając kombinację klawiszy Ctrl+F5), zosta­ nie uruchomiony systemowy mechanizm obsługi błędów2 , który wyświetli użytkownikowi informacyjne okienko dialogowe, jeszcze zanim zostanie wykonany kod umieszczony w bloku fi na 1 1 y! Wygląda to tak, jakby środowisko uruchomieni owe wstawiło dodatkowy blok catch do naszej własnej procedury obsługi wyjątków (głównego poziomu), zamiast przekazać wyjątek na wyższy poziom (co wiązałoby się z wykonaniem kodu znajdującego się w bloku fi n a l l y) . A co się stanie, gdy wyjątek zostanie zgłoszony poza fragmentem kodu, w którym wyjątki są przechwytywane i obsługiwane? 2 We wcześniejszych wersjach systemu Windows był on z większym poczuciem humoru określany jako dr Watson.

226 I

Rozdział 6. Obsługa błędów

Spróbujmy teraz dodać blok fi n a l l y do naszej metody Run For (patrz listing 6.20) .

Listing 6.20. Sprawdzanie, kiedy sq wykonywane bloki finally li Robot się porusza przez zadany okres czasu. publ i c vo i d Run For (doubl e durat i on) { try { li „.

catch ( I nval i dOperat i on Except i on i ox) { throw new Except i on ( " J a k i ś probl em z robotem . . . " , i ox) ; catch ( Except i on ex) { li Tu rejestrujemy informację o wyjqtku w dzienniku. Consol e . Wr i teli ne ( " Informacj a od d z i enn i ka : " + ex . Me s s age) ; li Ponowne zgłoszenie wyjątku throw ; fi nal l y { Consol e . Wr i teli ne ( "W bl o ku fi nal l y w kl as i e Turtl e . " ) ;

Kiedy skompilujemy i uruchomimy tę wersję aplikacji, wygeneruje ona następujące wyniki: Artur znaj duj e s i ę w mi ej s cu (O ; O) i j es t obrócony w ki erun ku 0 , 00 radi anów . W bl o ku fi nal l y w kl as i e Turt l e . Artur znaj duj e s i ę w mi ej s cu (0 ; 10) i j es t obrócony w ki erun ku 0 , 00 rad i anów . W bl o ku fi nal l y w kl as i e Turt l e . J a k i ś probl em z robotem . . . Właśc i wo ś c i Pl atformWi dth nal eży przyp i s a ć wartość > O . O Cze kamy w bl o ku fi nal l y . . .

A zatem bloki fi n a l l y są wykonywane po zgłoszeniu wyjątku, lecz przed procedurą obsługi wyjątków na wyższych poziomach stosu wywołań.

Określanie, jakie wyjątki będą przechwytywane Wciąż musimy znaleźć odpowiedź na jedno ważne pytanie: skąd mamy wiedzieć, jakiego typu wyjątki przechwytywać i obsługiwać? W odróżnieniu od niektórych języków programowania (takich jak Java) C# nie udostępnia żadnego słowa kluczowego, które pozwalałoby deklarować, że metoda może zgłaszać wyjątki pewnego konkretnego typu. Trzeba zatem polegać na dobrej dokumentacji napisanej przez twórców kodu. Dokumentacja MSDN dotycząca samej platformy .NET bardzo rzetelnie i dokładnie opisuje wszystkie wyjątki, jakie mogą być zgłaszane przez poszczególne metody (i właściwości); wszyscy programiści powinni postępować podobnie . Platforma .NET Framework udostępnia szeroką gamę typów wyjątków, które można przechwy­ tywać (i których często można używać) . Zajrzyjmy jeszcze raz do tabeli 6 .1 zamieszczonej na początku rozdziału (zawierającej informacje o najczęściej występujących błędach) i zobaczmy, jakie wyjątki można by zastosować w tych sytuacjach (przedstawia je tabela 6.2) .

Wyjątki

I

227

Tabela 6.2 . Niektóre popularne błędy i odpowiadające im typy wyjątków Błąd

Opis

Przykłady

Nieoczekiwane dane wejściowe

Klient przekazał do metody dane leżące poza

ArgumentExcepti on

dozwolonym zakresem.

ArgumentNul l Excepti on ArgumentOutOfRangeExcept i on

Nieoczekiwany typ danych

Klient przekazał do metody dane niewłaściwego typu.

I nva l i dCas tExcept i on

N ieoczekiwany format danych

Klient przekazał do metody dane zapisane

FormatExcepti on

w niewłaściwym formacie. Nieoczekiwany wynik

Klient otrzy m ał z metody i n f ormacj e , których n i e

Nul l ReferenceExcept i on

oczekiwał dla danego zestawu danych wejściowych . N ieoczekiwane wywołanie metody

Klasa nie oczekiwała wywołania konkretnej metody

I nva l i dOperat i onExcept i on

w danym czasie - przykładowo nie zostały wykonane n iezbęd ne czynności inicjalizacyjne. Zasób niedostępny

Metoda próbowała uzyskać dostęp do jakiegoś zasobu,

T i meoutExcept i on

który nie był dostępny - na przykład nie podłączono niezbędnego u rządze nia zewnętrznego . Rywalizacja o zasób

M etoda próbowała uzyskać d ostęp d o j a kiegoś

OutOfMemoryExcept i on

rzadkiego zasobu (pamięci lub urządzenia sprzętowego,

T ; meoutExcept; on

które n i e m oże być ws półdzielone ) , który n i e był dostępny, gdyż w danej chwili używał go ktoś inny.

Oczywiście lista ta nie jest wyczerpująca, jednak zawiera wiele spośród najczęściej występu­ jących wyjątków, z jakimi można się zetknąć w rzeczywistych aplikacjach. Jednym z najbardziej użytecznych wyjątków, które sami będziemy zgłaszać, jest wyjątek Arg ument Except i o n . Można go używać, gdy parametry przekazane do metody nie przejdą weryfikacji. Wykorzystajmy zatem ten wyjątek w naszej metodzie Run For. Załóżmy, że nasz robot ma pewną szczególną cechę związaną z jego komponentami sprzętowymi - ulega on awarii i przestaje reagować na jakiekolwiek sygnały, jeśli próbujemy go uruchomić na okres o długości O sekund. Możemy temu zaradzić samodzielnie, sprawdzając odpowiedni warunek w kodzie metody Run For i zgłaszając odpowiedni wyjątek w razie wystąpienia problemu (patrz listing 6.21) .

Listing 6.2 1 . Zgłaszanie wyjątku w przypadku przekazania niewłaściwych argumentów publ i c vo i d Run For (doubl e durat i on) {

i f (durati on e . StartTi me . Date == dateOflnterest) ;

foreach (Cal endarEvent i tem i n i temsOnDateOfinterest) { Consol e . Wri teli ne ( i tem . Ti t l e + " : " + i tem . StartTi me) ;

Należy zwrócić uwagę, że w powyższym przykładzie użyliśmy wyrażenia lambda, by poin­ formować metodę Fi ndA 1 1 , jakie elementy ma wybrać. Takie rozwiązanie nie jest jednak konieczne. Metoda F i n dAl l wymaga przekazania delegacji, a zatem można wykorzystać dowolną z opcji opisanych w rozdziale 5. - wyrażenia lambda, metody anonimowe, nazwę metody bądź też dowolne wyrażenie zwracające odpowiednią delegację. W tym przypadku typem delegacji jest Predi cate, gdzie T to typ elementu tablicy (dla naszego przykładu będzie to więc typ Predi cate "+) . Delegacje predykatów także zostały opisane w rozdziale 5. Na wypadek gdyby pamięć Czytelnika wymagała odświeżenia, musimy podać funkcję, która pobiera argu­ ment typu C a l endarEvent i zwraca wartość t ru e, jeśli wydarzenia pasują, lub wartość fal s e w przeciwnym razie. Przykład zamieszczony na listingu 7.14 korzysta z tego samego wyrażenia co instrukcja i f z listingu 7.13. Można uznać, że w stosunku do kodu z listingu 7.13 nie jest to żadnym znaczącym uspraw­ nieniem. Kod wcale nie jest krótszy, a dodatkowo w celu wykonania zadania zastosowaliśmy bardzo zaawansowaną możliwość języka - wyrażenia lambda. Jednak warto zwrócić uwagę na fakt, że w nowej wersji kodu odnaleźliśmy interesujące nas elementy tablicy już przed roz­ poczęciem pętli . Podczas gdy pętla z listingu 7.13 zawiera połączenie kodu poszukującego interesujących nas elementów oraz kodu, który coś z nimi robi, w naszym nowym rozwiązaniu obie te czynności są w elegancki sposób oddzielone od siebie. Gdyby selekcja interesujących nas elementów tablicy była zadaniem znacznie bardziej skomplikowanym, ta separacja zyskałaby dodatkowe znaczenie - znacznie łatwiej jest zrozumieć i utrzymać kod, jeśli nie stara się on wykonywać wielu zadań jednocześnie . Metoda F i ndA 1 1 staje się jeszcze bardziej użyteczna, jeśli chcemy przekazać zbiór odnalezionych elementów do jakiegoś innego fragmentu kodu. Tablicę wybranych elementów, którą zwraca ona jako wynik swego działania, można bowiem przekazać w wywołaniu innej metody. W jaki sposób moglibyśmy uzyskać podobną możliwość, stosując rozwiązanie z listingu 7.13, w którym kod odnajdujący odpowiednie elementy tablicy jest wymieszany z kodem je przetwarzającym? Choć pętla foreach zastosowana na listingu 7.13 jest dobrym rozwiązaniem w trywialnych przy­ padkach, to jednak metoda Fi ndA 1 1 oraz inne podobne techniki (takie jak LINQ, którym zajmiemy się w następnym rozdziale) znacznie lepiej nadają się do zastosowania w sytuacjach bardziej złożonych, które zapewne częściej będą się pojawiać w rzeczywistych aplikacjach. •

.

·

.„

.._,..�;

_. . �• , ...__ __

To ważna zasada, która nie ogranicza się jedynie do tablic i kolekcji. Ogólnie rzecz biorąc, należy starać się tworzyć programy poprzez łączenie niewielkich fragmentów, z których każdy realizuje precyzyjnie zdefiniowane zadanie . Kod napisany w taki sposób jest zazwyczaj łatwiej pielęgnować i zawiera on mniej błędów od złożonego kodu wykonującego wiele zadań jednocześnie. Separacja kodu selekcjonującego infor­ macje od kodu, który je przetwarza, jest jednym z przykładów zastosowania tej zasady.

Tablice

I 243

Klasa Array udostępnia kilka wariacji na temat metody F i ndA 1 1 . Jeśli na przykład interesuje nas pobranie tylko pierwszego elementu spełniającego zadane kryteria, to możemy skorzystać z metody F i n d . Dostępna jest także metoda F i n d l a s t, która zwraca ostatni ze zbioru tych elementów. Czasami można potrzebować informacji, w którym miejscu tablicy znajduje się odnaleziony element. Dlatego jako alternatywy dla metod F i nd oraz F i ndlast klasa Array udostępnia metody F i nd l ndex oraz F i ndlastl ndex, które działają tak samo, lecz zamiast odpowiednio pierwszego lub ostatniego dopasowanego elementu tablicy zwracają liczbę będącą jego indeksem. Istnieje także pewien szczególny przypadek odnajdywania indeksu, który pojawia się sto­ sunkowo często. Chodzi o sytuację, gdy dokładnie wiemy, którego obiektu szukamy, a intere­ suje nas jedynie jego indeks. Oczywiście zadanie to moglibyśmy zrealizować, używając odpo­ wiedniego predykatu takiego jak ten przedstawiony poniżej . i nt i ndex = Array . Fi ndl ndex (events , e => e == somePart i cu l arEvent) ;

Klasa Array udostępnia jednak wyspecjalizowane metody I ndexOf oraz Las t i ndexOf, dzięki którym to samo zadanie można wykonać w następujący sposób: i nt i ndex = Array . I ndexOf (events , somePart i cu l arEvent) ;

Porząd kowanie elementów tablic Czasami może się nam przydać możliwość zmiany kolejności elementów tablicy. Na przy­ kład w naszym kalendarzu niektóre wydarzenia mogą być planowane na wiele dni wcześniej, natomiast inne dodawane w ostatniej chwili, a przecież każda aplikacja zarządzająca kalenda­ rzem musi dysponować możliwością przedstawiania zapisanych wydarzeń w porządku chro­ nologicznym niezależnie do tego, w jakiej kolejności były one dodawane. Dlatego też będzie nam potrzebny jakiś sposób na zapisanie elementów tablicy w odpowiednim porządku. Dzięki metodzie Sort klasy Array staje się to bardzo łatwe. Musimy jedynie przekazać jej infor­ mację o tym, jak wydarzenia mają być uporządkowane - sama w żaden sposób nie będzie mogła się domyślić, czy mają one być posortowane według tytułu (T i t l e) , daty i godziny roz­ poczęcia ( St artT i m e) , czy też czasu trwania (Du rat i on ) . To zadanie doskonale nadaje się dla delegacji: możemy dostarczyć krótki fragment kodu, który porówna dwa obiekty Cal endarEvent i określi, w jakiej kolejności powinny zostać zapisane, a następnie przekazać go do metody Sort (patrz listing 7.15) .

Listing 7.15. Sortowanie tablicy Array . Sort (events , (even t l , event2) => event l . Start T i m e . CompareTo (event2 . Start T i me) ) ;

Pierwszy argument metody Sort - events - jest tablicą, której elementy należy posortować (zdefiniowaliśmy ją już wcześniej, na listingu 7.10) . Drugim argumentem jest natomiast dele­ gacja - w tym przypadku dla wygody zastosowaliśmy wyrażenie lambda (ogólna składnia tych wyrażeń została opisana w rozdziale 5.) . Dla każdych dwóch elementów tablicy metoda Sort chciałaby wiedzieć, czy pierwszy z nich powinien być umieszczony przed, czy za drugim. Wymaga ona przekazania delegacji typu Campari son - funkcji, która pobiera dwa argumenty (nazwaliśmy je tu even t l oraz event2 ) i zwraca liczbę. Jeśli argument even t l powinien się znaleźć przed argumentem event2, to zwrócona przez metodę liczba musi być ujemna; w przeciwnym razie (jeśli even t l ma być za event2 ) wynik funkcji musi być dodatni . Wartość zero oznacza, że

244 I

Rozdział 7. Tablice i listy

oba porównywane elementy są równe . W przykładzie przedstawionym na listingu 7.15 całe zadanie porównania zostało przekazane właściwości Start T i me. Ten obiekt typu DateT i meOffset udostępnia wygodną właściwość CompareTo, która robi dokładnie to, o co nam chodzi. Okazuje się, że sposób sortowania zastosowany w przykładzie z listingu 7.15 niczego nie zmie­ nia w naszej tablicy wydarzeń event s, gdyż jej elementy już podczas tworzenia były zapisywane w rosnącej kolejności daty i godziny. A zatem by pokazać, że faktycznie można sortować na podstawie dowolnego kryterium, uporządkujmy wydarzenia według długości ich trwania: Array . Sort (events , (even t l , event2) => event l . Durat i on . CompareTo (event2 . Dura t i on) ) ;

Przykład ten pokazuje, w jaki sposób zastosowanie delegacji pozwala nam wykorzystać dowolne kryterium i przekazać mozolne zadanie odpowiedniego pozmieniania kolejności poszczególnych elementów samej klasie A rray. Niektóre typy danych, takie jak daty lub czas, mają swoją naturalną kolejność sortowania. Denerwujące byłoby, gdybyśmy musieli instruować metodę Array . Sort, jak ma określać porzą­ dek, w którym mają być uszeregowane liczby. I, jak się okazuje, wcale nie musimy tego robić wystarczy przekazać tablicę liczb do przeciążonej wersji metody Sort, jak to pokazano na listingu 7.16 .

Listing 7.16. Sortowanie danych posiadających naturalne uporządkowanie i nt D numbers = { 4 , 1 , 2 , 5 , 3 } ; Array . Sort (numbers ) ;

Zgodnie z tym, czego można oczekiwać, powyższy przykładowy kod sortuje liczby w tablicy w kolejności rosnącej . W tym przypadku delegację trzeba by przekazywać wyłącznie wtedy, gdybyśmy chcieli posortować liczby inaczej . Czytelnik może się zastanawiać, co by się stało, gdybyśmy spróbowali w ten prostszy sposób posortować tablicę obiektów Cal endarEven t : Array . Sort (events) ; li Bum!

Gdybyśmy tak zrobili, metoda zgłosiłaby wyjątek I n val i dOperat i on Except i on, gdyż nie miałaby jak określić, w jakiej kolejności należy uporządkować obiekty. Metoda A rray . Sort może opero­ wać na typach, które mają określony naturalny porządek. Jednak gdybyśmy tego chcieli, to moglibyśmy zmodyfikować klasę Cal endarEvent tak, by taki porządek sortowania posiadała. Wystarczy w tym celu zaimplementować w niej interfejs I Comparab l e deklarujący pojedynczą metodę CompareTo . Listing 7.17 przedstawia tę implementację; zadanie sortowania zostało w niej przekazane do właściwości StartT i me typu DateTi meOffset, który z kolei imple­ mentuje interfejs I Comparabl e. A zatem cała nasza praca w tym przykładzie sprowadza się do przekazania odpowiedzialności za sortowanie elementów do właściwości, na podstawie której chcemy je posortować (dokładnie tak samo, jak zrobiliśmy to w przykładzie z listingu 7.15) . Jedyną dodatkową operacją, jaką wykonujemy, jest sprawdzenie, czy porównanie nie zwróciło wartości n u l l . Dokumentacja interfejsu I Comparabl e stwierdza, że wynikiem porównania jakiegokolwiek obiektu z n u l l zawsze musi być wartość większa od n u l l , a zatem w takim przypadku zwracamy wartość dodatnią. Gdybyśmy pominęli to porównanie, to w razie przekazania do metody CompareTo wartości n u l l zgłosiłaby ona wyjątek N u l l ReferenceExcept i on.

Listing 7.17. Zapewnienie możliwości porównywania typu cl ass Cal endarEvent : I Comparabl e

{ publ i c s t r i ng T i t l e { get ; set ; }

Tablice

I 245

publ i c DateTi meOffset StartT i me { get ; set ; publ i c T i meSpan Durat i on { get ; set ; } publ i c i nt CompareTo (Cal endarEvent other) { i f (other == nul l ) { return 1 ; } return StartTime . CompareTo (other . StartTime) ; }

Skoro nasza klasa Cal endarEvent określa już swój własny naturalny porządek sortowania, możemy skorzystać z prostszej wersji metody Array . Sort : Array . Sort (events) ; li Działa - teraz CalendarEvent implementuje już IComparable.

Porządkowanie elementów tablicy w określonej kolejności nie jest jedynym powodem ich przemieszczania, dlatego też klasa Array udostępnia nieco mniej wyspecjalizowane metody zmiany ich położenia wewnątrz tablic.

Przenoszenie i kopiowanie elementów Załóżmy, że chcielibyśmy napisać aplikację zarządzającą kalendarzami i pracującą z różnymi źródłami danych - być może korzystamy z kilku różnych witryn WWW udostępniających funkcjonalność kalendarza i chcielibyśmy pobrać wszystkie wydarzenia i połączyć je w jedną listę . Listing 7.18 przedstawia metodę, która pobiera dwie tablice obiektów C a l endarEvent i zwraca jedną tablicę zawierającą wszystkie elementy obu tablic wejściowych.

Listing 7.18. Kopiowanie elementów dwóch tablic do jednej dużej s t at i c Cal endarEvent [] Comb i neEvents (Cal endarEvent [] events l , Cal endarEvent [] events2) Cal endarEvent [] comb i nedEvents = new Cal endarEvent [events l . Length + events2 . Length] ; events l . CopyTo (comb i nedEvents , O) ; events2 . CopyTo (comb i nedEvents , events l . Length) ; return comb i nedEvents ;

W powyższym przykładzie wykorzystana została metoda CopyTo, która kopiuje wszystkie elementy tablicy źródłowej do tablicy docelowej przekazanej jako pierwszy argument jej wywo­ łania . Drugi argument określa miejsce tablicy docelowej, od którego zaczną być dodawane nowe, kopiowane elementy. W kodzie z listingu 7.18 przesunięcie pierwszego skopiowanego elementu względem początku tablicy wynosi O, a kolejny kopiowany element zostanie umiesz­ czony bezpośrednio za nim. (Oznacza to, że kolejność elementów nie będzie zbyt użyteczna, więc zapewne po zakończeniu kopiowania będziemy chcieli posortować nową tablicę) . Czasami będziemy chcieli być nieco bardziej wybiórczy i na przykład skopiować z tablicy źródłowej tylko pewne wybrane elementy. Załóżmy, że chcielibyśmy usunąć pierwszy element tablicy. W platformie .NET nie można zmniejszać tablic, nic jednak nie stoi na przeszkodzie, by utworzyć nową tablicę o jeden element mniejszą i zawierającą wszystkie elementy tablicy oryginalnej z wyjątkiem pierwszego. W takim przypadku nie można zastosować metody CopyTo, gdyż kopiuje ona jedynie całe tablice. Można natomiast skorzystać z bardziej elastycznej metody Array . Copy, której przykład użycia został przedstawiony na listingu 7.19 .

246 I

Rozdział 7. Tablice i listy

Listing 7.19. Kopiowanie wybranego fragmentu tablicy s t at i c Cal endarEvent [] Remove Fi rstEvent (Cal endarEvent [] even t s ) { Cal endarEvent [] croppedEvents = new Cal endarEvent [events . Length - 1] ; Array . Copy ( events , 1, croppedEvents , );

li Tablica źródłowa li Punkt poczqtkowy w tablicy źródłowej li Tablica docelowa o, li Punkt poczqtkowy w tablicy docelowej events . Length - 1 li Liczba elementów do skopiowania

return croppedEvents ;

W przykładzie tym największe znaczenie ma możliwość określenia początkowego indeksu tablicy źródłowej (tego, od którego rozpocznie się kopiowanie jej elementów) . W naszym przy­ padku ma on wartość 1 , co oznacza, że pierwszy element tablicy (o indeksie O) zostanie pominięty . ••

• .·

._,..�;

.

...._ .__ ____..,. ,

W praktyce takie rozwiązania są jednak rzadko stosowane. Gdyby konieczna była możliwość dodawania lub usuwania elementów z kolekcji, nie używalibyśmy normalnej tablicy, lecz klasy Li st , która zostanie dokładniej opisana w dalszej części tego rozdziału. Nawet w przypadku stosowania zwyczajnych tablic dostępna jest pomocnicza metoda Array . Resi ze, która jest zazwyczaj wykorzystywana w praktyce i która za nas wywołuje metodę Array . Capy. Potrzeba skopiowania danych z jednej tablicy do drugiej pojawia się stosunkowo często, nawet jeśli w naszym prostym przykładzie nie występuje ona bezpośrednio. Bardziej złożony przykład przesłoniłby jednak tylko rzeczywistą prostotę metody Array . Capy.

Zagadnienie wielkości tablic jest nieco bardziej złożone, niż można początkowo sądzić, zatem przyjrzyjmy mu się bardziej szczegółowo.

Wiel kość tablic Tablice wiedzą, ile elementów zawierają. W kilku z przedstawionych wcześniej przykładów wykorzystaliśmy właściwość Length, by dowiedzieć się, jaki jest rozmiar istniejącej tablicy. Ta przeznaczona tylko do odczytu właściwość jest zdefiniowana w klasie bazowej Array, a zatem jest dostępna w każdej z tablic 1 . Można sądzić, że to całkowicie wystarczy na potrzeby tak prostej czynności, jaką jest określenie wielkości tablicy, jednak ta nie musi wcale być prostą sekwencyjną listą. Może się okazać, że tworzone rozwiązanie musi operować na danych wielo­ wymiarowych, dlatego też .NET udostępnia dwa rodzaje takich tablic: nieregularne (ang. jagged arrays, nazywane także postrzępionymi) oraz prostokątne (ang. rectangular) .

1 Dostępna jest także właściwość Langlength będąca 64-bitową wersją właściwości Length, która teoretycznie pozwala na tworzenie większych tablic . Jednak aktualnie platforma .NET narzuca ograniczenie w postaci maksymalnej wielkości pojedynczej tablicy: nie może ona zajmować więcej niż 2 GB pamięci, i to nawet w pro­ cesie 64-bitowym. A zatem w praktyce w .NET 4.0 właściwość Langlength nie jest szczególnie użyteczna. (W pro­ cesach 64-bitowych można wykorzystywać znacznie więcej niż 2 GB pamięci - ta wielkość odnosi się jedynie do pojedynczej tablicy).

Tablice

I 247

Tabl ice tablic (czyl i tabl ice nieregu larne) Jak już powiedzieliśmy wcześniej, typ elementów tablic może być dowolny. Ponieważ zaś same tablice też są typem, istnieje możliwość utworzenia tablicy tablic. Załóżmy, że chcemy utworzyć listę wydarzeń zaplanowanych na kilka najbliższych dni pogrupowanych według dnia. Taką listę moglibyśmy przedstawić w formie tablicy, której poszczególne elementy odpowiadałyby konkretnym dniom, a ponieważ w każdym dniu można zaplanować więcej wydarzeń, każdy z nich także powinien być reprezentowany przez tablicę. Taką strukturę danych tworzy kod przedstawiony na listingu 7.20.

Listing 7.20. Tworzenie tablicy tablic s t at i c Cal endarEvent [] [] GetEventsByDay (Cal endarEvent [] al l Events , DateT i me fi rs tDay , i nt numberOfDays) Cal endarEvent [] [] eventsByDay = new Cal endarEvent [numberOfDays] [] ; for ( i nt day = O ; day < numberOfDays ; ++day) { DateT i me dateOfi nterest = (fi rs tDay + T i meSpan . FromDays (day) ) . Date ; Cal endarEvent [] i temsOnDateOfinteres t = Array . Fi ndA 1 1 (a 1 1 Events , e => e . StartT i me . Date == dateOfinterest) ; events ByDay [day] = i temsOnDateOf i nteres t ; return eventsByDay ;

Przeanalizujmy ten przykład fragment po fragmencie. Zacznijmy od deklaracji metody: s t at i c Cal endarEvent [] [] GetEventsByDay (Cal endarEvent [] al l Events , DateT i me fi rs tDay , i nt numberOfDays)

Typ wartości zwracanej - Ca 1 endarEvent [] [] - jest tablicą tablic, co zostało oznaczone poprzez użycie dwóch par nawiasów kwadratowych. Swoją drogą, takich poziomów zagłębienia można utworzyć dowolnie wiele - nic nie stoi na przeszkodzie, by utworzyć tablicę tablic tablic tablic pewnego typu. Argumenty naszej przykładowej metody są raczej oczywiste. Oczekuje ona, że zostanie do niej przekazana prosta tablica zawierająca nieposortowaną listę wszystkich wydarzeń. Oprócz tego musi ona wiedzieć, od którego dnia należy zacząć grupowanie wydarzeń oraz ile dni nas interesuje. Pierwszą operacją wykonywaną przez metodę jest utworzenie tablicy, która później zostanie przez nią zwrócona: Cal endarEvent [] [] events ByDay = new Cal endarEvent [numberOfDays] [] ;

Wyrażenie new Ca 1 endarEvent s [5] [] jest podobne do wyrażenia new Ca 1 endarEvents [5] , które two­ rzy pięcioelementową tablicę zawierającą obiekty Cal en darEvent, lecz w odróżnieniu od niego tworzy pięcioelementową tablicę zawierającą tablice obiektów Cal endarEven t . Ponieważ nasza metoda pozwala określić liczbę dni, z jakich zostaną pobrane wydarzenia, używamy tego argu­ mentu do określenia wielkości tablicy najwyższego poziomu. Pamiętajmy, że tablice są typami referencyjnymi oraz że za każdym razem, gdy tworzymy tablicę, której elementy są typu referencyjnego, zostaje ona początkowo wypełniona wartościami 248 I

Rozdział 7. Tablice i listy

n ul l . A zatem, choć nasza tablica even t s ByDay może odwoływać się do tablic dla poszczególnych dni, to jednak bezpośrednio po utworzeniu każdy jej element zawiera wartość n u l l . Dlatego kolejny fragment kodu zawiera pętlę, która wypełnia tę tablicę: for ( i nt day {

=

O ; day < numberOfDays ; ++day)

Pierwsza grupa instrukcji umieszczonych wewnątrz pętli jest nieco podobna do początku kodu z listingu 7 .14: DateT i me dateOfinterest = (fi rstDay + T i meSpan . FromDays (day) ) . Date ; Cal endarEvent D i temsOnDateOfi nterest = Array . Fi ndAl l (al l Even t s , e => e . StartT i me . Date == dateOfinteres t) ;

Jedyna różnica pomiędzy nimi polega na tym, że w tym przypadku podczas każdej iteracji pętli obliczamy, jaka data nas interesuje. Następnie wywołanie metody Array . F i n dAl l zwraca wszystkie wydarzenia przypadające na wyznaczony dzień. Ostatnia instrukcja wykonywana wewnątrz pętli zapisuje odnalezione wydarzenia do naszej tablicy wynikowej: eventsByDay [day] = i temsOnDateOfi nteres t ;

Po zakończeniu pętli zwracamy tablicę wynikową: return eventsByDay ;

Każdy jej element będzie zawierał tablicę wydarzeń przypadających na konkretny dzień. Kod korzystający z takiej tablicy może odwoływać się do jej elementów za pomocą standar­ dowej składni, na przykład: Consol e . Wri teli ne ( 11 Li czba wydarzeń p i erws zego dn i a : 11 + even t s ByDay [O] . Length) ;

Należy zwrócić uwagę, że w powyższym kodzie użyto zapisu zawierającego tylko jedną parę nawiasów kwadratowych. Oznacza on, że chcemy pobrać jedną z tablic z naszej tablicy najwyż­ szego poziomu (będącej tablicą tablic) . W tym przypadku interesuje nas wielkość pierwszej spośród tych tablic. Możemy jednak zejść o poziom niżej i zastosować dwa indeksy: Consol e . Wri teli ne ( 11 Dz i eii p i erws zy , wydarzen i e drug i e : 11 + eventsByDay [O] [1] . T i t l e) ;

Ten zapis, wykorzystujący kilka par nawiasów kwadratowych, dokładnie odpowiada składni służącej do deklarowania i tworzenia tablic wielowymiarowych. Ale dlaczego taka tablica tablic jest nazywana nieregularną (ang. jagged array)? Rysunek 7.4 przedstawia postać danych, którą uzyskalibyśmy, przekazując do metody z listingu 7.20 tablicę wydarzeń zdefiniowaną na listingu 7.10 i żądając przetworzenia wydarzeń z pięciu dni, począw­ szy od 11 lipca 2009 roku. Na rysunku każda z podrzędnych tablic została przedstawiona jako jeden wiersz. Poszczególne wiersze, co wyraźnie widać, mają różną długość: pierwsze dwa zawierają po dwa wydarzenia, trzeci tylko jedno, a dwa ostatnie są puste (czyli zawierają tablice o zerowej długości) . A zatem nasza struktura danych nie przypomina ładnego prostokąta obiek­ tów, lecz raczej jakiś nieregularny kształt. Zależnie od efektów, jakie staramy się uzyskać, ta nieregularność może być zarówno zaletą, jak i wadą. W naszym przypadku jest ona pomocna - wykorzystaliśmy ją do zapewnienia tego, by liczba wydarzeń każdego dnia mogła być inna, a w skrajnym wypadku, by danego dnia mogło nie być żadnych wydarzeń. Jeśli jednak operujemy na danych, które naturalnie pasują

Tablice

I 249

· · - ·· · ·

a: C=:J _ J- „.1 . . . .. . . . . ..



r

..

.

. .„. . .

+

a„.11„„

· · Ca l encta r E v ent [ J r ·,,,_ ······ l

• •• • •

„ .„„

... .

:

••• • •

)

. „•. .

.• . . . . . . . ., .„ .• • •.

• „• • •

• • •• • • • „

r

4 godziny

,

.... . . .

....

1 . Ca l enda rEvent[J . . ..

„I

„ :1 „ +

... .



ł I

„„ 1„ „ .. .._ _. , -

-... r



....

.

. .„• • . „• • . . • •

,i

Sobotnia noc

swingowa



:



19: 3 0 11 l ipca 2009

••• • . • ••• „ • . • . • „

Formuła 1 - Grand P lix Niemiec 12:10 12 Hpca 2009 3 godziny

r

....

·



. . „ ., •• . • • ••• ••

..... ........ �

1------11

.. . . . „ . . . „ • • . „.„ . • •

Swingowa pota1ńców ka na nabrzeżu 15:00 11 l i pca 2009

Ca l endarE vent [ J

.

...

6,5 godziny

•••• „ ••• „ „ •..



Piknik swin gowy 1 5:00 12 lipca 2009

4 godziny

-...



;

•• „„ „

...

Łamańce swing owe w klubie Set ka 19:45 n l 1ipca 2009 5 godzin

r

....

.

. , ••• ,. . . .

Ca l enda rEvent [ J I

(pusta)

„ •••„ ••••. „ ..,.,______,,

Ca l enda rEvent [ J

(pusta)

Rysunek 7.4. Tablica nieregularna do prostokątnej struktury (takich jak piksele obrazka), to wiersze o różnej długości oznaczają jakiś błąd - w takim przypadku znacznie lepsza byłaby struktura danych niedająca takich możliwości, dzięki czemu nie musielibyśmy zastanawiać się, jak obsługiwać takie błędy. Tablice nieregularne mogą być całkiem złożonymi strukturami danych - w końcu składają się one z wielu obiektów (co widać na rysunku 7.4) . Każda z tablic jest obiektem zupełnie niezależnym od obiektów, do których odwołują się jej elementy. W naszym przykładzie uzy­ skaliśmy więc w efekcie 1 1 obiektów: pięć wydarzeń, pięć tablic reprezentujących kolejne dni (w tym dwie puste) oraz dodatkową tablicę najwyższego poziomu zawierającą pięć pozosta­ łych. W sytuacjach, gdy taka elastyczność nie jest niezbędna, można zastosować inne, prostsze rozwiązanie pozwalające na utworzenie tablicy wielowymiarowej: tablicę prostokątną.

250 I

Rozdział 7. Tablice i listy

Tabl ice prostokątne Tablice prostokątne2 pozwalają na przechowywanie wielowymiarowych danych w jednej tablicy, a nie w tablicy tablic. Mają one bardziej regularną postać niż przedstawione wcześniej tablice nieregularne - w przypadku dwuwymiarowej tablicy prostokątnej każdy wiersz ma taką samą długość. '

. .

Warto zauważyć, że możliwości tablic prostokątnych nie ograniczają się do dwóch wymiarów. Analogicznie do tablic zawierających tablice tablic, można tworzyć tablice „prostokątne" o dowolnej liczbie wymiarów, choć wówczas nazwa „prostokątne" zaczyna brzmieć nieco dziwnie. W przypadku trzech wymiarów tablicę należałoby raczej określić jako sześcienną, a nie prostokątną; ogólnie rzecz ujmując, kształt takich tablic jest zawsze hiperprostokątem (ang. orthotope) . Zapewne projektanci C# i plat­ formy .NET uznali, że ta prawidłowa nazwa j est zbyt tajemnicza (nie zna jej nawet angielski słownik ortograficzny), a słowo prostokqtne jest bardziej użyteczne i opisowe, choć może niezbyt poprawne z technicznego punktu widzenia. Jak widać, pragma­ tyzm zyskał tu przewagę nad pedantycznością, gdyż C# jest językiem o praktycznym charakterze.

Tablice prostokątne służą zazwyczaj do rozwiązywania nieco innych problemów niż tablice nieregularne, dlatego opisując je, będziemy musieli posłużyć się innym przykładem. Załóżmy, że tworzymy prostą grę, w której bohater przechodzi przez labirynt. Jednak zamiast typowego nowoczesnego trójwymiarowego labiryntu przedstawianego z punktu widzenia bohatera wyobraźmy sobie grę o charakterze retro, w której labirynt jest widziany z góry, a jego ściany i krawędzie doskonale pasują do prostokątnej siatki. Jeśli Czytelnik jest zbyt młody, by pamiętać takie gry, to rysunek 7.5 daje wyobrażenie o tym, co zmieniło się w świecie elektronicznej rozrywki od czasu, gdy autorzy książki uczęszczali do szkoły.

Rysunek 7.5. Gra w stylu retro

-

3D jest dla mięczaków

Nie będziemy się szczególnie zagłębiać w same szczegóły rozgrywki - załóżmy jedynie, że nasz kod musi wiedzieć, gdzie są ściany, by określić, czy bohater może się przesunąć na następne pole oraz czy może strzelić do złych stworów goniących go po labiryncie . Takie informacje można by przedstawić w formie tablicy liczb, w której O reprezentowałoby pustą przestrzeń,

2 Tablice prostokątne są także czasami nazywane wielowymiarowymi, jednak ta nazwa może być nieco myląca,

gdyż także tablice nieregularne przechowują dane wielowymiarowe.

Tablice

251

natomiast 1 ścianę labiryntu (co pokazano na listingu 7.21) . (Równie dobrze zamiast liczb typu i nt można by zastosować wartości logiczne - boa l - gdyż wchodzą w grę tylko dwie opcje: ściana lub jej brak. Użycie wartości logicznych sprawiłoby jednak, że jeden wiersz kodu nie mie­ ściłby się w jednym wierszu tekstu w książce, a to znacznie by utrudniło zrozumienie, jak kod z listingu 7.21 odpowiada rysunkowi 7.5. Co więcej, zastosowanie liczb zostawia furtkę dla dodania do gry nowych fascynujących możliwości takich jak otwierane drzwi, obszary natych­ miastowej śmierci itd.) .

Listing 7.2 1 . Wielowymiarowa tablica prostokątna i nt [ , ] wal l s = { 1, 1, 1, o, o, 1 , 1 , o, 1 , 1 , o, o, 1 , o, 1 , 1 , o, 1 , 1 , o, 1 , 1 , o, 1 , 1 , o, 1 , 1 , o, 1 , 1 , o, o, 1, 1, 1, };

new i nt [ , ] 1, 1, 1, 1, 1, 1, 1, 1, 1 1 1, 1 , 1 , 1, 1, 1, 1, o, 1 o, o , o , o, 1 , o, o, o, 1 1, 1 , 1 , o, 1, o, 1, o, 1 o, o , o , o, o, o, 1 , o, o o, 1 , 1 , 1, 1, 1, 1, 1, 1 o, o , o , 1 , o, o, o, o, 1 o , 1 , o, 1 , o, o, 1 , o, 1 o , 1 , o, 1 , o, 1 , 1 , 1 , 1 o, 1, o, o, o, o, o, o, 1 1, 1, 1, 1, 1, 1, 1, 1, 1 o, o , o , o, o, o, o, o,

}, }, }, }' }, }, }, }, }, }, }, }

Pomiędzy tym a poprzednim przykładem występuje kilka różnic. Przede wszystkim należy zauważyć, że w deklaracji typu tablicy wewnątrz nawiasów kwadratowych został umieszczony przecinek. Liczba przecinków określa liczbę wymiarów tablicy - w przypadku braku prze­ cinka zostanie utworzona tablica jednowymiarowa (czyli taka, jakiej używaliśmy do tej pory), natomiast w razie użycia jednego przecinka utworzona zostanie tablica dwuwymiarowa. Tablicę o układzie sześciennym należałoby zadeklarować jako i n t [ „ ] . Analogicznie można by doda­ wać kolejne wymiary. Oprócz tego należy zwrócić uwagę, że w liście inicjalizatorów tablicy, podczas tworzenia jej poszczególnych wierszy, nie był używany operator new. W powyższym przykładzie pojawił się on tylko raz, co odzwierciedla fakt, że tworzona tablica jest jednym obiektem, choć wielowy­ miarowym. Jak pokazuje rysunek 7.6, takie tablice mają znacznie prostszą strukturę niż tablice nieregularne przedstawione na rysunku 7.4. '

.'

'---�

,

Choć rysunek 7 .6 precyzyjnie odzwierciedla fakt, że wszystkie dane tablicy prosto­ kątnej są przechowywane w jednym obiekcie, to jednak układ prostokątnej siatki nie jest dokładnym odpowiednikiem sposobu przechowywania zawartości takiej tablicy w pamięci, tak samo jak położenie obiektów na rysunku 7.4 jest jedynie umownym sposobem przedstawienia tego, co byśmy zobaczyli, zaglądając do środka układu scalo­ nego pamięci RAM komputera przy użyciu elektronowego mikroskopu skaningowego. W rzeczywistości zawartość tablic wielowymiarowych jest przechowywana jako lista sekwencyjna - dokładnie tak samo jak w przypadku prostej tablicy przedstawionej na rysunku 7.4 - gdyż sama pamięć komputera jest jedynie sekwencyjnym zbiorem komórek. A zatem to programistyczny model języka C# sprawia, że tablica wydaje się być trójwymiarową.

252 I

Rozdział 7. Tablice i listy

i nt [ . ]

11I I I 11I 1I I 1 11I o

i

l

11I I i

1 l

i

o

o

l

1

1

o

o

o

l

o

o

o o

o

o l

1

1

1 l

I

o l

i

o i

o

I I I

I

l

l

1

o

o

o

o

o

o

o

1

o

o

l

1

o

o

o

I

D

o

l

o

o

o o

i

I

I I

I

I

l

o

1

l

o

1

o

I o I i

1

1

1 l

1 1

l

1

o 1

o

1

o

o

o l

l 1

1 l

1

o

o

o

o

o

1

o

o o

1

1

o l

o l

I

1 I

I

o l

I

o I

11 I

1 l

o o

1

o

l

1

1

o

o o

I 11 11 o

o

1

o l

l 1

1

l

Rysunek 7.6. Dwuwymiarowa tablica prostokątna

Składnia odwołań do elementów tablicy prostokątnej jest nieco inna niż dla elementów tablic nieregularnych. W obu przypadkach jest ona jednak spójna ze składnią deklaracji, co pokazuje listing 7.22, na którym używamy jednej pary nawiasów kwadratowych oraz indeksów dla każ­ dego z wymiarów tablicy oddzielonych od siebie przecinkami. Listing 7.22 . Odwołania do elementów tablicy prostokątnej s t at i c bool CanCharacterMoveDown ( i nt x , i nt { i nt newY = y + l ;

y,

i nt [ ,] wal l s )

li Nie można wyjść poza dolnq krawędź mapy. i f (newY == wal l s . Get length (O) ) { return fal s e ; li Można się przesunqć, wylqcznie jeśli po drodze nie ma ściany. return wal l s [newY , x] == O ;

'

. .

.....___�

:

Jeśli w odwołaniu do tablicy prostokątnej zostanie podana niewłaściwa liczba indeksów, kompilator C# zgłosi błąd . Liczba wymiarów tablicy (oficjalnie nazywana stopniem, ang. rank) jest uważana za jeden z aspektów typu i nt [ , ] jest innym typem niż i nt [ , , ] a C# sprawdza, czy podana liczba indeksów odpowiada stopniowi tablicy. -

-

Metoda przedstawiona na listingu 7.22 wykonuje dwa testy: zanim sprawdzi, czy na drodze postaci znajduje się jakaś ściana, sprawdza, czy znajduje się ona na krawędzi mapy. Do tego celu konieczna jest znajomość wielkości mapy i zamiast przyjmować jakieś z góry ustalone wymiary, metoda odczytuje tę informację z tablicy. W tym przypadku nie jest jednak możliwe skorzystanie z właściwości Length (którą poznaliśmy już wcześniej), gdyż ta określa sumaryczną liczbę znajdujących się w tablicy elementów. Ponieważ nasza tablica ma wymiary 12x 12, wła­ ściwość Length będzie miała wartość 144. Jednak nas interesuje wysokość tablicy. Dlatego też skorzystamy z metody Get lengt h, która pobiera jeden argument określający, jaki wymiar tablicy nas interesuje O oznacza wysokość tablicy (czyli jej wymiar w pionie), a 1 jej szerokość (wymiar w poziomie) . -

Tablice

I 253

,

. •

.„

,_„�; , .___�.·

Tak naprawdę do tablic nie mają zastosowania pojęcia szerokości i wysokości. Tablice mają tyle wymiarów, ilu zażądamy, i to wyłącznie od naszego programu zależy, w jaki sposób każdy z nich będzie używany. W naszym przykładowym programie zdecydowaliśmy, że pierwszy wymiar będzie określał położenie postaci w labiryncie względem osi pionowej, natomiast drugi - względem osi poziomej .

Ten tym przykładzie zastosowaliśmy dwuwymiarową tablicę liczb całkowitych, a ponieważ i nt jest typem wartościowym, poszczególne wartości istnieją wewnątrz tablicy. Można także two­ rzyć wielowymiarowe tablice prostokątne, których elementy są typu referencyjnego. W takim przypadku także będziemy dysponować pojedynczym obiektem zawierającym wszystkie elementy tablicy we wszystkich wymiarach, jednak poszczególne elementy zostaną zainicjo­ wane wartością n u l l - a zatem dokładnie tak samo, jak miało to miejsce w przypadku tablic jednowymiarowych, także i wtedy konieczne będzie utworzenie obiektów, do których elementy tablicy będą się odwoływać. Choć zarówno tablice nieregularne, jak i wielowymiarowe tablice prostokątne zapewniają nam dużą elastyczność, jeśli chodzi o sposób określania ich wielkości, to nie rozwiązaliśmy jeszcze interesującego problemu związanego z wielkością tablic, o którym wspominaliśmy już na początku tego rozdziału. Chodzi o fakt, że wielkość tablicy jest stała i niezmienna. Jak mogli­ śmy się już przekonać, problem ten można ominąć, tworząc nową tablicę i kopiując do niej wybrane elementy starej bądź używając do zmiany rozmiaru tablicy metody Array . Res i z e . Jednak wszystkie te rozwiązania s ą mniej lub bardziej niewygodne, dlatego też w praktyce w języku C# rzadko pracujemy bezpośrednio na tablicach. Istnieje znacznie prostszy sposób obsługiwania kolekcji o zmiennej wielkości - jest nim klasa L i s t .

List Klasę L i st zdefiniowaną w przestrzeni nazw System . Col l ect i on s . Generi cs należałoby w zasa­ dzie określić jako tablicę o zmiennej wielkości . Precyzyjnie rzecz ujmując, jest to klasa ogólna wchodząca w skład biblioteki klas .NET Framework i, w odróżnieniu od tablic, nie jest ona traktowana w żaden szczególny sposób ani przez system, ani przez CLR. Jednak z punktu widzenia programisty używającego C# obiekty tej klasy zachowują się bardzo podobnie do tablic - można z nimi robić praktycznie to samo co z tablicami, a nie podlegają niewygodnym ograniczeniom wielkości . '

. .

,

.___�_,'

Można się zastanawiać, dlaczego platforma .NET udostępnia tablice, skoro klasa Li st wydaje się być bardziej przydatna i wygodna. Otóż nie dałoby się utworzyć tej klasy, gdyby nie tablice: używa ich ona bowiem do przechowywania danych. Wraz z dodawaniem coraz to nowych danych do listy klasa ta tworzy nowe, coraz to większe tablice i kopiuje do nich całą wcześniejszą zawartość. Wykorzystywane są przy tym różne sztuczki, by takie operacje były wykonywane jak najrzadziej .

Klasa L i s t jest jednym z najbardziej użytecznych typów dostępnych w bibliotece klas .NET Framework. Jeśli operujemy na wielu elementach danych, co w programach zdarza się nader często, to raczej powszechna jest potrzeba posiadania pewnej elastyczności co do liczby tych elementów - listy o stałej wielkości są tu bardziej wyjątkiem niż regułą. (Na przykład zawartość osobistego kalendarza będzie się zazwyczaj zmieniać wraz z upływem czasu) . Czy

254 I

Rozdział 7. Tablice i listy

Typy ogólne Li st jest przykładem tak zwanego typu ogólnego (ang. generic) . Takich typów nie używa się bez­ pośrednio, lecz korzysta się z nich w celu tworzenia nowych typów. Na przykład Li s t repre­ zentuje listę liczb całkowitych, a Li st - listę łańcuchów znaków. Są to dwa odrębne typy utworzone poprzez przekazanie do Li st odmiennych argumentów typu. Podanie argumentu typu w celu utworzenia nowego typu jest nazywane tworzeniem instancji typu ogólnego. Typy ogólne zostały dodane do języka C# w jego wersji 2.0 głównie w celu obsługi typów kolekcji, właśnie takich jak Li st. Wcześniej do identycznych celów trzeba było używać klasy ArrayL i st (z której aktualnie nie należy już korzystać - nie jest ona dostępna w technologii Silverlight, a może się okazać, że już niebawem jej stosowanie nie będzie zalecane na całej platformie .NET). Klasa ArrayL i st także implementowała tablice o zmiennej długości, jednak jej elementy zawsze były typu obj ect. Oznaczało to, że można było w nich zapisywać niemal wszystko, lecz zawsze podczas odczytu elementów takiej kolekcji trzeba było j e rzutować na odpowiedni, oczekiwany typ, a to mogło wprowadzać zamieszanie. W przypadku typów ogólnych można napisać kod, w którym stosowane będą nazwy zastępcze typów - jak na przykład T w typie Li st. Są to tak zwane parametry typów (ang. type parameters) . Rozróżnienie pomiędzy parametrem i argumentem jest tutaj takie samo jak w przypadku metod: parametr to swoista nazwa zastępcza, natomiast argument to faktyczna wartość lub typ podawany w miejscu tej nazwy zastępczej w momencie używania kodu. Można zatem napisać następujący fragment kodu: publ i c cl ass Wrapper { publ i c Wrapper(T v) { Val ue = v ; } publ i c T Val ue { get ; pri vate set; } Powyższy kod nie musi wiedzieć, czym tak naprawdę będzie T - w rzeczywistości może to być dowolny typ . Gdybyśmy chcieli umieścić w takim obiekcie wartość typu i nt, to użylibyśmy typu Wrapper, co spowodowałoby utworzenie klasy dokładnie takiej samej jak przedstawiona powyżej, w której parametr T zostałby zastąpiony przez i nt. Niektóre klasy posiadają więcej parametrów typu. Klasy słowników (opisane w rozdziale 9.) wyma­ gają odrębnego określenia typu dla kluczy oraz dla wartości. A zatem tworząc taki słownik, można by użyć na przykład takiego zapisu: Di cti onary. Utworzona instancja typu ogólnego jest pełnoprawnym typem, więc można by ją zastosować na przykład jako argument typu w innym typie ogólnym; oto przykład: Di cti onary zawie­ rający listę liczb określających ilość wierszy w każdym z plików. Być może Czytelnik zastanawia się, jak dokładnie to wszystko działa. Kod tworzący wyrażenie zapytania LINQ wygląda całkowicie odmiennie od standardowego kodu C# - z założenia przypomina on nieco kod poleceń służących do przeszukiwania baz danych . Okazuje się jednak, że cała ta składnia jest przekształcana na proste wywołania metod.

Wyrażenia zapytań a wywołan ia metod Specyfikacja języka C# definiuje proces, dzięki któremu wszystkie wyrażenia zapytań LINQ zostają zastąpione wywołaniami metod. Listing 8 .3 pokazuje, w co zostaje przekształcone wyrażenie zapytania, którego użyliśmy na listingu 8.2. Jak się okazuje, C# ignoruje białe znaki pomiędzy poszczególnymi elementami wyrażenia zapytania, dzięki czemu fakt, że zostało ono zapisane w kilku wierszach, by zmieścić je na stronie, nie wpłynie na możliwość jego kompilacji.

Listing 8.3. Zapytanie LINQ jako wywołania metod var b i g F i l es = GetAl l Fi l es i nDi rectory (@ " c : \ " ) . Where (fi l e => new Fi l e l nfo (fi l e) . Length > 10000000) ;

Porównajmy te wywołania z komponentami oryginalnego zapytania: var b i g F i l es = from fi l e i n GetAl l Fi l es in D i rectory (@ " c : \ " ) where new Fi l e l nfo (fi l e) . Length > 10000000 sel ect fi l e ;

Wyrażenia zapytań

I

277

Źródło, które jest określane w wyrażeniu zapytania za słowem kluczowym i n, staje się punk­ tem początkowym sekwencji wywołań. W naszym przypadku jest nim enumeracja zwracana przez metodę GetA 1 1 F i l es I n Di rectory . Kolejny etap jest określany zależnie od obecności klauzuli where - jeśli została ona użyta w wyrażeniu zapytania, to zostanie przekształcona w wywołanie metody Where operujące na źródłowej enumeracji. Jak widać na przykładzie, wyrażenie logiczne podane w tej klauzuli zostaje przekształcone w wyrażenie lambda i przekazane do metody Where jako argument jej wywołania. Umieszczona na samym końcu wyrażenia zapytania klauzula sel ect . . . nie jest w ogóle prze­ kształcana . Wynika to z faktu, że ma ona bardzo prostą postać i ogranicza się do zwrócenia zmiennej zakresu, a w takim przypadku nie ma żadnej potrzeby dodatkowego przetwarzania informacji przekazywanych z metody Where. Jednak gdyby w klauzuli sel ect zostało zastoso­ wane jakieś ciekawsze wyrażenie takie jak to: var b i g F i l es

=

from fi l e i n GetAl l Fi l es i nDi rectory (@ " c : \ " ) where new Fi l e l nfo (fi l e) . Length > 10000000 sel ect " Pl i k : " + fi l e ;

to w sekwencji wywołań pojawiłoby się dodatkowe wywołanie metody S e l ect przedstawione na listingu 8.4.

Listing 8.4. Where oraz Select jako metody var b i g F i l es = GetAl l Fi l es i nDi rectory (@ " c : \ " ) . Where (fi l e => new Fi l e l n fo (fi l e) . Length > 10000000) . Sel ect (fi l e => " Pl i k : " + fi l e) ;

Pozostaje jednak pewne pytanie: skąd wzięły się te metody Wh ere oraz Se l e c t . Metoda GetA 1 1 "+Fi l es i nDi rectory zwraca wynik typu I En umerabl e, a jeśli dokładnie zbadamy ten inter­ fejs (przedstawiony w poprzednim rozdziale), okaże się, że nie definiuje on żadnej metody Where. Pomimo tego jeśli spróbujemy skompilować kod korzystający z metod odpowiadających naszemu wyrażeniu zapytania, to nie będzie z tym najmniejszego problemu, o ile tylko na początku pliku źródłowego pojawi się dyrektywa us i ng Sys tern . L i nq, a w samym projekcie - odwołanie do biblioteki Sys t em . Core. Co się zatem dzieje? Otóż okazuje się, że Where oraz Sel ect są przy­ kładami metod rozszerzeń.

Metody rozszerzeń a LI NQ Jedną z możliwości języka dodanych do C# w wersji 3.0 z myślą o umożliwieniu stworzenia technologii LINQ są metody rozszerze1i (ang. extension methods) . Są to metody dodawane do określonego typu przez jakiś inny typ. Okazuje się więc, że do istniejących typów można doda­ wać nowe metody, i to nawet jeśli samego typu nie można przy tym zmienić, chocby dlatego, że jest to jeden z wbudowanych typów .NET Framework. Na przykład wbudowany typ stri ng nie jest czymś, co moglibyśmy zmodyfikować. Jest to klasa ostateczna, a zatem nie możemy utwo­ rzyć jej klas pochodnych. Nie oznacza to jednak, że nie możemy dodawać do niego nowych metod. Przykład przedstawiony na listingu 8.5 tworzy nową, niezbyt przydatną metodę, która zwraca kopię łańcucha zapisaną w odwrotnej kolejności 1 . 1 Ta metoda jest jeszcze mniej użyteczna, niż początkowo mogłoby się wydawać. Jeśli posiadany łańcuch będzie zawierał znaki, które muszą być zapisane w ściśle określonej kolejności - takie jak sekwencje lub znaki zastęp­ cze - to naiwne odwrócenie ich porządku może mieć dziwne skutki. Niemniej jednak w tym przykładzie najważniejsze jest pokazanie możliwości dodawania nowych metod do istniejących typów, a nie wyjaśnienie, dlaczego odwrócenie kolejności znaków w łańcuchu zapisanym w Unicode jest zaskakująco trudne.

278

I

Rozdział 8. LINQ

Listing 8.5. Dodawanie metody rozszerzenia do typu string s t at i c cl ass S t r i n gAddi t i ons { li Bardzo naiwne rozwiązanie slużqce wylqcznie do celów demonstracyjnych. li ABSOLUTNIE NIE NALEŻY używać go w rzeczywistym kodzie! publ i c s t at i c s tr i ng Backwards (th i s s t r i ng i nput) { char [] characters = i nput . ToCharArray () ; Array . Reverse (characters ) ; return new stri ng (characters ) ;

Koniecznie należy zwrócić uwagę na słowo kluczowe t h i s umieszczone przed pierwszym parametrem metody. To właśnie ono oznacza, że Bac kwards jest metodą rozszerzenia. Dodatkowo warto zauważyć, że klasę, w której została ona zdefiniowana, oznaczono jako statyczną ( stat i c) . Metody rozszerzeń można bowiem definiować wyłącznie w klasach statycznych. O ile tylko powyższa klasa będzie się znajdowała w naszym zakresie (czyli w tej samej prze­ strzeni nazw, w której jest umieszczony kod, lub też w przestrzeni dodanej przy użyciu dyrek­ tywy us i ng ) , to metodę Backward s będzie można wywoływać tak, jakby była zwyczajną metodą klasy stri ng: s t r i ng stati onName = " Park Łaz i en kows ki " ; Consol e . Wri teli ne ( s tati onName . Backwards () ) ;

Metody Where oraz Sel ect przedstawione na listingu 8 .4 są metodami rozszerzeń . Przestrzeń nazw System . Li nq definiuje statyczną klasę o nazwie Enumerabl e, która zawiera te dwie oraz wiele innych metod rozszerzeń dla interfejsu I Enumerab l e. Oto sygnatura jednej z kilku przeciążo­ nych wersji metody Where: publ i c s t at i c I Enumerabl e Where ( th i s I Enumerabl e source , Func predi cate)

Proszę zwrócić uwagę, że jest to metoda ogólna - przyjmuje ona argument typu, noszący w tym przykładzie nazwę TSource, i przekazuje go dalej jako argument typu T swojego pierwszego parametru I Enumerab l e. W efekcie metoda ta rozszerza interfejs I En umerab l e niezależnie od tego, jakim typem będzie T . Innymi słowy, o ile tylko przestrzeń nazw Sys t em . L i n q będzie się znajdować w naszym zakresie, to wszystkie implementacje I En umerabl e będą dysponowały metodą Where. Se l ect oraz Where są przykładami operatorów LINQ - standardowych metod dostępnych wszę­ dzie tam, gdzie LINQ jest obsługiwany. Klasa En umerab l e zdefiniowana w przestrzeni nazw System . L i ną dostarcza wszystkich operatorów LINQ dla implementacji interfejsu I En umerabl e, jednak nie jest to jedyny dostawca LINQ - zapewnia ona jedynie możliwość przeszukiwania kolekcji przechowywanych w pamięci i czasami jest określana jako LINQ to Objects. W kolej­ nych rozdziałach książki poznamy źródła obsługujące zapytania LINQ operujące na bazach danych oraz dokumentach XML. Każdy może tworzyć nowych dostawców LINQ, gdyż C# ani nie wie, ani nie zwraca uwagi na to, jakie jest źródło danych i jak ono działa . Język po prostu mechanicznie tłumaczy wyrażenia zapytań na wywołania metod i o ile tylko odpowiednie operatory LINQ są dostępne, będą one używane. Dzięki temu różne źródła danych mogą imple­ mentować różne operatory LINQ w dowolny sposób, jaki jest dla nich stosowny. Listing 8.6 pokazuje, jak można skorzystać z tej możliwości, by utworzyć własne implementacje operatorów Sel ect oraz Where.

Wyrażenia zapytań

I 279

Listing 8.6. Własne implementacje operatorów LINQ publ i c cl ass Foo { publ i c s t r i ng N ame { get ; set ; } publ i c Foo Where ( Func< Foo , bool > pred i cate) { return t h i s ; publ i c TRes u l t Sel ect ( Func sel ector) { return sel ector (th i s) ;

To zwyczajne metody, a nie metody rozszerzeń - tworzymy własny typ, więc możemy zde­ finiować operatory LINQ bezpośrednio w nim. Ponieważ C# po prostu konwertuje zapytania LINQ na wywołania metod, to, czy będą to zwyczajne metody, czy też metody rozszerzeń, nie ma znaczenia. Dysponując dwiema powyższymi metodami, możemy już napisać kod przedsta­ wiony na listingu 8.7.

Listing 8.7. Nieco mylące, lecz z technicznego punktu widzenia akceptowalne zastosowanie zapytari LINQ Foo source = new Foo { Name = " Ferde k " } ; var res u l t = from f i n source where f . Name == " Ferde k " se l e c t f . Name ;

Teraz C# zastosuje reguły przekształcania wyrażeń zapytań na wywołania metod, jak to robi we wszystkich zapytaniach, a zatem zamieni zapytanie z listingu 8 .7 na następującą sekwencję wywołań: Foo source = new Foo { Name = " Ferdek " } ; var res u l t = source . Where (f => f . Name == " Ferde k " ) . Sel ect (f => f . Name) ;

Ponieważ klasa Foo implementuje oczekiwane przez C# operatory Where i Sel ect, powyższy kod będzie można skompilować i uruchomić. Nie będzie on jednak szczególnie użyteczny, gdyż nasza implementacja metody Where całkowicie ignoruje przekazywany do niej predykat. Co więcej, nasze rozwiązanie jest nieco dziwne, gdyż klasa Foo nie wydaje się reprezentować jakiej­ kolwiek kolekcji. Zastosowanie w odniesieniu do niej składni przeznaczonej do użycia z kolek­ cjami jest więc raczej mylące. W rzeczywistości przykład z listingu 8.7 daje takie same rezultaty co instrukcja: var res u l t = source . Name ;

A zatem w praktyce nigdy nie pisalibyśmy kodu z listingów 8.6 i 8.7 dla klasy tak prostej jak Foo. Celem tych przykładów było pokazanie, że kompilator C# posłusznie przekształca wyrażenia zapytań na wywołania metod, nie próbując ich zrozumieć i nie mając żadnych oczekiwań odnośnie do ich przeznaczenia i działania. Prawdziwa funkcjonalność LINQ leży całkowicie po stronie biblioteki klas . Wyrażenia zapytań są jedynie wygodną formą zapisu.

Klauzule let Wyrażenia zapytań mogą zawierać klauzule l et . Są one o tyle interesujące, że w odróżnieniu od większości innych komponentów zapytań nie odpowiadają bezpośrednio żadnemu konkret­ nemu operatorowi LINQ. Ich przeznaczeniem jest jedynie ułatwienie tworzenia zapytań. 280 I

Rozdział 8. LINQ

Klauzule te stosuje się w sytuacjach, gdy te same informacje muszą zostać użyte w kilku róż­ nych miejscach zapytania. Załóżmy, że musimy zmodyfikować zapytanie z listingu 8 .2 w taki sposób, by zwracało ono obiekty F i l e l n fo, a nie nazwy plików. Można to zrobić następująco: var b i g F i l es

=

from fi l e i n GetAl l Fi l es in D i rectory (@ " c : \ " ) where new Fi l e l nfo (fi l e) . Length > 10000000 sel ect new Fi l e l nfo (fi l e) ;

Jednak takie rozwiązanie zawiera powtórzenie . Obiekt F i l e l n fo jest tworzony dwukrotnie: po raz pierwszy w klauzuli where, a następnie w klauzuli sel ect. Zastosowanie klauzuli l et pozwala uniknąć tej redundancji: var b i g F i l es = from fi l e i n GetAl l Fi l es in D i rectory (@ " c : \ " ) l et i n fo = new Fi l e l nfo (fi l e)

where i nfo . Length > 10000000 sel ect i nfo ;

Umożliwienie wykorzystania klauzul l et wymaga od kompilatora C# znacznego wysiłku. Nie trzeba zagłębiać się we wszystkie szczegóły techniczne, by móc je stosować, niemniej jed­ nak jeśli Czytelnik jest ciekaw, jak one działają, to poniżej pokrótce to opisaliśmy. Otóż w nie­ widoczny sposób kompilator tworzy klasę zawierającą dwie właściwości - fi l e oraz i n fo i w efekcie generuje dwa zapytania: var temp = from fi l e i n GetAl l Fi l es i n D i rectory (@ " c : \ " ) sel ect new Comp i l erGeneratedType (fi l e , new Fi l e l n fo (fi l e) ) ; var b i g F i l es = from i tem i n temp where i tem . i nfo . Length > 10000000 sel ect i tem . i nfo ;

Pierwsze zapytanie ma przygotować sekwencję, w której zmienna zakresu zostanie umiesz­ czona w typie wygenerowanym przez kompilator wraz ze wszystkimi innymi zmiennymi zadeklarowanymi w klauzuli l et . (Oczywiście w rzeczywistości nie nosi ona nazwy Compi l er "+GeneratedType2 - kompilator generuje w tym miejscu jakąś unikalną, pozbawioną sensu nazwę) . Dzięki temu wszystkie te zmienne będą dostępne we wszystkich klauzulach zapytania.

Koncepcje i tech niki LI NQ Zanim zaczniemy dokładnie prezentować wszystkie usługi udostępniane przez LINQ, należy przedstawić kilka możliwości dostępnych we wszystkich zastosowaniach LINQ, o których Czytelnik powinien się dowiedzieć.

Delegacje i wyrażenia lambda Składnia zapytań LINQ w niejawny sposób wykorzystuje wyrażenia lambda. Wyrażenia sto­ sowane w klauzulach where, sel ect oraz wielu innych są zapisywane jako zwyczajne wyrażenia, jednak, jak już widzieliśmy, kompilator C# przekształca te klauzule na sekwencje wywołań metod, a same wyrażenia - na wyrażenia lambda . W większości przypadków można po prostu napisać niezbędne wyrażenie i będzie ono działać. Trzeba jednak przy tym zwracać uwagę na kod, który może wywoływać efekty uboczne . Na przykład zastosowanie kodu przedstawionego na listingu 8.8 byłoby złym pomysłem. 2 Typ wygenerowany przez kompilator

-

przyp. tłum.

Koncepcje i techniki LINQ

I

281

Listing 8.8. Nieprzydatne efekty uboczne w zapytaniu i nt X = 10000 ; var b i g F i l es = from fi l e i n GetAl l Fi l es in D i rectory (@ " c : \ " ) where new Fi l e l n fo (fi l e) . Length > x++

sel ect fi l e ;

W tym przypadku klauzula where inkrementuje zmienną zadeklarowaną poza zakresem za­ pytania.

'---�.

,'

Takie rozwiązanie jest dopuszczalne (choć jest bardzo złym pomysłem) w LINQ to Objects, jednak inni dostawcy LINQ - tacy jak ci, których będziemy używali w ope­ racjach na bazach danych - mogą w przypadku napotkania takiego wyrażenia zgłosić błędy podczas działania programu.

Rozwiązanie to będzie dawać potencjalnie zaskakujące efekty, gdyż zapytanie może zwracać za każdym razem inne wyniki, choć zbiór danych, na jakich operuje, nie zmienia się. Trzeba pamiętać, że wyrażenie zastosowane w klauzuli where zostaje przekonwertowane na metodę anonimową, która będzie wywoływana jeden raz dla każdego elementu dostępnego w źródle danych . Podczas pierwszego wywołania zapytania zmienna lokalna x będzie inkremento­ wana dla każdego pliku na dysku twardym. Jeśli zapytanie zostanie wykonane powtórnie, zmienna x ponownie będzie inkrementowana, jednak nic nie przywróci jej wartości do stanu początkowego. Co więcej, czasami może się zdarzyć, że zapytania będą wykonywane nieco później niż w miej­ scu ich utworzenia, co sprawia, że taki kod zawierający efekty uboczne może być bardzo trudny do analizy. Patrząc na przykład z listingu 8.8, nie można dokładnie powiedzieć, kiedy zmienna x zostanie zmodyfikowana . Aby to określić, konieczna byłaby bardziej precyzyjna znajomość kontekstu, a konkretnie posiadanie informacji, kiedy zostanie przetworzone zapytanie bi g F i l es i ile razy. W praktyce bardzo duże znaczenie ma unikanie tworzenia takich zapytań z efektami ubocz­ nymi. Oczywiście nie ogranicza się to do zastosowania operatora ++ - trzeba także uważać na wywoływanie metod w wyrażeniu zapytania. Należy się wystrzegać wywoływania metod, które mogą zmieniać stan aplikacji . Zazwyczaj jednak nic nie stoi na przeszkodzie, by w wyrażeniu zapytania odczytywać wartości zmiennych z zakresu, w jakim zapytanie jest umieszczone. Niewielka modyfikacja w kodzie z listingu 8.8 pokazuje, w jaki sposób można takie rozwiązanie wykorzystać (patrz listing 8.9) .

Listing 8.9. Zastosowanie zmiennej lokalnej w zapytaniu i nt mi nS i ze = 10000 ; var b i g F i l es = from fi l e i n GetAl l Fi l es in D i rectory (@ " c : \ " ) where new Fi l e i nfo (fi l e) . Length > mi nS i ze sel ect fi l e ; var fi l esOverlOk = b i g F i l es . ToArray () ; mi nSi ze = 100000 ; var fi l esOverlOOk = b i g Fi l es . ToArray () ; mi nSi ze = 1000000 ; var fi l esOverlMB = b i g F i l es . ToArray () ; mi nSi ze 10000000 ; var fi l esOverlOMB = b i g Fi l es . ToArray () ; =

282 I

Rozdział 8. LINQ

To zapytanie, podobnie jak poprzednie, korzysta ze zmiennej lokalnej, jednak tym razem tylko odczytuje jej wartość. Zmieniając ją, możemy zmodyfikować sposób działania zapytania podczas jego kolejnego wywołania . (Wywołanie metody ToArray ( ) powoduje wykonanie zapytania i zapisanie jego wyników w tablicy. To jeden ze sposobów wymuszenia natychmiastowego wykonania zapytania) .

Styl funkcyjny i kom pozycja Wszystkie operatory LINQ mają pewną cechę wspólną: nie modyfikują danych, na których operują. Zapytanie LINQ może na przykład sortować zwracane wyniki, lecz w odróżnieniu od metod Array . Sort lub L i s t . Sort, które zmieniają kolejność danych w istniejącej kolekcji, sortowanie przy użyciu LINQ powoduje wygenerowanie nowego obiektu I En umerab l e zwra­ cającego elementy w określonym porządku. Oryginalna kolekcja nie jest w żaden sposób modyfikowana . Ten sposób działania przypomina nieco zachowanie typu stri ng. Klasa stri ng udostępnia różne metody, których działanie może sugerować, że modyfikują one istniejący łańcuch znaków; przykładami takich metod mogą być Tri m, ToUpper oraz Repl ace. Jednak łańcuchy znaków są niezmienne, a zatem wszystkie te metody w rzeczywistości tworzą nowe łańcuchy - otrzy­ mujemy zmodyfikowaną kopię, podczas gdy oryginał pozostaje niezmieniony. Zapytania LINQ nigdy nie starają się modyfikować źródła danych, dzięki czemu można ich używać do operowania na źródłach niezmiennych . LINQ to Objects bazuje na interfejsie I En umerab l e, który nie udostępnia żadnych możliwości modyfikacji lub sortowania kolekcji, na jakich operuje . ••

. .·

L---...iJ"'- ,

-_,..�;

.

Oczywiście LINQ nie wymaga, by używane w zapytaniach źródła danych były niezmienne. Interfejs I Enumerabl e może być implementowany zarówno przez takie źródla, jak i przez te, które zapewniają możliwość modyfikowania swej zawartości. Najważniejsze jest to, że zapytanie LINQ nigdy nie będzie próbowało wprowadzać zmian w kolekcji źródłowej .

Podejście takie jest czasami określane jako styl funkcyjny. Funkcyjne języki programowania, na przykład F#, często charakteryzują się tą właśnie cechą: kod funkcyjny - podobnie jak działa­ nia matematyczne takie jak dodawanie, mnożenie czy też funkcje trygonometryczne - nigdy nie modyfikuje swych danych wejściowych. Zamiast tego generuje on na ich podstawie nowe dane . W przypadku LINQ są to nowe enumeracje utworzone na bazie kolekcji źródłowych. C# nie jest językiem o charakterze funkcyjnym. Używając go, można pisać kod modyfikujący informacje - co więcej, jest to bardzo popularne. Nie przeszkadza to jednak w stosowaniu stylu funkcyjnego, czego najlepszym przykładem jest LINQ. Kod funkcyjny zapewnia bardzo duże możliwości „składania" programów z gotowych ele­ mentów. Zazwyczaj prowadzi on do tworzenia interfejsów API, które można łatwo łączyć na wiele różnych sposobów. To z kolei może znacznie ułatwiać utrzymanie kodu - niewielkie, proste możliwości łatwiej jest projektować, pisać i testować niż złożone, monolityczne frag­ menty. Jednocześnie możliwości takie pozwalają rozwiązywać bardzo złożone problemy po­ przez budowanie ich rozwiązań z niewielkich elementów. Ponieważ działanie LINQ opiera się na przekazywaniu sekwencji do metody, która przekształca ją w nową sekwencję, bez problemu można ze sobą łączyć dowolnie wiele operatorów LINQ. Gdyby wiele fragmentów

Koncepcje i techniki LINQ

I 283

kodu starało się modyfikować te same dane, bardzo trudno byłoby zagwarantować, że pro­ gram będzie działał prawidłowo. Dzięki zastosowaniu stylu funkcyjnego raz wytworzone dane nigdy nie będą zmieniane - nowe obliczenia będą generować nowe kolekcje, a nie mody­ fikować już istniejące . Jeśli można mieć pewność, że istniejące dane nigdy nie zostaną zmody­ fikowane, znacznie łatwiej jest zrozumieć działanie kodu i łatwiej będzie sprawić, że faktycznie będzie on działał tak, jak tego chcemy . Ma to szczególnie duże znaczenie w przypadku kodu wielowątkowego.

Wykonywanie opóźn ione W rozdziale 7 . przedstawiliśmy pojęcie leniwej enumeracji (określanej także czasami mianem wykonywania opóźnionego, ang. deffered execution) . Jak mieliśmy okazję się przekonać, enumeracje takie jak zwracana przez metodę GetAl l F i l es i n D i rectory wykonują niezbędne czynności na pojedynczych zwracanych elementach, a nie przetwarzają z góry całego ich zbioru. Zapytanie przedstawione na listingu 8 .2 także posiada tę cechę. Jeśli je uruchomimy, nie będziemy mu­ sieli czekać na zakończenie wykonywania metody GetAl l F i l es i nD i rectory, zanim pojawią się wyniki - nazwy plików zaczną się pojawiać natychmiast. (Cóż, właściwie to prawie natych­ miast - wszystko zależy od tego, jak długo trzeba będzie szukać, zanim uda się znaleźć plik na tyle duży, by spełnił warunki podane w klauzuli wh ere) . Ogólnie rzecz biorąc, zapytania LINQ będą się starały opóźnić realizację zadań tak bardzo, jak to tylko będzie możliwe - samo wykonanie kodu definiującego zapytanie nie spowoduje wykonania żadnych konkretnych czynności . A zatem kod użyty w naszym przykładzie: var b i g F i l es

=

from fi l e i n GetAl l Fi l es in D i rectory (@ " c : \ " ) where new Fi l e i nfo (fi l e) . Length > 10000000 sel ect fi l e ;

nie robi nic prócz opisania zapytania. Nic się nie stanie, dopóki nie zaczniemy pobierać wyników zapytania b i gFi l es w pętli foreach, a podczas każdej iteracji tej pętli zapytanie wykona najmniej­ szą liczbę operacji konieczną do pobrania następnego elementu. Może się to jednak wiązać z pobraniem wielu elementów z innych używanych kolekcji, gdyż klauzula where będzie pobie­ rać kolejne elementy tak długo, aż znajdzie element spełniający warunki bądź pobierze wszyst­ kie elementy z kolekcji źródłowej . Niemniej jednak wykona ona możliwie najmniej operacji . Sytuacja ta może się nieco zmienić, gdy zaczniemy używać niektórych bardziej złożonych mecha­ nizmów opisanych w dalszej części rozdziału. Możemy na przykład nakazać, by zapytanie LINQ sortowało dane, a w takim przypadku, zanim będzie ono w stanie zacząć zwracać kolejne ele­ menty we właściwej kolejności, będzie musiało pobrać całą zawartość zbioru źródłowego. (Choć nie jest to powszechne, to jednak można napisać źródło danych w taki sposób, by wie­ działo ono wszystko na temat sortowania, a jeśli dysponujemy także wiedzą dotyczącą samego źródła danych, to może ono zostać przez nas napisane tak, by zwracało wyniki w odpowiednim porządku, a przy tym działało w „leniwy" sposób. W następnym rozdziale, gdy zajmiemy się używaniem LINQ do obsługi baz danych, poznamy dostawców działających właśnie na tej zasadzie) . Choć wykonywanie opóźnione niemal zawsze jest dobrym i pożądanym rozwiąza­ niem, to trzeba pamiętać o jednym zagrożeniu. Ponieważ zapytanie nie jest wykony­ wane od razu, będzie ono wykonywane za każdym razem, gdy je przetworzymy. LINQ nie gromadzi kopii wyników wykonanych zapytań. Co więcej, istnieją bardzo ważkie

284 I

Rozdział 8. LINQ

argumenty wyjaśniające, dlaczego nie chcielibyśmy takich wyników przechowywać. Mogą one zajmować bardzo dużo miejsca w pamięci i uniemożliwić stosowanie tech­ niki wykorzystanej w przykładzie z listingu 8 .9 . Ta cecha LINQ sprawia, że pozornie niewinnie wyglądający kod może być bardzo kosztowny, zwłaszcza w przypadku korzystania z LINQ wraz z bazami danych. Nieumyślne wielokrotne przetworzenie zapytania może bowiem powodować wielokrotne odwołania do serwera bazy danych.

Operatory LI NQ Dostępnych jest około 50 standardowych operatorów LINQ. W pozostałej części tego rozdziału najważniejsze z nich zostały pogrupowane pod względem funkcjonalnym i opisane. Pokażemy, w jaki sposób można ich używać w zapytaniach LINQ (o ile tylko jest to możliwe) oraz jak wywoływać je bezpośrednio jako metody . .

•• . •

._„�;

.

L-------11.J"' '

Czasami użyteczne jest jawne wywoływanie operatorów LINQ jako metod, a nie stosowanie ich w wyrażeniach zapytań. Niektóre z operatorów posiadają wersje przeciążone pozwalające na korzystanie z bardziej zaawansowanych możliwości, które w przypadku używania ich w wyrażeniach zapytań nie są dostępne. Na przykład sortowanie łańcuchów znaków jest operacją zależną od wybranych ustawień lokalnych, gdyż kolejność alfabetyczna może w różnych językach oznaczać co innego. Składnia sortowania używana w wyrażeniach zapytań zawsze korzysta z domyślnych ustawień regionalnych bieżącego wątku. Jeśli jednak z j akiegokolwiek powodu konieczne jest użycie innych ustawień bądź też jeśli należy zastosować niezależny od tych ustawień sposób sortowania, to niezbędne będzie jawne wywołanie przeciążonej wersji opera­ tora OrderBy zamiast umieszczania w wyrażeniu zapytania klauzuli orderby. Istniej ą także operatory LINQ, które nawet nie mają odpowiadających im klauzul umieszczanych w wyrażeniach zapytań. Dlatego też zrozumienie sposobu, w jaki LINQ używa metod, nie jest jedynie kwestią poznania szczegółów implementacyjnych - jest to jedyny sposób umożliwiający wykorzystanie niektórych spośród bardziej zaawan­ sowanych możliwości tej technologii.

Filtrowanie Czytelnik miał już okazję poznać podstawowe możliwości filtrowania w LINQ - w przykła­ dach z listingów 8 .2 oraz 8.3 przedstawiliśmy kolejno klauzulę wh ere oraz odpowiadającą jej metodę Where . Następnym operatorem filtrowania, o którym warto wiedzieć, jest OfType. Nie posiada on swojego odpowiednika, który można by stosować w wyrażeniach zapytań, a zatem można go używać wyłącznie w formie wywołania metody. Operator ten jest przydatny w sytu­ acjach, gdy przetwarzana kolekcja może zawierać obiekty różnych typów, a nas interesują elementy tylko jednego, konkretnego z nich. Na przykład podczas obsługi interfejsu użytkow­ nika mogą nas interesować tylko elementy sterujące (takie jak przyciski), natomiast inne, o cha­ rakterze czysto wizualnym (takie jak obrazki i rysunki), możemy chcieć zignorować. W takim przypadku można zastosować kod o następującej postaci: var control s

=

myPanel . Ch i l dren . OfType () ;

Jeśli myPanel . Ch i l dren jest kolekcją obiektów pewnego typu, to powyższy kod zapewni, że control s będzie enumeracją zawierającą wyłącznie obiekty, które można zrzutować na typ Con t rol .

Operatory LINQ

I 285

Choć operator OfTy pe nie ma swojego odpowiednika w wyrażeniach zapytań, to jednak nie przeszkadza to w stosowaniu tego operatora wraz z nimi - można użyć zwróconych przez niego wyników jako źródła danych dla zapytania: var control Names

=

from control i n myPanel . Ch i l dren . OfType () where ! s tri ng . I s N u l l OrEmpty (control . Name) sel ect control . Name ;

W powyższym przykładzie operator OfType filtruje kolekcję źródłową, wybierając z niej wyłącz­ nie obiekty typu Cant rol , a następnie klauzula where poddaje je dodatkowej selekcji, pozostawiając wyłącznie te, w których właściwość N ame nie jest pusta.

Porządkowanie Wyrażenia zapytań mogą zawierać klauzulę orderby określającą kolejność zwracanych ele­ mentów. W przypadku zapytań, w których nie jest ona używana, LINQ (zazwyczaj) nie daje żadnych gwarancji dotyczących ich kolejności . Dostawca LINQ to Objects w takiej sytuacji zwraca wyniki w porządku, w jakim są one pobierane z enumeracji źródłowej, jednak w przy­ padku innych dostawców taka domyślna kolejność wcale nie musi być określona. (Dotyczy to w szczególności dostawców LINQ operujących na bazach danych, którzy w przypadku braku jawnie określonego sposobu sortowania będą zwracali wyniki w nieprzewidywalnej kolejności) . A zatem skoro mamy posortować jakieś dane, wróćmy d o klasy C a l endarEven t, której używa­ liśmy w rozdziale 7. (patrz listing 8.10) .

Listing 8.10. Klasa reprezentująca wydarzenia rejestrowane w kalendarzu cl ass Cal endarEvent { publ i c s t r i ng T i t l e { get ; set ; } publ i c DateTi meOffset StartT i me { get ; set ; } publ i c T i meSpan Durat i on { get ; set ; }

We wszystkich przykładach zamieszczonych w tym rozdziale, w których odwołujemy się do zmiennej event s, należy przyjąć, że została ona zainicjowana tak, jak pokazano to na listingu 8.1 1 .

Listing 8. 1 1 . Przykładowe dane Li s t events = new Li s t { new Cal endarEvent { T i t l e = "Swi ngowa potańcówka na nabrzeżu " , StartT i me = new DateT i meOffset (2009 , 7 , 1 1 , 1 5 , OO , OO , T i meSpan . Zero) , Durat i on = T i meSpan . FromHours (4) }, new Cal endarEvent { T i t l e = "Sobotn i a noc swi ngowa " , StartT i me = new DateT i meOffset (2009 , 7 , 1 1 , 19 , 30 , OO , T i meSpan . Zero) , Durat i on = T i meSpan . FromHours (6 . 5) }, new Cal endarEvent { T i t l e = " Formuł a 1 - Grand Pri x N i emi ec " , StartT i me = new DateT i meOffset (2009 , 7 , 12 , 12 , 10 , OO , T i meSpan . Zero) ,

286 I

Rozdział 8. LINQ

Durat i on = T i meSpan . FromHours (3) }, new Cal endarEvent { T i t l e = " P i kn i k swi ngowy " , StartT i me = new DateT i meOffset (2009 , 7 , 12 , 1 5 , OO , OO , T i meSpan . Zero) , Durat i on = T i meSpan . FromHours (4) }, new Cal endarEvent { T i t l e = "Łamańce swi ngowe w kl u b i e Set ka " , StartT i me = new DateT i meOffset (2009 , 7 , 13 , 19 , 45 , OO , T i meSpan . Zero) , Durat i on = T i meSpan . FromHours (5) };

Przykład z listingu 8 .12 pokazuje, w jaki sposób w zapytaniu LINQ można uporządkować te wydarzenia według czasu ich rozpoczęcia.

Listing 8 . 1 2 . Sortowanie elementów w zapytaniu LINQ var eventsByStartT i me = from ev i n events orderby ev . StartT i me sel ect ev ;

Domyślnie elementy będą sortowane w kolejności rosnącej . Jeśli chcemy, możemy także okre­ ślić tę kolejność jawnie: var eventsByStartT i me = from ev i n events orderby ev . StartT i me as cend i ng sel ect ev ;

Oczywiście istnieje także możliwość posortowania wydarzeń w kolejności malejącej: var eventsByStartT i me = from ev i n events orderby ev . StartT i me descendi ng sel ect ev ;

Wyrażenie podane w klauzuli orderby nie musi bezpośrednio odwoływać się do właściwości obiektu źródłowego. Może ono być wyrażeniem znacznie bardziej złożonym. Na przykład moglibyśmy pobrać tylko godzinę rozpoczęcia wydarzenia, by wygenerować nieco mylącą kolekcję wydarzeń posortowanych wyłącznie po godzinie, lecz bez uwzględnienia daty: var eventsByStartT i me = from ev i n events orderby ev . StartT i me . Ti meOfDay sel ect ev ;

Można także zastosować większą liczbę kryteriów sortowania . Przykład przedstawiony na listingu 8.13 sortuje wydarzenia najpierw według daty (ignorując godzinę), a następnie na podstawie czasu ich trwania .

Listing 8.13. Kilka kryteriów sortowania var eventsByStartDateThenDurat i on = from ev i n events orderby ev . StartT i me . Date , ev . Durat i on sel ect ev ;

Klauzuli orderby odpowiadają cztery metody operatorów. Najbardziej oczywista z nich OrderBy pobiera pojedyncze kryterium sortowania jako wyrażenie lambda:

-

-

var eventsByStartT i me = events . OrderBy (ev => ev . StartT i me) ;

Operatory LINQ

I 287

Powyższy kod da dokładnie takie same wyniki co kod z listingu 8.12. Oczywiście, podobnie jak niemal wszystkie operatory LINQ, także tę metodę można łączyć z innymi. A zatem możemy ją połączyć na przykład z operatorem Where: var l ong Events

=

events . OrderBy (ev = > ev . StartT i me) . Where (ev => ev . Durat i on > T i meSpan . FromHours (2) ) ;

Ta sekwencja wywołań stanowi odpowiednik następującego zapytania: var l ong Events = from ev i n events orderby ev . StartT i me where ev . Durat i on > T i meSpan . FromHours (2) sel ect ev ;

Działanie mechanizmu porównywania używanego do sortowania można modyfikować, wyko­ rzystując przeciążoną wersję metody pozwalającą przekazać specjalny obiekt porównujący. Musi on implementować interfejs I Comparer3, gdzie T Key jest typem zwracanym przez wyrażenie porównujące. A zatem w powyższym przykładzie obiekt ten musiałby implemen­ tować interfejs I Comp arer, gdyż właśnie tego typu jest właściwość Start T i me używana do sortowania danych. Porównywanie dat jest zagadnieniem raczej jednoznacznym, dlatego też nasz przykład niezbyt dobrze nadaje się do prezentowania możliwości użycia alter­ natywnych sposobów porównywania. Z drugiej strony całkiem duże różnice mogą występować podczas porównywania łańcuchów znaków, ponieważ różne języki w odmienny sposób defi­ niują kolejność poszczególnych liter; dotyczy to zwłaszcza liter ze znakami diakrytycznymi. Biblioteka klas .NET Framewor k zawiera klasę St ri ngCompa rer udostępniającą implementację interfejsu I Comparer, która obsługuje wszystkie języki i ustawienia regionalne obsługi­ wane przez .NET. W poniższym przykładzie klasa ta została wykorzystana wraz z przeciążoną wersją operatora OrderBy w celu posortowania wydarzeń bez uwzględniania wielkości liter na podstawie ich tytułów, zgodnie z kanadyjskimi, francuskojęzycznymi ustawieniami regionalnymi. Cul ture l n fo cul t = new Cul ture l n fo ( " fr - CA" ) ; li Jeśli drugim argumentem będzie true, to będzie uwzględniana wielkość liter. Stri ngComparer comp = Stri ngComparer . Create (cul t , true) ; var eventsByT i t l e = events . OrderBy (ev => ev . T i t l e , comp) ;

Nie można napisać wyrażenia zapytania, które stanowiłoby odpowiednik tej sekwencji wywo­ łań. Jeśli nie interesuje nas domyślny sposób porównywania dla danego typu, lecz chcemy zastosować jakikolwiek inny, musimy skorzystać z przeciążonej wersji operatora OrderBy . Metoda operatora OrderBy zawsze sortuje w kolejności rosnącej . Do sortowania w kolejności malejącej służy operator OrderByDes cendi ng. W przypadku gdy konieczne jest zastosowanie kilku kryteriów sortowania, jak w przykładzie z listingu 8.13, należy skorzystać z innych operatorów: ThenBy lub ThenByDes cendi ng. Wynika to z faktu, że operatory OrderBy oraz OrderByDes cen d i n g ignorują kolejność danych wejściowych i określają ją w całości od początku - w końcu właśnie do tego służą. Dopracowywanie kolej­ ności poprzez zastosowanie dodatkowych kryteriów jest operacją innego rodzaju, dlatego też jest ona realizowana przy użyciu innych operatorów. A zatem wywołania metod odpowiadające przykładowi z listingu 8.13 miałyby następującą postać:

3

Interfejs ten jest bardzo podobny do przedstawionego w poprzednim rozdziale interfejsu I Comparabl e, jednak w odróżnieniu od obiektów implementujących I Comparabl e, które same mogą być porównywane z innymi obiektami typu T, obiekty implementujące interfejs I Comparer pozwalają porównywać dwa obiekty typu T. Innymi słowy, obiekty porównywane są niezależne od obiektu, który je porównuje.

288

I

Rozdział 8. LINQ

var eventsByStartT i me = events . OrderBy (ev => ev . StartT i me) . ThenBy (ev => ev . Durat i on) ;

Sortowanie sprawia, że przed zwróceniem jakiegokolwiek elementu dostawca LINQ to Objects pobiera całą zawartość kolekcji źródłowej . Może on bowiem posortować elementy, wyłącznie gdy „widzi" je wszystkie.

Kon katenacja Czasami może się pojawić potrzeba połączenia dwóch sekwencji wartości w jedną. LINQ udostępnia do tego celu bardzo prosty operator: Con cat . Nie ma on swojego odpowiednika, któ­ rego można by używać w wyrażeniach zapytań. Gdybyśmy chcieli połączyć dwie listy wyda­ rzeń w jedną, musielibyśmy zastosować kod przedstawiony na listingu 8 .14.

Listing 8.14. Konkatenacja dwóch sekwencji var al l Events = ex i s t i ng Events . Concat (newEvents ) ;

Trzeba pamiętać, że operacja ta nie modyfikuje danych wejściowych. W powyższym przy­ kładzie utworzony zostanie nowy obiekt enumeracji, który zwróci wszystkie elementy kolekcji exi s t i n g E vents, a następnie wszystkie elementy n ewEven t s . A zatem rozwiązanie to może być bezpieczniejsze od metody L i st . AddRange przedstawionej w rozdziale 7., gdyż niczego nie modyfikuje . (Gdybyśmy jednak oczekiwali, że kod z powyższego przykładu zmodyfikuje zawartość kolekcji exi s t i ngEvents, to moglibyśmy być zawiedzeni) . '

.

'

'---�.

'

To doskonały przykład demonstrujący stosowanie przez LINQ opisywanego wcze­ śniej stylu funkcyjnego. Większość operatorów LINQ, podobnie jak funkcje matema­ tyczne, wylicza swoje wyniki bez modyfikowania danych wejściowych. Na przykład gdybyśmy dysponowali dwiema zmiennymi typu i nt - x i y - to oczekiwalibyśmy, że wyliczenie sumy x+y nie spowoduje zmiany wartości samych zmiennych. Konka­ tenacja działa dokładnie w taki sam sposób - można utworzyć sekwencję stanowiącą połączenie dwóch sekwencji wejściowych bez ich modyfikowania.

Podobnie jak większość operatorów LINQ, także konkatenacja wykorzystuje przetwarzanie opóźnione . Nie będzie ona z góry pobierać elementów z używanych kolekcji źródłowych . Dopiero gdy zaczniemy pobierać elementy a l l Even t s, operator Concat rozpocznie pobieranie elementów z enumeracji exi st i ngEven t s . (Co więcej, nie zacznie on pobierać jakichkolwiek ele­ mentów z enumeracji newEvents, dopóki nie zostaną pobrane wszystkie elementy exi st i ng Event s ) .

Grupowanie LINQ zapewnia możliwość przetwarzania zwyczajnych, „płaskich" list i grupowania ich ele­ mentów. Jak pokazuje listing 8 .15, możemy skorzystać z tej możliwości, by napisać w LINQ alternatywną wersję metody Get Even t s ByDay (przedstawionej w rozdziale 7.) .

Listing 8.15. Proste grupowanie w LINQ var eventsByDay = from ev i n events g roup ev by ev . Start T i me . Date ;

Powyższe zapytanie pogrupuje elementy enumeracji even t s według daty.

Operatory LINQ

I 289

Typ zmiennej even t s ByDay z powyższego przykładu będzie inny od wszystkiego, z czym mieli­ śmy się okazję dotąd spotkać. Jest to bowiem typ I Enumerabl e< I Groupi ng events ByDay ] Enume ra bl e

. .„ .



�:··:··: ····:::�·:·· : :„...J O

Klucz: 1 2 lipca 2009

IEnumera ble :

I < F i rstName>Mari an < LastName>Opan i a mari anO@onet . pl Kl i ent w Obj ect : Mari an Opan i a Ema i l : mari anO@onet . pl

Aby przeprowadzić serializację obiektu za pomocą mechanizmu serializacji zapewnianego przez platformę .NET, należy utworzyć obiekt klasy Xml S eri al i zer: Xml Seri al i zer seri a l i zer = new Xml Seri al i zer (typeof (Cus tomer) ) ;

Typ obiektu, który ma zostać poddany serializacji, trzeba przekazać do konstruktora klasy Xml Seri al i z er . Jeśli typ ten nie jest znany na etapie programowania, można go sprawdzić, wywołując na rzecz obiektu metodę G etType ( ) : Xml Seri al i zer seri a l i zer = new Xml Seri al i ze r ( c l . GetType () ) ;

Należy również określić miejsce przechowywania zserializowanego dokumentu XML. W powyż­ szym przykładzie jest on po prostu przesyłany do zmiennej klasy Stri ngWri t er:

470 I

Rozdział 12. XML

Stri ngWri ter wri ter = new Stri ngWri ter ( ) ; seri al i zer . Seri al i ze (wri ter , c l ) ; s t r i ng xml = wri ter . ToStri ng () ; Consol e . Wri teli ne ( " Kl i ent w XM L : \n { O } \n " , xml ) ;

Wynikowy łańcuch XML jest następnie wyświetlany na konsoli: < F i rstName>Mari an < LastName>Opan i a mari anO@onet . pl

Pierwszy wiersz to deklaracja XML. Jej zadaniem jest poinformowanie odbiorców (osób będą­ cych użytkownikami oraz aplikacji komputerowych) tego dokumentu o tym, że ma on postać pliku XML, a także o tym, z którą oficjalną wersją standardu jest on zgodny i jaki format kodo­ wania został w nim zastosowany. W przypadku XML-a jest to opcjonalne, ale powyższy kod zawsze generuje również ten fragment. Elementem głównym jest tu element Customer, a każda właściwość ma postać elementu dziecka. Atrybuty xml n s : xs i i xml n s : xs d odnoszą się do specyfikacji Schemat XML (ang. XML Schema) . Są one opcjonalne i w tym przykładzie nie mają większego znaczenia, dlatego nie ma tu sensu dokładniejsze wyjaśnianie ich roli. Jeśli Czytelnika interesują szczegółowe informacje na ten temat, powinien poczytać specyfikację XML lub inną podobną dokumentację taką jak MSDN Library. Jeśli nie liczyć tych opcjonalnych fragmentów, przedstawiona tu reprezentacja XML obiektu Customer jest odpowiednikiem tej, która powstała w wyniku wykonania programu zaprezento­ wanego na listingu 12.1 . Jednak tutaj zamiast pisać wiele wierszy kodu niezbędnego do obsługi specyficznych właściwości języka XML, trzeba jedynie skorzystać z trzech wierszy, w których używa się klas platformy .NET odpowiedzialnych za serializację XML. Co więcej, równie proste jest odtworzenie obiektu z postaci, w jakiej został zapisany w kodzie XML: Cus tomer c2 = seri a l i zer . Deseri al i ze (new Stri ngReader (xml ) ) as Cus tomer ; Consol e . Wri teli ne ( " Kl i ent w Obj ect : \n { O } " , c2 . ToStri ng () ) ;

Wymaga to jedynie wywołania metody Xml Seri al i zer. Deseri al i z e. Ma ona kilka przeładowa­ nych wersji, z których każda przyjmuje instancję klasy T extReader jako argument wejściowy. Z racji tego, że klasa Stri ngReader wywodzi się z klasy Text Reader, można tu po prostu przeka­ zać instancję klasy Stri ngReader, która ma odczytać dane z łańcucha XML. Metoda Deseri a l i z e zwraca obiekt, dlatego wymagane jest zrzutowanie wyniku na właściwy typ . Trzeba oczywiście w pewien sposób za to zapłacić. Serializacja XML jest metodą mniej elastyczną niż korzystanie bezpośrednio z API XML. W przypadku serializacji sami, pisząc kod, decy­ dujemy dokładnie, które elementy i atrybuty XML spodziewamy się ujrzeć. Jeśli musimy mieć możliwość dynamicznej adaptacji do elementów, których nazwy można poznać dopiero w czasie wykonania, powinniśmy trzymać się XML-owych API .

Serializacja XML

I

471

Dostosowywan ie serial izacji XML za pomocą atrybutów Standardowo wszystkie publiczne właściwości do odczytu i zapisu są serializowane jako elementy dzieci . Można zatem dostosować klasy, określając odpowiedni typ węzła XML dla każdej z ich publicznych właściwości, tak jak zostało to pokazane na listingu 12.9.

Listing 12.9. Dostosowywanie serializacji XML za pomocą atrybutów us i ng Sys tem ; us i ng Sys tem . I O ; us i ng Sys tem . Xml . Seri al i zat i on ; namespace Programm i ng-CSharp { li Prosta klasa klienta publ i c cl ass Cus tomer { [Xml Attri bute] publ i c s t r i ng Fi rstName { get ; set ; } [Xml I gnore] publ i c s t r i ng Las tName { get ; s et ; } publ i c s t r i ng Ema i l Addres s { get ; set ;

li Metoda przesłaniająca metodę Object.ToString(), która zwraca tekstową postać właściwości obiektu. publ i c overri de s t r i ng ToStri ng () { return stri ng . Format ( " { O } { l } \nEmai l : { 2 } " , Fi rst Name , LastName , Ema i l Address ) ;

li Główny program publ i c cl ass Tes ter { stat i c voi d Mai n () { Customer cl = new Cus tomer Fi rs tName = " Mari an " , Las tName " Opan i a " , Ema i l Addres s = "mari anO@onet . pl " =

};

li XmlSerializer serializer new XmlSerializer(cl . GetType()); Xml Ser i al i zer seri al i zer = new Xml Seri al i zer (typeof (Cus tomer) ) ; Stri ngWri ter wri ter = new Stri ngWri ter () ; =

seri al i zer. Seri al i ze (wri ter , c l ) ; s t r i ng xml = wri te r . ToStri ng () ; Consol e . Wri teli ne ( " Kl i ent w XM L : \n { O } \n " , xml ) ; Customer c2 = seri al i zer . Deseri al i ze (new Stri ngReader (xml ) ) as Cus tomer ; Consol e . Wri teli ne ( " Kl i ent w Obj ect : \n { O } " , c2 . ToS tri ng () ) ; Consol e . ReadKey () ;

472

I

Rozdział 12. XML

Wynik: Kl i ent w XM L : mari anO@onet . pl Kl i ent w Obj ect : Mari an Ema i l : mari anO@onet . pl

Jedyne zmiany wprowadzone w tym przykładzie polegają na dodaniu kilku atrybutów seriali­ zacji XML do klasy Customer: [Xml Attri bute] publ i c stri ng Fi rs tName { get ; set ; }

Pierwsza zmiana polega na określeniu, że właściwość F i rstName ma podlegać serializacji do postaci atrybutu elementu C u s tomer przez dodanie atrybutu Xml Attri buteAttri bute do tej wła­ ściwości . [Xml Ignore] publ i c stri ng Last N ame { get ; set ; }

Inną zmianą jest powiadomienie mechanizmu serializacji XML, że w ogóle nie należy seriali­ zować właściwości LastName. Robi się to, dodając do niej atrybut Xml I gnoreAttri bute. Jak można zauważyć w przykładowych wynikach działania programu, obiekt klasy Customer jest seriali­ zowany z pominięciem właściwości LastName, czyli dokładnie tak, jak powinien. Czytelnik z pewnością jednak zauważył, że podczas deserializacji obiektu właściwość LastName zostaje utracona. Z racji tego, że nie jest ona serializowana, Xml Seri al i zer nie jest w stanie przypi­ sać jej żadnej wartości - pozostaje ona wartością domyślną, a tą jest pusty łańcuch znakowy. W praktyce powinno się zatem wyłączać z procesu serializacji jedynie te właściwości, których się nie potrzebuje albo które jest się w stanie wyznaczyć lub uzyskać w jakiś inny sposób.

Podsu mowan ie W rozdziale tym został przedstawiony sposób używania klas LINQ to XML w celu tworzenia obiektów reprezentujących strukturę dokumentu XML, która może następnie zostać przetwo­ rzona na prawdziwy dokument tego typu. Zaprezentowano tu również sposób korzystania z tych samych klas w celu ponownego załadowania kodu XML z łańcucha tekstowego lub pliku do pamięci w postaci obiektów. Klasy te obsługują język LINQ zarówno w przypadku budowa­ nia nowych dokumentów XML, jak i w przypadku wyszukiwania informacji w dokumentach już istniejących. Przedstawiono także, w jaki sposób serializacja XML może ukryć pewne szcze­ góły obsługi kodu XML za zwykłymi klasami C# w sytuacjach, gdy dokładnie wiadomo, jakiej struktury XML należy się spodziewać.

Podsumowanie

I

473

474

I

Rozdział 12. XML

ROZDZIAŁ 13.

Sieci

Najciekawsze systemy komputerowe mają obecnie charakter rozproszony - uruchamianie programów w izolacji na pojedynczych maszynach staje się powoli prawdziwą rzadkością. Z tego powodu platforma .NET zapewnia szereg różnych sposobów komunikacji za pośrednictwem sieci komputerowych. Spektrum dostępnych możliwości związanych z sieciami może począt­ kowo wprawiać w lekką konsternację: istnieje 10 przestrzeni nazw, których nazwy zaczynają się od Sys t em . Net i które zawierają ponad 250 klas, a nie jest to nawet pełen zestaw, ponieważ dostępne jest jeszcze większe API umożliwiające produkowanie i konsumowanie usług WWW. Na szczęście sprawy wyglądają w rzeczywistości dużo prościej, niż mogłoby się na początku wydawać - mimo ogromnego zakresu API większość dostępnych tu opcji można podzielić na trzy główne kategorie. Istnieje platforma WCF (ang. Windows Communication Foundation platforma komunikacji Windows), która umożliwia budowanie i używanie usług WWW. Dos­ tępne są również API niższego poziomu, za pomocą których można bezpośrednio korzystać z protokołów WWW. Można także używać gniazd, jeśli niezbędne jest kontrolowanie operacji na bardzo niskim poziomie. Zaczniemy od opisu tego, w jaki sposób można wybrać najod­ powiedniejszy styl komunikacji na potrzeby danej aplikacji, a następnie przyjrzymy się bliżej każdej z trzech wymienionych opcji.

Wybór tech nologi i sieciowej Pierwszym krokiem na drodze do wyboru właściwego API sieciowego jest podjęcie decyzji dotyczącej natury komunikacji, której wymaga aplikacja. Istnieje wiele różnych typów apli­ kacji rozproszonych. Być może Czytelnik buduje dostępną publicznie usługę WWW, która ma być używana przez rozmaitych klientów. A może właśnie jest na odwrót - może pisze kod klienta używającego serwisu WWW opracowanego przez kogoś zupełnie innego. Można też tworzyć oprogramowanie, które będzie działać po obydwu stronach połączenia, co bynajm­ niej nie zmienia faktu, że będziemy musieli sobie odpowiedzieć na kilka ważnych pytań. Czy chcemy połączyć interfejs użytkownika z usługą w ściśle kontrolowanym środowisku, w którym w tym samym czasie da się łatwo wdrażać aktualizacje zarówno klienta, jak i serwera? A może mamy bardzo mały wpływ na aktualizacje klienta - być może sprzedajemy oprogramowa­ nie tysiącom użytkowników, których komputery będą się łączyć z naszą usługą, a co za tym idzie musimy spodziewać się współpracy z wieloma różnymi wersjami programu klienta

475

w każdej chwili? Być może nie ma nawet większego sensu dyskutować o klientach i serwerach, bo możemy przecież tworzyć system peer-to-peer. A może nasz system jest znacznie prostszy i musimy jedynie zapewnić możliwość komunikacji pomiędzy dwoma komputerami? Liczba możliwych kombinacji jest tu nieograniczona, dlatego nie ma jednego uniwersalnego rozwiązania, które mogłoby się sprawdzić we wszystkich rodzajach systemów. W kilku kolejnych punktach przyjrzymy się najczęściej spotykanym scenariuszom i opiszemy zarówno zalety, jak i wady różnych opcji proponowanych przez platformę .NET. Nawet w ramach określo­ nego scenariusza będzie się zwykle dało z powodzeniem zastosować co najmniej kilka róż­ nych rozwiązań dla osiągnięcia spodziewanego efektu. Nie istnieją tu żadne niezmienne zasady, ponieważ każdy projekt ma inne wymagania. Z tego też powodu w niniejszym podrozdziale nie udzielimy Czytelnikowi prostej odpowiedzi na pytanie, co należy zrobić, lecz opiszemy tylko kwestie, które powinien brać pod uwagę. Ostatecznie tylko Czytelnik jest w stanie wybrać właściwe rozwiązania dla swojego systemu. Zaczniemy od bardzo typowego scenariusza zwią­ zanego z obsługą WWW.

Apl ikacja WWW z kodem klienta Interfejsy użytkownika WWW stają się ostatnimi czasy coraz lepsze . Jeszcze kilka lat temu większość logiki aplikacji WWW znajdowała się po stronie serwera, a działający po stronie klienta kod w przeglądarce internetowej był odpowiedzialny za bardzo proste operacje, zwykle niewykraczające poza zaznaczanie przycisków i rozwijanie menu w reakcji na ruchy myszy. Teraz jednak po interfejsach użytkownika WWW oczekuje się już znacznie więcej . Niezależ­ nie od tego, czy użyjemy technologii AJAX (ang. Asynchronous JavaScript and XML - asynchro­ niczny JavaScript i XML), czy też technologii RIA (ang. Rich Internet Application - bogata aplikacja internetowa) takiej jak Silverlight lub Flash, aplikacje WWW będą zwykle komuniko­ wać się z serwerem WWW w sposób ciągły, a nie jedynie w chwilach przechodzenia pomiędzy stronami. Pisząc w języku C# kod działający po stronie serwera w tego rodzaju aplikacjach, będziemy przeważnie korzystali z platformy ASP.NET, aby zapewnić odpowiedni interfejs użytkownika WWW. Jednak czego powinniśmy użyć w celu zapewnienia komunikacji programowej, czyli przesyłania komunikatów krążących pomiędzy Ul WWW a serwerem, gdy strona zostanie już załadowana? Odpowiednio elastyczną opcją będzie tu platforma WCF, ponieważ jak to pokazano na rysunku 13.1, można dzięki niej sprawić, aby pojedynczy zestaw zdalnych usług był dostępny dla wielu powszechnie wykorzystywanych technologii interfejsu użytkownika działających w oparciu o przeglądarki internetowe . Usługa WCF może zostać skonfigurowana tak, aby była w stanie komunikować się równolegle na kilka różnych sposobów. Dałoby się też użyć formatu JSON (ang . JavaScript Object Notation - notacja obiektów JavaScript), który jest pow­ szechnie stosowany w interfejsach użytkownika działających na bazie technologii AJAX, ponie­ waż stanowi on wygodny format komunikatów dla kodu JavaScript klienta. Można byłoby również skorzystać z usług WWW działających w oparciu o XML. Pamiętajmy, że zastoso­ wanie WCF po stronie serwera wcale nie wymusza użycia tej platformy po stronie klienta. Dodatkowo z usług tego rodzaju można korzystać z poziomu klientów napisanych przy użyciu innych technologii takich jak Java - muszą one jedynie obsługiwać te same standardy usług WWW co platforma WCF.

476

I

Rozdział 13. Sieci

Styll kommlika1cji JavaScript lub ActionScript (np. JQuery, ASP.NET AJAX itd.I

.Serwer WWW

Silverlight (WCF)

Aplet Java

XBAP WPF (WCF)

Rysunek 13.1 . Klienty aplikacji WWW i usługi WCF Przypadek, gdy aplikacja WWW korzysta z kodu C# po stronie klienta, będzie się w praktyce sprowadzał do zastosowania technologii Silverlight lub WPF. WPF można umieścić na stronie WWW, pisząc XBAP (ang. XAML Browser Application - aplikacja przeglądarkowa XAML) . Roz­ wiązanie to będzie działać wyłącznie wówczas, gdy użytkownik końcowy będzie miał zainsta­ lowany mechanizm WPF. Gdy będziemy używali języka C# zarówno po stronie klienta, jak i ser­ wera, najprostszym wyborem będzie prawdopodobnie zastosowanie po obydwu stronach WCF. A co w przypadku, gdy po stronie serwera nie działa platforma .NET, ale nadal zależy nam na jej użyciu w kliencie WWW? Istnieją wówczas pewne obostrzenia dotyczące zastosowania WCF. Wersja platformy WCF zapewniana przez mechanizm Silverlight jest znacznie bardziej ograniczona niż wersja, którą oferuje pełna platforma .NET Framework - podczas gdy pełną wersję można skonfigurować tak, aby wykorzystywała wszelkiego rodzaju protokoły, plat­ forma WCF wbudowana w Silverlight obsługuje jedynie dwa z nich. Są to tak zwany profil podstawowy (ang. basie profile) dla usług WWW, w przypadku którego dostępny jest tylko wąski zbiór możliwości, oraz unikatowy dla WCF protokół binarny, który oferuje ten sam niewielki zakres możliwości, ale wykorzystuje przepustowość sieci nieco bardziej wydajnie od działa­ jącego w oparciu o XML profilu podstawowego. Jeśli zatem chcemy, aby klient Silverlight używał platformy WCF do komunikacji z usługą WWW niekorzystającą z .NET, jak ma to miejsce w przykładzie przedstawionym na rysunku 13.2, rozwiązanie to będzie działać wyłącz­ nie wtedy, gdy nasza usługa będzie obsługiwała profil podstawowy. Bardziej zaskakujące może być to, że podobne ograniczenia istnieją w przypadku XBAP WPF. Mimo że aplikacje tego rodzaju korzystają z pełnej wersji platformy .NET, niektóre możliwości WCF są w ich przypadku wyłączone z przyczyn związanych z bezpieczeństwem - kod klienta działający w przeglądarce internetowej nie powinien mieć całkowitej swobody w kwe­ stii łączenia się z dowolnym miejscem w sieci, ponieważ znacznie ułatwiłoby to życie hake­ rom. Z tego powodu WCF oferuje jedynie bardzo ograniczone wersje swoich usług aplikacjom .NET działającym w obrębie przeglądarek, co oznacza, że aplikacji XBAP dotyczą te same ograniczenia odnośnie do WCF co aplikacji Silverlight. Wybór technologii sieciowej

I

477

Serwer WWW Silverlight (WCF)

Usługi WWW (profil podstawowy)

Rysunek 13.2 . Klient Silverlight i usługa WWW niekorzystająca z platformy .NET Pisanie klienta Silverlight i chęć komunikowania się z usługą, która nie zapewnia zgodności z profilem podstawowym usług WWW, niekoniecznie musi oznaczać kłopoty. Wyklucza jedy­ nie zastosowanie WCF. Zamiast niego będziemy zatem musieli korzystać z API WWW niż­ szego poziomu lub nawet API gniazd, jeśli w przypadku danej usługi zajdzie taka potrzeba . Zwróćmy uwagę na to, że choć WCF stanowi zwykle dobrą opcję standardową po stronie serwera w przypadku aplikacji WWW z kodem klienta, istnieje kilka sytuacji, w których praw­ dopodobnie nie powinno się stosować tego rozwiązania . ASP.NET zapewnia swój własny mechanizm obsługi klientów AJAX i choć jest on o wiele mniej elastyczny niż ten, który oferuje platforma WCF, może to nie mieć znaczenia, ponieważ nie zawsze będzie nam zależało na elastyczności. Prostota korzystania po stronie serwera tylko z jednej platformy zamiast z dwóch może się w takich przypadkach okazać ważniejsza . Istnieje jeszcze bardziej subtelny powód, dla którego platforma WCF nie zawsze okaże się najlepszą opcją. Jest nim styl komunikacji. Gdy skorzystamy z WCF w aplikacji WWW, zapew­ niana przez nią komunikacja zwykle będzie wiązała się z następującymi krokami: 1.

Jakiś kod po stronie klienta (skrypt przeglądarki, kod C# lub Flash ActionScript) postana­ wia wysłać komunikat do serwera .

2 . Serwer odbiera ten komunikat i uruchamia pewien fragment kodu, który wykonuje ope­ racje niezbędne do jego przetworzenia .

3.

Gdy działanie kodu zostaje zakończone, serwer wysyła do klienta komunikat zwrotny zawierający wszelkie dane zwrócone w wyniku tego działania (lub, gdy nie ma żadnych danych do zwrócenia, po prostu komunikat informujący, że odpowiednie zadanie zostało wykonane) .

W praktyce mamy tu więc do czynienia ze zdalnym wywołaniem metody - jest to sposób, z którego można skorzystać, aby poprosić serwer o uruchomienie określonego fragmentu kodu i ewentualne zwrócenie wartości . (Ogólnie rzecz biorąc, platforma WCF jest w tej kwestii bardziej elastyczna, ale w kontekście aplikacji WWW wzorce komunikacji są dość ograniczone, ponieważ klienty znajdują się zwykle za zaporami sieciowymi) . Rozwiązanie to sprawdzi się z pewnością w przypadku takich operacji jak wyszukiwanie notowań giełdowych lub spraw­ dzanie prognozy pogody, jeśli jednak tworzymy aplikację do przeglądania zdjęć, raczej nie będzie to dobry sposób pobierania fotografii. Dałoby się co prawda sprawić, aby rozwiązanie to działało, ale łatwiej będzie skorzystać z wbudowanych już w przeglądarki internetowe

478

I

Rozdział 13. Sieci

mechanizmów do pobierania obrazów - niemal na pewno będziemy chcieli, aby bitmapy można było pobrać za pośrednictwem protokołu HTTP zamiast przy użyciu WCF. HTML i Sil­ verlight oferują elementy Ul, które radzą sobie z renderowaniem obrazów pobranych za pomocą HTTP. Przeglądarki są zwykle w stanie rozpocząć renderowanie obrazów bez konieczności oczekiwania na zakończenie procesu ich pobierania, a to trudno jest osiągnąć przy użyciu idiomu wywoływania metody. Korzystając z normalnego mechanizmu pobierania obrazów HTTP, możemy również cieszyć się z zalet standardowego buforowania HTTP w swojej prze­ glądarce internetowej oraz z zalet wszelkich buforujących obiektów pośredniczących, których być może używamy. Stary prosty protokół HTTP sprawdza się tu lepiej niż próby pobierania bitmap za pomocą czegoś, co w zarysie przypomina wywoływanie metod. Ogólniej rzecz biorąc, jeśli informacje, z jakich korzysta kod naszego klienta, mają postać zbioru zasobów, które da się zidentyfikować za pomocą łańcuchów URI (ang. Uniform Resource Iden­ tifier - uniwersalny identyfikator zasobu; przykładem może tu być http://helion.pl/) i do których dostęp można uzyskać za pośrednictwem protokołu HTTP, prawdopodobnie lepiej będzie trzymać się zwykłego HTTP, zamiast używać WCF. Pozwala to nie tylko korzystać z zalet normalnego buforowania HTTP podczas odczytu danych, lecz również może upraszczać kwestie bezpieczeństwa, oferując możliwość zastosowania dowolnego mechanizmu - wyko­ rzystywanego przez użytkowników serwisu do logowania i zabezpieczania ich dostępu do stron WWW - w celu ochrony zasobów, które mają być pobierane przez program.

.„

ł..��,'

,

:

Usługa prezentująca zbiór zasobów identyfikowanych za pomocą łańcuchów URI, do których dostęp jest możliwy za pośrednictwem standardowego mechanizmu HTTP, bywa czasami określana mianem usługi RESTful. REST (ang. Representational State Transfer - transfer stanu reprezentacyjnego) to styl architektury systemów rozpro­ szonych. Interesuje on nas szczególnie dlatego, że jest to styl używany w sieci World Wide Web. Termin ten pochodzi z pracy doktorskiej jednego z autorów specyfikacji HTTP, a dokładnie Roya Fieldinga. REST jest pojęciem bardzo niewłaściwie rozumia­ nym i wiele osób myśli, że skoro używają protokołu HTTP, muszą też posługiwać się stylem REST. Nie jest to jednak aż tak proste. Bliższe prawdy jest stwierdzenie, że korzystanie ze stylu REST oznacza używanie mechanizmu HTTP w duchu, w jakim mechanizm ten miał być pierwotnie używany. Więcej informacji na temat teoretycz­ nych podstaw rozwiązania REST można znaleźć w książce Sama Ruby'ego i Leonarda Richardsona pt. RESTful Web Services (http;//oreilly.com/catalog/9780596529260/), która ukazała się nakładem wydawnictwa O'Reilly.

Zastosowanie WCF wymaga zwykle mniej wysiłku niż projektowanie usługi RESTful - całe rozwiązanie da się skonfigurować i uruchomić, myśląc i planując wcześniej o wiele mniej (choć braku myślenia i planowania nie musimy oczywiście uważać za coś pozytywnego w przy­ padku swojej aplikacji) . Jeśli jednak wymagana komunikacja z serwerem nie wydaje się wpisy­ wać dobrze w styl przypominający wywoływanie metod, prawdopodobnie zechcemy rozważyć inne rozwiązania niż użycie platformy WCF. Może się zdarzyć, że ani WCF, ani prosty protokół HTTP nie będą stanowiły najlepszych rozwiązań w przypadku łączenia Ul WWW z usługą . Używając technologii Silverlight, możemy skorzystać z gniazd TCP lub UDP z poziomu przeglądarki internetowej . (Obsługa UDP jest w pewien sposób ograniczona. Silverlight 4, czyli aktualna wersja tego mechanizmu w czasie pisania tej książki, obsługuje UDP jedynie w scenariuszach transmisji grupowej do klientów) . Rozwiązanie to wymaga znacznie większego nakładu pracy, może jednak zapewnić znacznie bardziej elastyczne wzorce komunikacji - nie ogranicza nas tu styl żądanie-odpowiedź Wybór technologii sieciowej

I

479

oferowany przez protokół HTTP. Elastyczność ta może być niezbędna w przypadku gier i apli­ kacji do czatowania, ponieważ zapewnia ona serwerowi sposób informowania klienta za każ­ dym razem, gdy wydarzy się coś ciekawego. Gniazda mogą również oferować mniejsze opóź­ nienia komunikacji niż mechanizm HTTP, co może mieć duże znaczenie w przypadku gier internetowych.

Kl ient . N ET i serwer . N ET Aplikacje WWW, choć są ostatnimi czasy niezwykle modne, nie stanowią jedynego rodzaju systemów rozproszonych. Tradycyjne aplikacje Windows zbudowane przy użyciu WPF lub Windows Forms są nadal powszechnie wykorzystywane, ponieważ oferują nieraz bardzo ważne możliwości, a ich zalety mogą stanowić o przewadze tego typu rozwiązań nad aplika­ cjami WWW zarówno z punktu widzenia użytkowników oprogramowania, jak i jego twór­ ców. Opcję tę należy rozważać oczywiście tylko wtedy, gdy wszyscy końcowi użytkownicy aplikacji korzystają z systemu Windows, ale w przypadku bardzo wielu programów założe­ nie to można z powodzeniem przyjąć. Jednak nawet w sytuacji, gdy mamy pewność, że klienci korzystają z systemu Windows, rozwiązanie to ma pewną istotną wadę . Polega ona na tym, że w porównaniu z aplikacjami WWW niezwykle trudno jest tutaj kontrolować kwestię wdrażania oprogramowania. W przypadku aplikacji WWW aktualizować musimy jedynie aplikację na serwerze. Wszyscy klienci będą używali najnowszej wersji programu przy kolejnym załadowaniu nowej strony . •

. •

. ..

._,..�;

_. . ...._ .__ � ,

'

Aplikacje internetowe działające poza przeglądarkami internetowymi mogą w znacznym stopniu zatrzeć tę różnicę. Technologie Silverlight oraz Flash umożliwiają tworzenie takich aplikacji internetowych, które mają części instalowane na maszynach użytkowników i uruchamiane jak zwykłe programy poza przeglądarką WWW. Rozważania przedstawione w tym punkcie mogłyby mieć zastosowanie do naszych pro­ jektów, gdybyśmy budowali właśnie tego rodzaju aplikacje WWW.

Aby zaktualizować klasyczną aplikację Windows, musimy w jakiś sposób dostarczyć nową wersję programu do maszyn użytkowników końcowych. Ponieważ zainstalowanie nowej wersji aplikacji na komputerach wszystkich użytkowników naraz rzadko okazuje się rozwiązaniem możliwym do zastosowania w praktyce, powinniśmy w jakiś sposób poradzić sobie z bardzo prawdopodobną sytuacją, w której kilka różnych wersji oprogramowania klienta próbuje łączyć się z naszym serwerem. Skala potencjalnych problemów będzie tu zależeć od tego, jak dużą kontrolę mamy nad komputerami klientów.

Ściśle kontrolowane wdrażanie Niektóre aplikacje są wdrażane w ściśle kontrolowanych środowiskach. Załóżmy na przy­ kład, że przy użyciu WPF piszemy pewną aplikację branżową, która zostanie zainstalowana wyłącznie na maszynach należących do naszej firmy. Jeśli nasz dział IT sprawuje żelazną kontrolę nad należącymi do przedsiębiorstwa komputerami, możemy mieć stosunkowo duży wpływ na to, które wersje aplikacji są na nich zainstalowane . Administratorzy sieci mogliby w takiej sytuacji wymuszać stałą aktualizację oprogramowania użytkowników, tak aby mieli oni zawsze najnowszą wersję aplikacji. Co za tym idzie, nowe wersje pracowałyby równolegle ze starymi przez bardzo krótki czas rzędu jednego lub dwóch dni . Można by nawet posunąć się nieco dalej i sprawić, aby aplikacja sprawdzała dostępność aktualizacji i odmawiała pracy, gdy tylko pojawi się jej nowsza wersja . 480

I

Rozdział 13. Sieci

Dla programisty jest to bardzo korzystna sytuacja, ponieważ znacznie ułatwia ona wprowadza­ nie zmian na serwerze . Na pewnym etapie z pewnością zechcemy dodać nowe usługi mające zapewnić obsługę nowych możliwości oferowanych przez aplikację. Możemy też chcieć zmodyfikować działanie istniejących usług, co jest zwykle bardziej kłopotliwe niż dodanie zupełnie nowych możliwości - gdy korzystamy z WCF, nie jest łatwo zmienić sposób dzia­ łania usługi bez jednoczesnego odcinania od niej starszych klientów. Jest to co prawda moż­ liwe, ale dość trudne do wykonania - przeważnie dużo prościej jest zaspokoić potrzeby starszych klientów przez zapewnienie w okresie przejściowym równoległego działania róż­ nych wersji usługi . Zaletą posiadania na tyle dużej kontroli, aby móc pozbyć się starszych wersji aplikacji, jest to, że dokładnie znany jest moment, w którym nadchodzi koniec okresu przejściowego i w którym możemy wyłączyć poprzednie wersje usług. Sytuacja nie będzie tak komfortowa, gdy nie będziemy w stanie wymusić tego rodzaju zmiany po stronie klienta.

Słabo kontrolowane wdrażanie Sprawy się komplikują, gdy nie wszyscy klienci korzystający z naszej aplikacji pracują w naszej firmie, ponieważ trudniej jest w takim przypadku wymusić odpowiednie aktualizacje opro­ gramowania użytkowników. Nie jest to niemożliwe - na przykład program Windows Live Messenger firmy Microsoft co jakiś czas informuje nas, że jeśli nie zaktualizujemy posiadanej wersji, nie będziemy w stanie w dalszym ciągu korzystać z usługi. Pamiętajmy, że jest to usługa bezpłatna i dlatego jej twórcy mogą sobie pozwolić na takie dyktowanie warunków jej używa­ nia. Czytelnik z pewnością przekona się, że klienci płacący za oprogramowanie raczej nie zechcą godzić się na tego typu warunki i będą nalegać, aby produkt, który kupili, pracował nadal bez konieczności regularnego instalowania aktualizacji . Płynie stąd wniosek, że prawdopodobnie będziemy musieli bez końca zapewniać obsługę starszych wersji swoich usług. W tym momencie WCF może nie wydawać się zbyt dobrym rozwiązaniem. Jedną z pozytywnych cech tej platformy jest to, że po cichu wykonuje ona za nas mnóstwo pracy, jest to jednak miecz obosieczny - WCF sprawdza się naprawdę dosko­ nale, gdy obydwie strony połączenia ewoluują równolegle, jednak z czasem może też stać się nie lada przeszkodą, gdy strony te nie idą do przodu w tym samym tempie . Jeśli chcemy sprawić, aby usługa mogła się rozwijać niezależnie od klienta, musimy dokładnie zrozumieć sposób, w jaki WCF prezentuje naszą usługę, oraz to, jak zmiany, które będziemy chcieli wpro­ wadzić, mogą wpływać na jej działanie. Jeśli na przykład zdecydujemy, że metoda dostępna w ramach usługi wymaga dodatkowego argumentu, będziemy musieli odpowiedzieć sobie na pytanie, co stanie się ze starym klientem próbującym wywołać operację bez podania tego nowego argumentu. W praktyce prostsze może się okazać bezpośrednie korzystanie z mecha­ nizmu HTTP i XML, ponieważ dzięki nim mamy pełną kontrolę nad tym, jakie komunikaty są przesyłane przez sieć. Nie oznacza to jednak, że WCF z całą pewnością musi być tutaj złym wyborem. Z opisanym powyżej problemem możemy sobie poradzić, na przykład utrzymując kilka wersji swojej usługi lub korzystając z API niższego poziomu odpowiedzialnego za komunikaty WCF. Wybór pomiędzy WCF a HTTP jest uzależniony od natury naszego wdrożenia. W przypadku wdraża­ nia ściśle kontrolowanego platforma WCF okaże się prawdopodobnie dobrym wyjściem, jeśli jednak dysponujemy mniejszą kontrolą, dodatkowy koszt związany z zastosowaniem API niż­ szego poziomu może zacząć wyglądać na wart poniesienia.

Wybór technologii sieciowej

I

481

Niezależnie od tego, jak dużą mamy kontrolę nad procesem wdrażania oprogramowania, podobnie jak w przypadku aplikacji WWW istnieją tu szczególne scenariusze, w których nie sprawdzą się najlepiej ani usługi korzystające z możliwości platformy WCF, ani API WWW. Jeśli potrzebujemy modeli komunikacji, które nie pasują dobrze do protokołu HTTP, pamię­ tajmy, że w przypadku tego stylu aplikacji możemy korzystać z pełnego spektrum możliwości komunikacyjnych oferowanych przez WCF; jak się wkrótce przekonamy, platforma ta obsługuje więcej niż tylko te typowe wzorce komunikacji WWW . Oznacza to, że w scenariuszu tym zasto­ sowanie gniazd wydaje się wyborem jeszcze bardziej niezwykłym i że rozwiązanie to zazwyczaj będzie się okazywało przydatne tylko wówczas, gdy będziemy potrzebowali bardzo precyzyj­ nej kontroli nad sposobem, w jaki komunikaty są konstruowane i dostarczane .

Kl ient .NET i usługa WWW pochodząca z zewnątrz Nie zawsze będzie tak, że to my będziemy pisali kod działający po obydwu stronach połącze­ nia. Może się zdarzyć, że będziemy odpowiedzialni za opracowanie klienta .NET, który będzie komunikował się z usługą WWW zapewnianą przez kogoś innego. Możemy na przykład pisać część interfejsową WPF dla serwisu społecznościowego takiego jak Twitter lub też pracować nad klientem Silverlight, który ma realizować dostęp do zewnętrznej witryny takiej jak Digg. W tego rodzaju przypadkach nasz wybór technologii komunikacji będzie zdeterminowany głównie przez usługę, z którą będziemy się łączyć. Jeśli prezentuje ona informacje w sposób, który umożliwia ich konsumpcję przez platformę WCF, należy skorzystać właśnie z niej . Skąd mamy jednak wiedzieć, jak jest w tym przypadku? Moglibyśmy spróbować zapytać personel odpowiedzialny za obsługę techniczną i pracujący dla dostawcy usługi, czy współpracuje ona z platformą WCF, jeśli jednak ludzie ci nie będą mieli pewności, będzie to uzależnione od natury samej usługi . Jeżeli jej dostawca używa tak zwanej rodziny standardów usług WWW WS-*, istnieje spora szansa, że WCF będzie w stanie komunikować się z tą usługą. Jeśli Czytelnik żywi nadzieję na coś bardziej rozstrzygającego niż „spora szansa", ma pecha. Z samego faktu, że dwa systemy powstały z myślą o korzystaniu z tego samego zestawu standardów, nie wynika jeszcze, że będą mogły skutecznie się ze sobą porozumiewać, nawet jeśli bardzo ściśle się tym standardom podporządkowują. Jeśli ta informacja jest dla Czytelnika nowością, witamy w świecie integracji systemów!

Świetnie, jeśli platforma WCF sprawdza się w przypadku naszego rozwiązania, jeśli jednak jej zastosowanie nie wchodzi w grę, powinniśmy skorzystać z bazujących na HTTP API plat­ formy .NET. Możemy to oczywiście zrobić, chyba że stanowiąca zagadkę usługa nie opiera swojego działania na mechanizmie HTTP i wymaga bezpośredniego korzystania z protokołu TCP lub UDP, w których przypadku powinniśmy zastosować gniazda. W skrócie, jesteśmy na łasce serwera i musimy po prostu wybrać takie rozwiązanie, które będzie poprawnie działać. Pamiętajmy, że ponieważ wersja platformy WCF zapewniana przez mechanizm Silverlight ma zdecydowanie bardziej ograniczone możliwości niż jej wersja należąca do pełnej platformy .NET, to w przypadku klienta Silverlight prawdopodobieństwo konieczności korzystania z API HTTP jest większe, niż ma to miejsce w przypadku pełnego klienta .NET.

482

I

Rozdział 13. Sieci

Kl ient zewnętrzny i usługa WWW . N ET

Jeśli w oparciu o platformę .NET piszemy usługę WWW, która ma być dostępna dla programów klientów opracowanych przez inne osoby, wybór odpowiedniej technologii będzie podykto­ wany dwoma aspektami: naturą usługi oraz wymaganiami naszych klientów 1 . Jeśli mamy do czynienia z czymś, co bardzo naturalnie pasuje do protokołu HTTP - a więc budujemy na przykład usługę do pobierania bitmap - opracowanie tego jako zwykłej aplikacji ASP.NET może być najlepszym rozwiązaniem (w tej sytuacji Czytelnik powinien zerknąć do rozdziału 21 .) . Jednak w przypadku usług, które zdają się raczej mieć postać zestawu zdalnie wywoływal­ nych metod, najlepszym wyjściem będzie prawdopodobnie zastosowanie platformy WCF. Da się ją skonfigurować w taki sposób, aby obsługiwała szerokie spektrum różnych protokołów sieciowych nawet w przypadku jednej usługi, dzięki czemu może ona zapewnić obsługę róż­ nych rodzajów klientów. Podobnie jak ma to miejsce w przypadku innych typów aplikacji, z gniazd powinniśmy korzy­ stać wyłącznie wtedy, gdy nasza aplikacja ma nietypowe wymagania, których nie da się łatwo zaspokoić za pomocą modeli komunikacji oferowanych przez protokół HTTP. Znając już zatem pewne najczęściej spotykane scenariusze i wiedząc, które z opcji komunikacji mają większe, a które mniejsze szanse na spełnienie naszych oczekiwań, przyjrzymy się sposo­ bom praktycznego zastosowania tych rozwiązań.

Platforma WCF WCF to platforma umożliwiająca budowanie dostępnych zdalnie usług i ich używanie. Została ona szczególnie dobrze dopasowana do standardów WWW bazujących na języku XML, choć jej użycie nie jest bynajmniej ograniczone wyłącznie do nich. Zapewnia ona model programi­ styczny, który obsługuje wiele różnych podstawowych mechanizmów komunikacyjnych. Wspierając wiele standardów usług WWW, platforma WCF oferuje również własne proto­ koły o wysokiej wydajności, których możemy używać w systemach pracujących w całości w oparciu o .NET, i pozwala się rozszerzać, dzięki czemu da się dodawać do niej obsługę innych protokołów. Zasada działania WCF sprawia, że wiele z tych detali staje się kwestią konfigu­ racji - usługi i klienty można dzięki temu pisać w ten sam sposób niezależnie od tego, jaki mechanizm komunikacji jest używany. Aby poznać możliwości platformy WCF, zbudujemy bardzo prostą aplikację komunikatora internetowego, której zadaniem będzie umożliwianie wielu użytkownikom prowadzenia poga­ wędek przez sieć. Dzięki temu będziemy mogli się skupić na kodzie odpowiedzialnym za komunikację, a klientem będzie mogła być bardzo prosta aplikacja konsolowa .

Tworzenie projektu WCF Zaczniemy od opracowania serwera naszej aplikacji czatu. Jeśli Czytelnik chce budować swoją kopię projektu równolegle z lekturą, powinien otworzyć okno dialogowe New Project środo­ wiska Visual Studio (Ctrl+Shift+N) i ze znajdującej się w lewej części listy szablonów wybrać

1 A dokładniej wymaganiami, na które jesteśmy skłonni się zgodzić.

Platforma WCF

I

483

pozycję Visual C#/WCF. W środkowej części okna należy wskazać szablon projektu o nazwie WCF Service Library. Nazwijmy projekt ChatServerL i brary i po sprawdzeniu, czy zaznaczone jest pole opcji Create directory for solution, nazwijmy solucję WcfChat. W wyniku kompilacji tego projektu powstanie plik DLL, ponieważ projekt bazujący na szablo­ nie WCF Service Library nie wymusza udostępniania usługi WCF w ramach jakiejś określonej aplikacji kontenera . WCF może działać w obrębie IIS, Windows Service, aplikacji konsolowej lub w gruncie rzeczy dowolnej aplikacji .NET. Jeśli zamierzamy używać jakiegoś konkretnego rodzaju hosta, możemy po prostu utworzyć projekt odpowiedniego typu. Na przykład zamiast korzystać z szablonu WCF Service Library, moglibyśmy utworzyć projekt aplikacji WWW ASP.NET, gdybyśmy chcieli, aby usługa WCF była udostępniana właśnie przez program tego rodzaju. (Usługę WCF możemy dodać jako nowy element do istniejącego projektu WWW, dlatego nie potrzebujemy tu bynajmniej typu projektu specyficznego dla WCF) . Jednak zasto­ sowanie tego szablonu biblioteki niesie ze sobą określone korzyści . Jak się wkrótce przeko­ namy, zapewnia nam to łatwy sposób przeprowadzania prostych ręcznych testów usługi . Oznacza to również, że możemy osadzać tę usługę w wielu różnych aplikacjach hostów, co może się okazać przydatne przy wykonywaniu automatycznych testów - dzięki temu będzie się dało sprawdzać działanie usługi bez konieczności wdrażania jej w ostatecznym środowi­ sku pracy. Ś rodowisko Visual Studio dodało do projektu pojedynczą usługę o nazwie Serv i cel . Plik ten zawiera trochę przykładowego kodu wykonującego operacje, które nie są nam do niczego potrzebne w przykładowej aplikacji komunikatora internetowego, dlatego możemy je swo­ bodnie pominąć. (Jeśli czytając te słowa, Czytelnik buduje własną wersję aplikacji, spokojnie może usunąć ten kod) . Korzystając okna dialogowego Add New Item, dodajmy do projektu nowy element typu WCF Service o nazwie ChatServi ce. W wyniku tej operacji środowisko Visual Studio doda do naszego projektu dwa pliki: ChatService.cs oraz IChatService.cs. Odzwierciedla to fakt, że WCF rozróżnia kod implementujący usługę oraz kontrakt (ang. contract) tej usługi.

Kontrakty WCF Gdy dwa systemy komunikują się z a pośrednictwem sieci, muszą uzgodnić, jakie informacje mają być przesyłane w obydwie strony. WCF formalizuje to za pomocą tak zwanych kontraktów (ang. contracts) . Interfejs I C hat Serv i c e dodany przez środowisko Visual Studio reprezentuje zatem kontrakt usługi (ang. service contract) . Kontrakt usługi definiuje operacje, które usługa ta oferuje. Jak widać w kodzie przedstawionym na listingu 13.1, interfejs ten jest oznaczony za pomocą atrybutu S erv i ceContract, aby dać nam jasno do zrozumienia, że mamy tu do czynie­ nia z definicją kontraktu.

Listing 1 3 . 1 . Kontrakt usługi [Serv i ceContract] publ i c i nterface IChatServ i ce { [Operat i onContract] vo i d DoWor k ( ) ;

Każda należąca do interfejsu metoda, która definiuje operację oferowaną przez usługę, musi być oznaczona za pomocą atrybutu Operat i on Cant ra ct . Być może wydawało się Czytelnikowi, że wystarczające będzie oznaczenie interfejsu przy użyciu atrybutu Serv i ceContract. Po co

484

I

Rozdział 13. Sieci

oznaczać też każdą metodę z osobna? Platforma WCF wymaga, abyśmy jawnie określili tu swoje intencje, dzięki czemu staje się oczywiste, że definiujemy pewne możliwości systemu, które mają być widoczne w sieci. Wywołanie metody na rzecz lokalnego obiektu jest operacją zupełnie innego rodzaju niż korzystanie ze zdalnej usługi - kwestie wydajności i niezawod­ ności znajdują się w tych przypadkach wręcz na przeciwnych biegunach. Dlatego też bardzo istotne jest, aby w kodzie wyraźnie zaznaczyć to rozgraniczenie. '

. '

1..-.----11:.I*

,

Choć dla każdej operacji definiujemy odpowiednią metodę, ostatecznie to kontrakt decyduje, jakie komunikaty usługa może odbierać i wysyłać. Aby wywołać operację, klient będzie musiał wysłać komunikat do serwera za pośrednictwem sieci. Gdy dodajemy metodę oznaczoną za pomocą atrybutu Operati onContract do interfejsu z atrybutem Servi ceContract, w rzeczywistości definiujemy logiczną strukturę komunikatu, który będzie wysyłany w celu wywołania tej operacji, a także komunikatu, który będzie odsyłany do klienta, gdy operacja zostanie wykonana. Platforma WCF umożliwia nam odwzorowanie tych formatów komunikatów w postaci sygnatur metod, ponie­ waż stanowią one wygodny sposób zapisu dla programistów. Platforma WCF obsługuje również inne sposoby definiowania formatów komuni­ katów - można napisać odpowiedni kontrakt w języku WSDL (ang . Web Service Definition Language - język definicji usług WWW), a następnie generować typy na tej podstawie. Opis używania tej metody wykracza jednak poza zakres tematów opisy­ wanych w niniejszej książce.

Zadaniem naszej usługi jest umożliwianie ludziom prowadzenia internetowych pogawędek, dlatego będzie ona musiała zapewnić klientom sposób wysyłania krótkich fragmentów tekstu, które będziemy określać mianem notatek. (Bardziej oczywistą nazwą byłby tu komunikat, ale prowadziłoby to do powstawania dwuznaczności - WCF wysyła komunikaty do serwera i od niego dla każdej operacji, dlatego określanie pewnej części informacji, która występuje w nie­ których komunikatach, jako komunikatu wprowadzałoby tylko niepotrzebne zamieszanie) . Aby uprościć sprawę, zapewnimy tylko jedną wielką przestrzeń komunikacyjną (czyli jeden „pokój rozmów"), w której każda osoba będzie mogła widzieć wszystkie notatki; nie będziemy zatem obsługiwać prywatnych konwersacji. By obsłużyć operację wysyłania notatek, pozbę­ dziemy się metody DoWork dodanej przez środowisko Visual Studio, zastępując ją kodem przed­ stawionym na listingu 13.2. Listing 13.2. Modyfikacja kontraktu [Opera t i onContract] vo i d Pos tNote (stri ng from , stri ng note) ;

Gdy spróbujemy teraz zbudować nasz projekt w środowisku Visual Studio, kompilator zgłosi następujący błąd: error CS0535 : ' ChatServerli brary . ChatServ i ce ' does not i mpl ement i nterface member '-+ 1 ChatServerL i brary . I ChatServ i ce . Post Note (stri ng , s t r i ng) ' '

Pamiętajmy, że środowisko Visual Studio dodało dwa pliki: IChatService.cs (kontrakt) oraz ChatService.cs (implementację usługi) . Kompilator wskazuje nam, że implementacja usługi nie jest już zgodna z jej kontraktem. Co za tym idzie, w pliku ChatService.cs powinniśmy zastąpić metodę DoWork następującym kodem: 2 Błąd CS0535: ChatServerL i brary . ChatServi ce nie implementuje składowej interfejsu ChatServerL i brary . I ChatServi ce . '-+PostNote (stri ng , stri ng) przyp. tłum. -

Platforma WCF

I

485

publ i c vo i d PostNote (stri ng from , s t r i ng note) { Debug . Wri teli ne ( " { O } : { 1 } " , from , note) ;

Aby skompilować projekt, należy jeszcze dodać dyrektywę u s i ng Sys t em . Di agnos t i c s w górnej części tego pliku. ••

.._,..�;

.

L-------11.J"' '

W przypadku tej usługi pojawia się oczywiste pytanie związane z bezpieczeństwem: skąd wiemy, że notatka jest przysyłana przez osobę, którą deklaruje ona jako nadawcę? Odpowiedź brzmi: nie wiemy. Kwestia identyfikacji jest dość złożonym zagadnieniem i może zostać rozwiązana na wiele różnych sposobów. Wybór najwłaściwszego rozwiązania zależał tu będzie od kontekstu, w którym używana będzie aplikacja. Gdy będzie ona działała w sieci korporacyjnej, zintegrowany mechanizm bezpieczeń­ stwa Windows może okazać się najlepszą opcją, nie sprawdzi się on jednak w przy­ padku aplikacji pracującej w dostępnym publicznie internecie. Sposób radzenia sobie z tego rodzaju problemami jest obecnie szeroko dyskutowany, a j ego opis mógłby z powodzeniem wypełnić cały rozdział. Z uwagi na to, że nasz przykład ma jedynie ilustrować działanie podstawowych mechanizmów platformy WCF, będziemy w nim korzystać z naiwnego modelu zaufania w kwestii tożsamości: użytkownicy mogą tu podawać się, za kogo tylko chcą, a nasza aplikacja przyjmie to za dobrą monetę.

Testowy host i kl ient WCF Możemy teraz zbudować i uruchomić aplikację, naciskając w tym celu klawisz F5 lub wybie­ rając z menu polecenie Debug/Start Debugging. W normalnej sytuacji przy próbie uruchomie­ nia projektu biblioteki otrzymalibyśmy komunikat błędu, ponieważ nie da się uruchomić pliku DLL. Środowisko Visual Studio wie jednak, że jest to projekt WCF, i zapewnia specjalny mecha­ nizm, za pomocą którego da się uruchamiać i testować biblioteki tego typu. Gdy uruchomimy swój projekt, w okolicach paska zadań systemu Windows pojawi się okienko podpowiedzi, które zostało pokazane na rysunku 13.3.

Rysunek 13.3. Testowy host usługi WCF Testowy host usługi WCF (ang. WCF Service Host lub W c fSvcHost, jak nazwa ta jest skracana w okienku podpowiedzi) to program zapewniany przez Visual Studio, który ładuje naszą biblio­ tekę DLL WCF i udostępnia jej usługi lokalnie na potrzeby debugowania. Visual Studio uru­ chamia również drugi program będący testowym klientem WCF (ang. WCF Test Client) - jest to aplikacja Windows zapewniająca interfejs użytkownika do wywoływania operacji ofero­ wanych przez naszą usługę w celu umożliwienia sprawdzenia jej działania . Jak widać na rysunku 13.4, program ten wyświetla w postaci drzewa listę wszystkich usług zdefiniowanych w naszym projekcie i wszystkich operacji dostępnych w ramach każdej z usług. (Jeśli Czytelnik usunął ze swojego kodu niepotrzebny interfejs I Serv i cel, o którym była mowa wcześniej, ujrzy tu tylko jedną usługę) . 486

I

Rozdział 13. Sieci



http :/Jlocalhost : 3732/Design_Time_Addresses/Chat ServerUbrary/Service 1 /mex

:_ ··

1 Service 1 (lllf S Htt p Binding_I Service 1 )

'·· ·· -= '-- ·· -=

--

Get Data O Get Data lJsing DataContractO

Config File

Ę:J S° IChat Service (lll/ S Http Binding_IChat Service) : ' -= Post Note() L B Config File

http :/Jlocalhost : 3732/Design_Ti m e _Addresses/Chat ServerUbrary/Chat Service/mex

··

·· ··

Rysunek 13.4. Lista usług wyświetlana przez program WCF Test Client Testowy klient odnalazł zarówno oryginalną usługę Serv i cel, którą postanowiliśmy ignorować, jak i dodaną przez nas usługę ChatServi ce. Dwukrotne kliknięcie elementu Pos tNote, który repre­ zentuje operację zdefiniowaną przez nas dla usługi komunikatora, powoduje wyświetlenie w prawej części okna programu panelu, za pomocą którego można wypróbować działanie usługi - zadanie testowego klienta polega na umożliwieniu nam wywołania operacji zapew­ nianych przez usługę bez konieczności pisania w tym celu całego programu. Na rysunku 13.5 przedstawiony został ten panel z przykładowymi wartościami argumentów. Gdy spojrzymy na kolumnę Value, zobaczymy argumenty przeznaczone dla parametrów from i note operacji PostNote - wartości te możemy po prostu wpisać bezpośrednio do odpowiednich pól kolumny Value. Post Note

I

Request Name

Value

from

Łukasz

note

Res pon se

Wnaj, świeciel G System . String

IO �tart a new proxy Value

Name

Formatted

Type System . String

I XM L

I

[nvoke

I

Type

I

Rysunek 13.5. Przekazywanie argumentów za pomocą programu WCF Test Client Kliknięcie przycisku Invoke powoduje wywołanie operacji PostNote udostępnianej przez usługę. Możemy tu sprawdzić, czy informacja wprowadzona w polach programu WCF Test Client została prawidłowo przesłana, zaglądając na panel Output środowiska Visual Studio, czyli tam, gdzie pojawia się tekst przekazany metodzie Debug . Wri t e l i n e . (Panel ten można wyświetlić za pomocą odpowiedniego polecenia znajdującego się w menu View, jeśli nie jest on widoczny) . Panel Output jest zapełniony różnymi komunikatami, dlatego będziemy musieli dokładnie

Platforma WCF

I

487

przejrzeć widoczne w nim teksty, ale gdzieś pośród tego szumu powinno nam się udać odna­ leźć wartości argumentów przekazanych parametrom from oraz note, które mogą wyglądać na przykład tak: Łu kas z : Wi taj , świ e c i e

'

. '

1.---1.1"'.

:

Jeśli samodzielnie spróbujemy wykonać tę operację, niewykluczone, że w oknie pro­ gramu WCF Test Client zobaczymy komunikat błędu. Stanie się tak, gdy w środowisku Visual Studio ustawiliśmy jakieś punkty wstrzymania - program klienta przekroczy czas oczekiwania, jeśli zbyt długo zmarudzimy na którymś z tych punktów. W przy­ padku systemów sieciowych typowym działaniem jest rezygnacja z wykonania danej operacji po upływie określonego czasu. Jeśli klient nie otrzyma odpowiedzi, błąd może mieć rozmaite przyczyny - problem może dotyczyć połączenia i leżeć po stronie lokalnej lub po stronie serwera bądź też gdzieś pośrodku. Być może serwer odcięty jest od sieci lub jest po prostu zbyt obciążony, aby odpowiedzieć na żądanie. Klient nie jest w stanie w prosty sposób ustalić przyczyny - wie on tylko tyle, że nie otrzymuje odpowiedzi. Z tego powodu WCF poddaje się po upływie minuty i zwraca wyjątek. Program WCF Test Client informuje o tym za pomocą odpowiedniego okienka dialo­ gowego błędu.

Gdy klient testowy otrzyma odpowiedź od usługi, zasygnalizuje to w dolnej części panelu ope­ racji. Typ wartości zwracanej przez naszą operację Pos tNote to voi d, co oznacza, że odsyła ona pustą odpowiedź. (Nadal jednak przesyła ona odpowiedź w celu poinformowania, że operacja została zakończona . Odpowiedź ta nie zawiera po prostu żadnych danych) . Być może Czytelnik zastanawia się, jak wyglądają komunikaty przesyłane pomiędzy klientem a serwerem, a jeśli nie, to zalecamy zainteresowanie się takimi kwestiami. Trudno jest bowiem projektować dobre, nietrywialne systemy rozproszone (a już zupełnie nie da się diagnozować problemów, które mogą się w nich pojawić), gdy nie mamy pojęcia, jaką postać mają przesyłane przez nie komunikaty. Niestety niektórzy programiści czują się świetnie, nie mając zielonego pojęcia na temat tego rodzaju spraw, lecz skutkuje to tym, że często utykają oni w miejscu i muszą prosić o pomoc osoby, które zawsze dokładnie wiedzą, co się dzieje, gdy coś idzie źle. Jeśli zatem wolimy należeć do kręgu magików, którzy potrafią rozwiązywać tego typu pro­ blemy, musimy dowiedzieć się, jak naprawdę wyglądają komunikaty przesyłane za pośred­ nictwem sieci. Komunikaty te możemy zobaczyć w oknie programu WCF Test Client. W tym celu należy kliknąć kartę XML widoczną w dolnej części panelu operacji . Szczegółowe obja­ śnienie struktury wyświetlanych tu komunikatów WCF wykracza poza zakres materiału pre­ zentowanego w niniejszej książce, która jest w końcu poświęcona jedynie językowi C#, łatwo jednak zauważyć, gdzie pojawiają się dane, które wysłaliśmy w tym przykładzie. Jeśli Czytel­ nik jest zainteresowany dalszymi informacjami na ten temat, dobrą lekturą na początek będzie z pewnością napisana przez Michele Leroux Bustamante książka Learning WCF (http:/joreilly.com/catalog/9780596101626/), zaś bardziej zaawansowanych zagadnień możemy poszukać w książce Programming WCF Services (http:j/oreilly.com/catalog/9780596526993/), którą napisał Juval Lawy (obydwie pozycje ukazały się nakładem wydawnictwa O'Reilly) . Programy WCF Service Host oraz WCF Test Client przydają się do przeprowadzania pro­ stych testów interaktywnych, ale prawdziwe użyteczne usługi muszą działać w jakimś bardziej stałym miejscu. Z tego powodu przyjrzymy się teraz, w jaki sposób programy .NET mogą zapewniać środowisko pracy usługom WCF.

488

I

Rozdział 13. Sieci

'

. .

, ..._-__-� ·

Jeśli zamierzamy na poważnie wykorzystywać komunikację sieciową, jedną z naj­ lepszych rzeczy, jakie możemy zrobić, będzie zapoznanie się z narzędziem, które umożliwia badanie zawartości komunikatów wysyłanych i odbieranych przez kartę sieciową naszego komputera. Aplikacja Network Monitor firmy Microsoft jest dostępna za darmo podobnie jak oferowany na zasadzie otwartego kodu program Wireshark (http://www.wireshark.org/) . Obydwa te rozwiązania mogą się wydawać na początku dość straszne z uwagi na ogromną liczbę szczegółowych informacji, które udostęp­ niają, ale stanowią one nieodzowne narzędzia do diagnozowania problemów komu­ nikacyjnych, ponieważ pokazują dokładnie, jakie komunikaty zostały przesłane i co zawierały.

Udostępnian ie usługi WCF Usługi WCF oferują dużą elastyczność w kwestii tego, gdzie rezydują - może je udostępniać zwykła aplikacja .NET, dlatego w środowisku Visual Studio nie istnieje specjalny szablon projektu dla hosta usługi WCF. Usługi tego rodzaju możemy osadzać w aplikacjach WWW ASP.NET, Windows Service, aplikacjach konsolowych, a nawet w aplikacjach wyposażonych w GUI zbudowane przy użyciu Windows Forms lub WPF. Każdy proces, który może zaak­ ceptować przychodzące połączenia sieciowe, powinien sprawdzić się w tej roli doskonale, dlatego prawdopodobnie jedyny program, który nie będzie w stanie gościć usługi WCF, to proces, którego ograniczenia bezpieczeństwa będą uniemożliwiały nawiązywanie połączeń przychodzących, taki jak przeglądarka internetowa . (Klienty Silverlight mogą na przykład nawiązywać wychodzące połączenia WCF, lecz nie mogą udostępniać usług, które akceptują połączenia przychodzące) . Aplikacje WWW ASP.NET stanowią szczególnie popularną grupę środowisk dla usług WCF, ponieważ technologia ta rozwiązuje wiele problemów, z którymi musimy sobie radzić w przy­ padku usług sieciowych. Aplikacje WWW są automatycznie udostępniane podczas rozruchu maszyny, nie ma więc potrzeby, aby ktokolwiek logował się w systemie w celu uruchomienia odpowiedniego programu. Technologia ASP.NET zapewnia solidne środowisko obsługi. Jest ono w stanie ponownie uruchamiać usługi po wystąpieniu błędów oraz integrować je z diagno­ stycznymi systemami zarządzania, dzięki czemu administratorzy systemu mogą łatwo dostrzec wszelkie problemy. Istnieją również doskonale sprawdzone sposoby odpowiedniej dystrybucji zadań związanych z aplikacjami WWW pomiędzy wieloma serwerami sieciowymi. ASP.NET jest ponadto w stanie korzystać z zapewnianych przez IIS możliwości ochrony takich jak zinte­ growane uwierzytelnianie . Jednak ASP.NET nie zawsze musi okazać się właściwym wyborem. Usługa WCF osadzona w aplikacji WWW nie jest w stanie korzystać z pełnego spektrum protokołów obsługiwanych przez platformę, bowiem komunikaty przychodzące muszą docierać za pośrednictwem pro­ tokołu HTTP. Poza tym aplikacje WWW przeważnie wykonują kod jedynie podczas aktyw­ nego obsługiwania żądania od klienta . Jeśli zatem musimy przeprowadzać długie operacje, które trwają nawet wówczas, gdy nie funkcjonują żadne połączenia z klientami, osadzenie usługi w aplikacji WWW może okazać się złym pomysłem, ponieważ w przypadku niektórych konfiguracji mechanizm ASP .NET będzie od czasu do czasu ponownie uruchamiał aplikacje WWW lub nawet całkowicie je wyłączał, gdy przez pewien czas nie pojawią się żadne przy­ chodzące żądania . W pewnych sytuacjach sensowniejsze może być opracowanie własnego hosta . Dobrym wyborem może się tu okazać Windows Service, ponieważ jest on w stanie automatycznie rozpoczynać pracę wraz z uruchomieniem maszyny. Platforma WCF

I

489

Czasami dobrym rozwiązaniem jest osadzenie usługi WCF wewnątrz zwykłej aplikacji Win­ dows. Wyobraźmy sobie aplikację WPF oferującą możliwość wyświetlania pewnego rodzaju reklam w oknie sklepowym prezentowanym na ekranie komputera - może się to przydać do wbudowania odpowiedniej usługi WCF w celu kontrolowania prezentowanej treści bez konieczności uzyskiwania fizycznego dostępu do maszyny. Techniki wykorzystywane do osadzania usług we wszystkich tych przypadkach wyglądają dość podobnie. Z uwagi na to, że w tej części książki nie będziemy się jeszcze zajmować tech­ nologią ASP.NET, uprościmy sobie sprawę, osadzając naszą usługę w aplikacji konsolowej . Przeniesienie jej do innych środowisk w późniejszym czasie będzie bardzo łatwe, ponieważ sama usługa ma postać oddzielnego projektu DLL - równie dobrze będzie go można dodać do aplikacji WWW, jak i do Windows Service. Niezależnie od typu hosta jedną z najważniejszych rzeczy związanych z osadzaniem WCF jest plik konfiguracji .

Konfiguracja WCF Gdy przyjrzymy się projektowi Ch atServerL i brary, zauważymy w nim plik App.config. Plik tego rodzaju lub jego odpowiednik WWW o nazwie web.config znajdziemy w wielu różnych typach aplikacji .NET, jednak występowanie pliku App.config w projekcie biblioteki stanowi pewną anomalię . Jak wskazuje już sama nazwa, pliki konfiguracji aplikacji mają przecież konfiguro­ wać aplikacje, a biblioteka nie jest przecież aplikacją. W normalnym przypadku dodanie pliku App.config do projektu tworzącego bibliotekę DLL nie skutkuje niczym użytecznym, ale pro­ jekty WCF są tu wyjątkiem, a przyczynę stanowi program WCF Service Host, który przed­ stawiliśmy wcześniej. Testowy host ładuje zawartość tego pliku do swojej konfiguracji aplikacji. W zwykłym przypadku pliki konfiguracji aplikacji odnoszą się do projektów tworzących apli­ kacje wykonywalne lub projektów WWW. Plik App.config w projekcie WCF Service Library jest używany wyłącznie przez program WCF Service Host. Konfigurację tę będziemy zawsze musieli skopiować do swojej rzeczywistej aplikacji udostępniającej usługę.

Aby dysponować jakąś aplikacją do skonfigurowania, do przykładowej solucji WcfChat dodamy aplikację konsolową o nazwie ChatHos t . Aplikacja ta będzie udostępniała naszą usługę WCF, dlatego dodamy w niej odwołanie do biblioteki ChatServerL i brary . A skoro zamiast programu WcfSvcHost w roli hosta będziemy od tej pory używać naszej aplikacji konsolowej, powinniśmy skopiować konfigurację zapisaną w pliku App.config projektu C h atServerL i brary do pliku App.config związanego z projektem ChatHo s t . (Po wykonaniu tej operacji będzie można usunąć plik App.config związany z projektem C h atServerL i brary) . Przyjrzyjmy się każdej części pliku App.config, aby zrozumieć jego działanie. Cała jego zawar­ tość znajduje się w elemencie głównym conf i gurat i on - wszystkie pliki App.config oraz web.config mają ten element niezależnie od tego, jakiego rodzaju aplikację tworzymy. Pierwszy element potomny ma następującą postać:

490

I

Rozdział 13. Sieci

W naszym przykładzie nie jest on potrzebny, dlatego można go bez konsekwencji usunąć. Szablon WCF Service Library dodaje ten element na wypadek, gdybyśmy zamierzali osadzić projekt w aplikacji WWW, gdyż umożliwia on debugowanie aplikacji tego rodzaju. Z racji tego, że nie tworzymy aplikacji WWW, element ten po prostu jest tu zbędny. Kolejnym elementem jest system . s erv i c eModel - w gruncie rzeczy cała reszta pliku App.config znajduje się właśnie w tym elemencie . Stanowi on miejsce, w którym niezależnie od typu aplikacji hosta przechowywana jest konfiguracja WCF. Pierwszym elementem należącym do właściwej konfiguracji WCF jest s erv i ces . Zawiera on po jednym elemencie serv i ce dla każdej usługi udostępnianej przez program. Środowisko Visual Studio dodało tu dwa takie elementy: jeden z nich związany jest z nieużywaną przez nas usługą Serv i cel, a drugi dotyczy usługi C hatServi ce, którą opracowaliśmy. Z uwagi na to, że usługa Serv i cel nie jest nam do niczego potrzebna, możemy pozbyć się pierwszego elementu s erv i c e oraz całej jego zawartości . Pozostanie po tym już tylko element s erv i ce związany z naszą usługą ChatServi ce. Zaczyna się on w następujący sposób:

Atrybut n ame zawiera nazwę klasy, która implementuje usługę, wraz z odpowiednią prze­ strzenią nazw. Wewnątrz elementu s erv i ce znajduje się kilka elementów endpoi n t . Czytelnik pamięta z pewnością, że stwierdziliśmy wcześniej, iż WCF jest w stanie udostępniać implemen­ tację pojedynczej usługi za pośrednictwem wielu różnych mechanizmów komunikacji . Każdy z mechanizmów, które mają być obsługiwane, dodaje się właśnie za pomocą odpowiedniego „punktu końcowego", czyli elementu endpoi n t . Oto pierwszy element tego typu, który dodało dla nas środowisko Visual Studio: < i dent i ty>

Punkt końcowy jest definiowany przez trzy rzeczy: przez adres (ang. address), przez wiązanie (ang. binding) oraz przez kontrakt (ang. contract) . Wspólnie określane są one czasami jako ABC platformy WCF. Adres stanowi zwykle łańcuch URL - jest to adres, którego będzie używał klient, aby połączyć się z usługą . W tym przypadku adres jest pusty, co oznacza, że WCF ma wydedukować go na nasz użytek; niebawem przekonamy się, jak się to odbywa. Wiązanie wskazuje technologię komunikacji, której platforma WCF będzie używała w przypadku tego punktu końcowego. W naszym przykładzie zastosowane zostało jedno z wbudowanych wiązań o nazwie wsHttpBi ndi ng. Litery „ws" oznaczają, że są tu wykorzystywane różne standardy usług WWW, których nazwy zaczynają się od WS-. Wiązanie to obsługuje zatem standardy takie jak WS-ADDRESSING oraz WS-SECURITY. Jest to wiązanie wielofunkcyjne i może ono korzystać z możliwości, z którymi nie będą sobie w stanie poradzić niektóre programy klienc­ kie - nie jest ono na przykład obsługiwane przez technologię Silverlight. Jeśli chcielibyśmy zastosować profil podstawowy, który jest obsługiwany przez klienty Silverlight, powinniśmy w tym miejscu podać nazwę bas i cHt t p B i n d i ng. W przypadku naszej aplikacji można jednak pozostawić to wiązanie bez zmian.

Platforma WCF

I

491

Ostatni z występujących tu atrybutów, con t ra ct, zawiera nazwę interfejsu definiującego kon­ trakt operacji dla naszej obsługi . O kontraktach była już w tym rozdziale mowa . Atrybut ten odnosi się do interfejsu, który został przedstawiony na listingu 13.1 i zmodyfikowany na listingu 13.2. Wewnątrz elementu endpo i n t znajduje się element i d ent i ty . Jest on przeznaczony do użycia w scenariuszach, w których usługa musi być w stanie bezpiecznie legitymować się klientowi przykładem może tu być aplikacja bankowa, w przypadku której użytkownik chciałby mieć całkowitą pewność, że naprawdę łączy się ze swoim bankiem. W naszej przykładowej apli­ kacji nie będziemy jednak zagłębiać się w kwestie bezpieczeństwa, dlatego możemy usunąć element i dent i ty oraz całą jego zawartość. Gdy utworzyliśmy usługę C hatServi ce, środowisko Visual Studio dodało do pliku App.config drugi punkt końcowy:

Umożliwia on przeprowadzanie działania określanego mianem wymiany metadanych (ang. metadata exchange) . Ten punkt końcowy nie zapewnia kolejnego sposobu użycia usługi zamiast tego pozwala na uzyskanie jej opisu (ang. description) . Skorzystamy z niego później, gdy będziemy budować klienta naszej usługi . Wreszcie po dwóch elementach endpoi n t pojawia się element host, który został pokazany na listingu 13.3. (Zawiera on bardzo długi zapis, który został tu podzielony między dwa wiersze kodu, aby dało się go zmieścić na stronie) . Ten element h o s t należy jeszcze do elementu s ervi ce, a zatem - podobnie jak dwa wymienione powyżej elementy end poi n t - również opisuje jedną, konkretną usługę: naszą przykładową usługę C hatServi ce.

Listing 13.3. Element host ze standardowym adresem bazowym

Element ten zawiera informacje związane z osadzaniem, które dotyczą wszystkich punktów końcowych danej usługi - to właśnie w ten sposób WCF określa, jakiego adresu ma używać dla każdego z punktów końcowych. Atrybut baseAddre s s jest łączony z zawartością atrybutu a ddres s związanego z danym elementem end po i n t w celu wyznaczenia ostatecznego adresu wykorzystywanego w przypadku tego punktu końcowego. Z uwagi na to, że adres pierwszego punktu końcowego jest pusty, jego adresem będzie podana tutaj wartość atrybutu baseAddres s . Adres drugiego z punktów końcowych to mex, dlatego ten punkt usługi będzie dostępny pod rzeczywistym adresem o postaci: http: //l ocal hos t : 8732/Des i gn_T i me_Addresses/ChatServerli brary/ChatServ i ce/mex

Jeśli Czytelnik zastanawia się, dlaczego środowisko Visual Studio wybrało dla naszej usługi ten dość dziwnie wyglądający adres jako standardowy adres bazowy, powinien zapoznać się z zawartością ramki zamieszczonej poniżej .

492

I

Rozdział 13. Sieci

Punkty końcowe, bezpieczeństwo i uprawn ienia ad ministracyjne Każdy proces udostępniający usługi WCF musi być w stanie akceptować przychodzące komuni­ katy sieciowe. Gdy korzystamy z wiązania pracującego w oparciu o protokół HTTP, takiego jak standardowe wiązanie wsHttpBi ndi ng lub wiązanie zgodne z profilem podstawowym bas i cHttpBi n­ di ng, host usługi prawdopodobnie nie jest jedynym programem działającym na maszynie, który chce odbierać przychodzące żądania HTTP. System Windows zapewnia mechanizm określający, które aplikacje mają je obsługiwać. Programy mogą zgłaszać zamiar nasłuchiwania żądań nadcho­ dzących na określone adresy URL lub na adresy zaczynające się od wybranego przedrostka. Programy mogą jednak nie mieć uprawnień do nasłuchiwania komunikatów nadchodzących na stare adresy. Niektóre aplikacje mogą rezerwować określone przedrostki URL, uniemożliwiając w ten sposób innym aplikacjom obsługiwanie zgłaszanych na nie żądań. Jeśli na przykład używamy edycji Windows wyposażonej w oprogramowanie Windows Media Center, nasz system umożliwi urządzeniom rozszerzającym Media Center (takim jak Xbox 360) połączenie się z adresem http://:10243/WMPNSSv4/ i Media Center zarezerwuje ten adres, korzystając z funkcji zabezpieczeń systemu Windows. Będziemy mogli wówczas zastosować do przedrostka URL listę kontroli dostępu (ang. access control list - ACL), aby określić, które konta mają prawo do nasłuchi­ wania przychodzących żądań na dowolnym adresie URL, który zaczyna się od tego zarezerwo­ wanego łańcucha znakowego. Możliwość korzystania z tego przedrostka URL otrzyma jedynie pro­ gram uruchomiony za pomocą specjalnego konta użytkownika wykorzystywanego przez usługi Windows Media. To, które adresy URL zostały zarezerwowane przez które konta użytkowników, możemy sprawdzić za pomocą następującej komendy uruchomionej z linii poleceń: netsh http show url acl (W przypadku korzystania z systemu Windows 2003, Windows XP lub której z wcześniejszych wersji środowiska będziemy musieli zastosować inny program wywoływany poleceniem httpcfg, ale dla systemu Windows Vista lub któregoś z kolejnych właściwą komendą jest netsh ) .

Jeśli nasz program nie zostanie uruchomiony z uprawnieniami administracyjnymi, nie będzie w sta­ nie nasłuchiwać komunikatów nadchodzących na adresy URL, do których nasze konto użytkow­ nika nie uzyskało dostępu (w praktyce nie będzie miał więc możliwości korzystania z większości adresów URL) . Uruchamianie programu z uprawnieniami administracyjnymi przypomina trochę bieganie z nożyczkami w dłoni, dlatego powinno się tego unikać. Sytuacja ta wydaje się jednak sygnalizować nam większy problem, który może dotyczyć programistów tworzących usługi z wyko­ rzystaniem platformy WCF. Gdy wdrażamy naszą aplikację w ostatecznym środowisku produk­ cyjnym, jej instalator może skonfigurować odpowiednią listę ACL dla adresu URL na docelowej maszynie, aby zapewnić, że program będzie w stanie należycie nasłuchiwać komunikatów. Co jednak powinniśmy zrobić w przypadku maszyny, za pomocą której tworzymy kod? Aby ułatwić programistom życie, instalator środowiska Visual Studio definiuje specjalny zakres adresów za pomocą listy ACL, która otwiera go dla każdego użytkownika zalogowanego na maszynie. Dzięki temu możemy nasłuchiwać komunikatów napływających na dowolny adres zaczynający się od http://localhost:8732/Design_Time_Addresses/, nawet gdy jesteśmy zalogowani na konto niead­ ministracyjne. To właśnie jest powodem, dla którego środowisko Visual Studio wybiera adres bazowy przedstawiony na listingu 13.3 - tym sposobem nie musimy uruchamiać programu z większymi uprawnieniami.

Po elemencie s erv i ces w pliku App.config znajduje się element behavi ars zawierający element s erv i ceBehavi ars, któiy z kolei zawiera element behavi or. Ta część pliku konfiguracyjnego umoż­ liwia włączanie i wyłączanie różnych możliwości WCF. Czytelnik zastanawia się z pewnością,

Platforma WCF

I

493

dlaczego ustawienia te nie należą do części s erv i ces . Powodem jest to, że moglibyśmy chcieć udostępniać wiele usług korzystających ze wspólnej konfiguracji działania. Dzięki temu można by zdefiniować pojedynczy nazwany element behavi or, a następnie sprawić, aby atrybuty behavi or "+Con fi gu rat i on związane z wieloma elementami serv i ce odwoływały się do tej definicji, redu­ kując w ten sposób bałagan w swoim pliku konfiguracyjnym. Możemy też, jak ma to miejsce w tym przypadku, utworzyć nienazwany element behavi or, który określa standardowy spo­ sób działania dotyczący wszystkich usług udostępnianych w ramach naszego procesu hosta . Z uwagi na to, że udostępniamy tylko jedną usługę, rozwiązanie to nie oferuje nam szczegól­ nych korzyści, jednak separacja ta może okazać się naprawdę przydatna, gdy w grę wchodzi wiele usług. Element behavi or zapewniany przez środowisko Visual Studio zawiera komentarze informujące, co można w nim zmienić i dlaczego, jednak po zredukowaniu go do zasadniczej zawartości otrzymujemy poniższy kod.

Fragment ten konfiguruje dwie opcjonalne możliwości. Pierwsza z nich ma związek z wymianą metadanych, o której wspominaliśmy już wcześniej - zapewnia ona, że opis usługi może zostać pobrany w określony sposób. I znów, do tematu metadanych powrócimy później, gdy zaj­ miemy się tworzeniem klienta, dlatego póki co możemy swobodnie pominąć ten element. Użycie drugiego znajdującego się tu elementu - s erv i ceDebug - nie odnosi żadnego skutku, ponieważ ustawia on standardową wartość właściwości i n c l udeExcep t i onDeta i l I n Faul ts, czyli Fal se. Nic nie zmieni się, jeśli po prostu usuniemy go z pliku konfiguracji . Środowisko Visual Studio umieszcza tu ten element tylko po to, aby pomóc nam na etapie debugowania kodu. Czasami może się okazać przydatne tymczasowe przypisanie tej właściwości wartości True, a umieszczenie tego wpisu w pliku uwalnia nas od konieczności żmudnego wyszukiwania nazwy odpowiedniego ustawienia. Podanie tu wartości True powoduje, że w sytuacji, gdy nasza usługa zgłosi wyjątek, wszystkie dotyczące go szczegóły, w tym obraz stosu, zostaną odesłane klientowi w odpowiedzi. Ogólnie rzecz biorąc, nigdy nie powinniśmy korzystać z tej możliwości, ponieważ wysyłanie śla­ dów stosu do klientów ujawnia szczegóły implementacji naszego systemu. Jeśli niektórzy użytkownicy programu są hakerami i mają złe intencje, może im to ułatwić włamanie się do systemu. (Z technicznego punktu widzenia, jeśli nasz system jest całkowicie zabezpieczony, ślad stosu nie powinien im szczególnie pomóc, ale kiedy ostatnio Czytelnik słyszał o w pełni bez­ piecznym systemie komputerowym? Rozsądnie jest przyjąć założenie, że każde oprogramo­ wanie ma pewne wady w kwestii zabezpieczeń, dlatego im mniej pomagamy hakerom, tym lepiej . Filozofia ta jest często określana jako redukowanie podatnej na atak powierzchni systemu) . Choć zwykle nie będziemy chcieli wysyłać śladów stosu przez sieć, skorzystanie z tej możliwo­ ści może nieraz uprościć diagnozowanie problemów w czasie tworzenia programu, dlatego aby ułatwić sobie życie, możemy tymczasowo włączyć tę funkcję. Koniecznie pamiętajmy jednak, aby wyłączyć ją, zanim rozpoczniemy dystrybucję aplikacji!

494

I

Rozdział 13. Sieci

To już wszystko, co środowisko Visual Studio umieszcza w pliku konfiguracji. Stanowi to tylko niewielką część ustawień, które można określić w tym miejscu, ale niniejsza książka nie jest poświęcona szczegółowemu opisowi platformy WCF, dlatego pozwolimy sobie poprze­ stać na tym. Nasz program na tym etapie nadal nie jest gotów do udostępniania usługi. Oprócz odpowied­ nich wpisów w pliku konfiguracji aplikacji musimy w naszym programie zapewnić wywołanie API informujące platformę WCF, że ma on udostępniać usługi . (Pisząc aplikację WWW, nie musielibyśmy tego robić - wystarczyłyby wówczas ustawienia zdefiniowane w pliku konfi­ guracji web.config - jednak w przypadku wszystkich innych typów aplikacji ten ostatni krok jest niezbędny) . Do projektu ChatHo s t musimy zatem dodać odwołanie do komponentu Sys t em . Servi ceModel stanowiącego główną bibliotekę klas DLL platformy .NET dla WCF. Ponadto w górnej części pliku Program.es powinniśmy umieścić dyrektywy us i ng Sys tern . Serv i ceMode l oraz us i ng Ch atSer "+verl i brary . Następnie będziemy mogli opracować metodę Ma i n podobną do tej, która została przedstawiona na listingu 13.4.

Listing 13.4. Osadzanie usługi WCF s t at i c vo i d Mai n (s tri ng O arg s ) { us i ng (Serv i ceHost host = new Serv i ceHost (typeo f (ChatServ i ce) ) ) { hos t . Open ( ) ; Consol e . Wr i teli ne ( " Us ł uga gotowa do pracy " ) ; Consol e . ReadKey () ;

Kod ten tworzy obiekt Serv i ceHost udostępniający usługę C hatServi ce. Platforma WCF zała­ duje konfigurację z pliku App.config, aby określić sposób, w jaki ma być oferowana ta usługa, my zaś musimy zapewnić, że nasz program nie zostanie od razu zamknięty - usługa będzie dostępna tylko tak długo, jak długo będzie działał program, który ją udostępnia. Z tego powodu pozwalamy aplikacji pracować aż do czasu naciśnięcia przez użytkownika jakiegoś klawisza. Jeśli Czytelnik chce wypróbować działanie tego kodu, powinien sprawić, aby aplikacja kon­ solowa była programem, który będzie standardowo uruchamiany przez środowisko Visual Studio - w tej chwili tak nie jest, ponieważ to projekt ChatServerL i brary jest uznawany za pro­ gram rozruchowy. Odpowiedniej zmiany można dokonać, klikając widoczną w panelu Solution Explorer pozycję ChatHost prawym przyciskiem myszy i wybierając z menu kontekstowego polecenie Set as Startup Project. Dzięki temu naciśnięcie klawisza F5 spowoduje uruchomienie tego programu i wyświetlenie na ekranie okna konsoli, w którym zostanie pokazany komuni­ kat „Usługa gotowa do pracy", gdy tylko Serv i ceHost zacznie działać. Co teraz? Nie mamy już do dyspozycji programu WCF Test Client, ponieważ środowisku Visual Studio wydaje się, że mamy zamiar uruchamiać normalną aplikacją konsolową. Z uwagi na to, że standardowe wiązanie wsHttpBi n d i ng punktu końcowego naszej usługi wykorzystuje proto­ kół HTTP, moglibyśmy odwołać się do niego za pomocą przeglądarki internetowej . Pamię­ tajmy, że usługa dostępna jest pod adresem określonym w pliku konfiguracji: http: //l ocal hos t : 8732/Des i gn_T i me_Addresses/ChatServerli brary/ChatServ i ce/

Platforma WCF

I

495

Jeśli nie usunęliśmy wcześniej pliku App.config należącego do projektu ChatServerLi brary, próba kompilacji programu spowoduje zgłoszenie błędu. Nawet gdy wskażemy pro­ jekt ChatHost jako aplikację rozruchową, środowisko Visual Studio nadal będzie próbo­ wało uruchomić program WCF Service Host dla projektu ChatServerLi brary. Miałoby to sens w przypadku solucji zawierającej jedynie klienta WCF oraz bibliotekę DLL usługi. Tutaj jednak rozwiązanie to nie jest zbyt przydatne, ponieważ w efekcie mamy dwa programy próbujące udostępnić ten sam serwer pod tym samym adresem URL nawet gdy jednemu z nich uda się to zrobić, drugi zgłosi błąd. -

Jeśli nie chcemy usuwać pliku App. config związanego z tym projektem, możemy wyłączyć program WCF Service Host, otwierając okno Properties projektu ChatServer 4Li brary, a następnie przechodząc na kartę WCF Options i usuwając zaznaczenie z odpo­ wiedniego pola opcji.

Jeśli chodzi o ścisłość, usługa nie jest tak naprawdę dostosowana do obsługi przeglądarki inter­ netowej. Cały ten rozdział poświęcony został sposobom zapewniania możliwości komunikacji pomiędzy programami, nie zaś zagadnieniom budowania interfejsów użytkownika WWW. Platforma WCF jest tu jednak raczej tolerancyjna - zauważa, że połączyliśmy się z usługą za pomocą przeglądarki, i próbuje nam pomóc. Generuje w tym celu stronę WWW, przy użyciu której cierpliwie wyjaśnia, że to, z czym udało nam się połączyć, jest usługą, oraz pokazuje nam, jak należy napisać kod, który będzie w stanie skorzystać z tej usługi . I właśnie tym zajmiemy się w następnej kolejności .

Pisan ie kl ienta WCF Aby móc skorzystać z naszej usługi, musimy opracować odpowiedni program kliencki . Rów­ nież tu w celu uproszczenia zadania sprawimy, aby miał on postać aplikacji konsolowej. Dodamy ją do tej samej solucji, nadając projektowi nazwę ChatCl i ent. (Oczywiście jeśli w dalszym ciągu testujemy program ChatHost i nadal działa on w debuggerze, musimy go zatrzymać) . Gdy prawym przyciskiem myszy klikniemy element znajdujący się na liście References widocz­ nej w panelu Solution Explorer środowiska Visual Studio, zauważymy, że w menu konteksto­ wym oprócz znanego nam już polecenia Add Reference będzie dostępne nowe - Add Service Reference. Skorzystamy z niego, aby połączyć tworzonego klienta z naszym serwerem za pośred­ nictwem platformy WCF. Jak widać na rysunku 13.6, okno dialogowe Add Service Reference zawiera przycisk Discover, za pomocą którego można zlokalizować usługi obecne w bieżącej solucji . Niestety, gdybyśmy na typ etapie postanowili skorzystać z niego w naszym kodzie, narzędzie to poinformowałoby nas, że nie udało mu się odnaleźć żadnych usług. Stałoby się tak dlatego, że cały kod udostęp­ niający usługę w ramach aplikacji ChatHost napisaliśmy ręcznie - program Visual Studio nie ma pojęcia o tym, że dodana niedawno aplikacja konsolowa stanowi środowisko działania usługi . Narzędzie przeszukuje zwykle jedynie projekty WWW, gdybyśmy zatem osadzili usługę w aplikacji WWW ASP.NET, udałoby mu się ją znaleźć. W przypadku, z którym mamy tu do czynienia, narzędziu temu trzeba trochę pomóc. Aby środowisko Visual Studio mogło połączyć się z naszą usługą udostępnianą przez aplika­ cję konsolową, usługa musi zostać uruchomiona i działać, zanim otworzymy okno dialogowe Add Service Reference. Najprostszym sposobem osiągnięcia tego jest uruchomienie projektu bez debugowania go. Zamiast więc naciskać klawisz F5, należy wybrać polecenie Debug/Start

496

I

Rozdział 13. Sieci

l\ddres�: htt ://l oca l host:8732/Des1 n_T1 m e_Addresses/C h atServerL1 bra

0 i'.J Ch atServi ce

Servi ces:

Q p er

a ti

o n s:

/C h at5erv1 ce/

... I

§o

I 1 1 Qisrnver H

� IChatService

Rysunek 13.6. Okno dialogowe Add Service Reference

Without Debugging lub skorzystać z kombinacji klawiszy Ctrl+F5. Spowoduje to uruchomienie programu ChatHost bez debugowania, dzięki czemu środowisko Visual Studio będzie w stanie obsługiwać inne zadania takie jak dodawanie odwołania do usługi .



....,.___�_ '

Gdybyśmy w projekcie ChatServerL i brary pozostawili plik App.config, zostałby on odna­ leziony i spowodowałby uruchomienie programu WCF Service Host w momencie kliknięcia przycisku Discover. Bądźmy jednak ostrożni - nasza usługa jest tak naprawdę udostępniana przez aplikację ChatHost i gdy zaczniemy modyfikować ustawienia znajdujące się w związanym z nią pliku App.config (czym zajmiemy się nieco później), duże znaczenie będzie miało to, aby okno dialogowe Add Service Reference odwoływało się do właściwej usługi. To właśnie z tego powodu sugerowaliśmy wcześniejsze usu­ nięcie pliku App.config należącego do projektu biblioteki DLL - pozwala to uniknąć ryzyka przypadkowego skonfigurowania klienta do pracy z niewłaściwym hostem usługi.

Powinniśmy mieć pod ręką adres usługi, a ponieważ jest on dość długi, najłatwiej będzie otwo­ rzyć plik App.config związany z naszym hostem i skopiować ten adres do schowka . (Stanowi on wartość atrybutu baseAddre s s należącego do elementu host ) . Następnie możemy przejść do projektu C h a t Cl i e n t i dodać odpowiednie odwołanie do usługi . Gdy wkleimy adres usługi w pole Address, a następnie klikniemy przycisk Go, po kilku sekundach w znajdującym się w lewej części okna obszarze Services ujrzymy pozycję C hatServi c e . Rozwinięcie tej gałęzi spowoduje wyświetlenie wpisu I ChatServ i ce reprezentującego nasz kontrakt, a wybranie go będzie skutko­ wało pokazaniem operacji udostępnianej przez ten kontrakt, czyli Pos tNote, tak jak zostało to zaprezentowane na rysunku 13.6. Lista usług, kontraktów i operacji widoczna w oknie dialogowym Add Service Reference przy­ daje się do weryfikacji, czy mamy dostęp do usługi, na której nam zależy, jednak znaczenie prezentowanych tu informacji jest dużo większe - świadczą one o sposobie, w jaki systemy komunikują się w ramach WCF. Jak Czytelnik z pewnością pamięta, aby opisać operacje, które nasza usługa zapewnia swoim klientom, zdefiniowaliśmy wcześniej odpowiedni kontrakt. Aby klient mógł skutecznie komunikować się z serwerem, również on musi mieć swoją kopię tego kontraktu. Dlatego okno dialogowe Add Service Reference najlepiej jest traktować jako narzędzie przechowujące kontrakt opisujący usługę. To temu służy właśnie wpis wymiany metadanych, który widzieliśmy wcześniej w pliku konfiguracji wygenerowanym przez środowisko Visual Studio dla naszej usługi WCF. Wymiana metadanych to po prostu wymyślne określenie tego, że usługa zapewnia pewien sposób udostępnienia klientowi odpowiedniego kontraktu i związanych z nim informacji na jej temat. Okno dialogowe Add Service Reference korzysta z nich w celu takiego skonfigurowania aplikacji klienta, aby mogła ona komunikować się z usługą, oraz w celu zapewnienia jej kopii kontraktu.

Platforma WCF

I

497

Aby przekonać się o efektach działania tego mechanizmu, dokończymy uzupełnianie ustawień dostępnych w obrębie okna dialogowego Add Service Reference. W znajdującym się w dolnej części okna polu edycji o nazwie Namespace powinniśmy wpisać nazwę przestrzeni C hatServi ce dzięki temu środowisko Visual Studio umieści odpowiedni kontrakt i wszystkie związane z usługą typy w podanej tu przestrzeni nazw. Gdy klikniemy przycisk OK, w gałęzi projektu widocznej w panelu Solution Explorer pojawi się element Service References, który będzie zawie­ rał wpis o nazwie C hatServi ce. (Teraz, gdy już wykonaliśmy odpowiednie operacje, możemy zatrzymać działanie udostępniającej usługę aplikacji konsolowej, którą uruchomiliśmy wcześniej) .

-

W momencie dodawania odwołania do usługi środowisko Visual Studio generuje nieco kodu. Standardowo jest on ukryty przed naszym wzrokiem, ale możemy mu się przyjrzeć, jeśli tylko chcemy. W górnej części panelu Solution Explorer znajduje się pasek narzędzi. Gdy umieścimy wskaźnik myszy nad jego przyciskami, dzięki odpowiednim podpowiedziom przekonamy się, że nazwa jednego z nich brzmi Show All Files. Przycisk ma charakter przełącznika zmie­ niającego stan przy każdym kliknięciu. Gdy jest wciśnięty, odwołanie do usługi C hatServi ce będzie można rozwinąć, tak jak zostało to pokazane na rysunku 13.7. "

:Servi c e References

� C h ał:Service.wsd l c o nfig u r ati o n svc i nfo � configu ratio n91.svcinfo

" llJJ C h atServiceJ ···

"

.

"' ReJerenc esvcmap � Referenc e. cs

Rysunek 13.7. Wygenerowane przez środowisko Visual Studio pliki widoczne w obrębie odwołania do usługi Najciekawszy jest tu widoczny w obrębie elementu Reference.svcmap plik Reference.cs . Nieda­ leko jego początku znajduje się kopia interfejsu I ChatServ i ce, czyli kontraktu, który napisaliśmy wcześniej: [System . CodeDom . Comp i l er . GeneratedCodeAttri bute ( " System . Serv i ceModel " , " 4 . 0 . 0 . 0 " ) ] [System . Serv i ceModel . Serv i ceContractAttri bute (Confi gurat i onName= " ChatServ i ce . I ChatServ i ce " ) ] publ i c i nterface I ChatServ i ce { [System . Serv i ceModel . Opera t i onContractAttri bute ( Act i on = " http : //tempuri . org/ I ChatServ i ce/PostNote " , Repl yAct i on = " http : //tempuri . org/I ChatServ i ce/PostNoteResponse " ) ] vo i d Pos tNote ( s t r i ng from , stri ng note) ;

Kod ten jest nieco bardziej skomplikowany niż w oryginale, ponieważ środowisko Visual Studio dodało tu kilka różnych atrybutów, ale dość łatwo można wskazać w nim wartości, które zostały standardowo uzupełnione przez platformę WCF3 . Pomijając te dodatkowe szczegóły, widać jednak, że jest to zasadniczo kopia kodu, który znajduje się w oryginalnym kontrakcie. 3

W gruncie rzeczy ujawniło to pewien niewielki problem: fragment tempuri . org widoczny w łańcuchu URL ozna­ cza coś tymczasowego, co powinniśmy uzupełnić samodzielnie - atrybut Servi ceContract występujący w ory­ ginalnej definicji interfejsu ma atrybut Namespace, my zaś powinniśmy wybrać tu URt który będzie miał charakter unikatowy dla naszej usługi. Nie jest to obowiązkowe w przypadku naszej aplikacji, ponieważ wszystko działa doskonale przy ustawieniach standardowych, jednak ten tymczasowy URI po prostu nie wygląda zbyt profesjonalnie.

498

I

Rozdział 13. Sieci

Współdzielenie kontraktów Być może Czytelnik zastanawia się teraz, dlaczego wyczynialiśmy wszystkie te cuda, zamiast po prostu skopiować kod interfejsu I C hatServi ce z projektu usługi i wkleić go w projekcie klienta. W rzeczywistości rozwiązanie takie sprawdziłoby się całkiem dobrze . Co więcej, moglibyśmy nawet napisać oddzielny projekt biblioteki DLL, aby zdefiniować ten interfejs kontraktu, i współ­ dzielić ją pomiędzy dwoma projektami. Jak wkrótce się przekonamy, przeprowadzając proces dodawania odwołania do usługi, środowisko Visual Studio wygenerowało kilka innych przy­ datnych rzeczy, jednak jak się to nieraz zdarza, pomysł bezpośredniego współdzielenia defi­ nicji kontraktu może mieć bardzo dobre uzasadnienie - nie ma tu konieczności korzystania z wymiany metadanych. Oczywiście nie zawsze będziemy właścicielami kodu działającego po obydwu stronach połą­ czenia. Gdy musimy się łączyć z dostępną w internecie usługą opracowaną przez kogoś innego, to wymiana metadanych zyskuje na znaczeniu - zapewnia ona sposób przechowywania kon­ traktu, którego sami nie napisaliśmy. A z uwagi na to, że mechanizm ten działa w oparciu o pewne standardy, rozwiązanie to sprawdza się również w przypadku usług, które nie zostały napisane przy użyciu platformy .NET. Wymiana metadanych nie jest mechanizmem obsługiwanym powszechnie. Odkrywa­ nie kontraktów w praktyce może być przeprowadzane na wiele różnych sposobów, w tym (to nie żart) na drodze faksowania wydruku prezentującego przykładowe 4 komunikaty, które usługa spodziewa się wysyłać i odbierac . Gdy uzyskujemy kontrakt za pośrednictwem tego rodzaju nieformalnego kanału komunikacji, musimy po prostu samodzielnie napisać odpowiedni interfejs w swoim programie klienckim, aby mógł on reprezentować kontrakt wykorzystywanej usługi.

Proces importowania metadanych zwraca również uwagę na ważną kwestię dotyczącą ewolucji usług. Moglibyśmy zmodyfikować usługę C hatServi ce już po dodaniu do projektu ChatCl i ent odwołania do niej . Jeśli z tymi modyfikacjami wiązałyby się zmiany w kontrakcie, byłoby jasne, że pojawiłby się tu pewien problem: kopia kontraktu posiadana przez klienta stałaby się nie­ aktualna. Z pewnością przyszło Czytelnikowi do głowy, że doskonałym sposobem uniknięcia tego kłopotu byłoby bezpośrednie współdzielenie tego interfejsu za pomocą biblioteki DLL, to jednak mogłoby sprawić, że problem byłoby tylko trudniej zauważyć. Co w przypadku, gdybyśmy zdążyli już wydać oficjalną wersję klienta? Gdybyśmy wówczas zmodyfikowali kontrakt, poddany zmianom kod mógłby działać dobrze na naszej maszynie, lecz po wdro­ żeniu aktualizacji usługi korzystającej z tego zmienionego kontraktu wszystkie kopie starego klienta działające na maszynach użytkownika znalazłyby się w kłopocie, ponieważ w dalszym ciągu używałyby tej starej wersji. Jawne zastosowanie wymiany metadanych rzecz jasna nie upraszcza w żaden sposób rozwiązania tego problemu, ale zmniejsza nieco prawdopodobień­ stwo przypadkowego pojawienia się błędu oraz tego, że nie zostanie on wykryty. Pełne rozwią­ zanie przedstawionego tu problemu ewolucji usług wykracza poza zakres tematów przed­ stawionych w niniejszej książce, dlatego na razie chcemy jedynie ostrzec Czytelnika, że nie powinien lekceważyć kwestii dokonywania zmian kontraktu.

4

W rzeczywistości może być jeszcze gorzej. Odpowiednie przykłady można znaleźć w internecie.

Platforma WCF

I

499

'

..

......_-�

,



Temat pracy z różnymi wersjami kontraktów usługi został szerzej przedstawiony w książce Michele Leroux Bustamante Learning WCF (http://oreilly. com/catalog/ 9780596101626/), która ukazała się nakładem wydawnictwa O'Reilly .

Obiekt pośredn iczący (proxy) Kolejną po kontrakcie interesującą rzeczą, którą znajdziemy w pliku Reference.cs wygenero­ wanym w wyniku dodania odwołania do usługi, jest klasa o nazwie C hatServi ceCl i ent . Klasa ta implementuje interfejs I Ch atServ i ce, ponieważ pełni ona rolę obiektu pośredniczącego (ang. proxy) dla usługi. Jeśli chcemy nawiązać komunikację z usługą, musimy jedynie utworzyć instan­ cję tego proxy i wywołać metodę reprezentującą operację, którą zamierzamy przeprowadzić. Gdy zatem umieścimy dyrektywę u s i ng Chat Cl i ent . Ch atServ i ce w początkowej części pliku Program.es należącego do projektu ChatCl i ent, będziemy mogli zmodyfikować znajdującą się w nim metodę Mai n w sposób zaprezentowany na listingu 13.5 . Listing 13.5. Wywoływanie usługi sieciowej przy użyciu obiektu pośredniczącego WCF s t at i c vo i d Mai n (s tri ng O arg s ) { us i ng (ChatServ i ceCl i ent chat Proxy = new ChatServ i ceCl i en t () ) { chat Proxy . PostNote ( " Łu kasz " , "Wi taj znów , świ ec i e " ) ;

Zwróćmy uwagę na instrukcję u s i ng - bardzo ważne jest tutaj, aby zwolnić wszelkie obiekty pośredniczące WCF po zakończeniu korzystania z nich. Gdy klient wywołuje tę metodę za pomocą obiektu pośredniczącego, platforma WCF buduje komunikat zawierający dane wej­ ściowe i wysyła go do usługi . W obrębie usługi (która działa w ramach oddzielnego procesu, być może na zupełnie innej maszynie) mechanizm WCF odbierze ten komunikat, rozpakuje dane wejściowe i prześle je do metody Pos tNote należącej do klasy C hatServi ce. Aby wypróbować to w praktyce, musimy uruchomić jednocześnie programy klienta i serwera. Oznacza to, że należy nieco inaczej skonfigurować solucję w środowisku Visual Studio. Kliknijmy prawym przyciskiem myszy solucję WcfChat widoczną w panelu Solution Explorer i wybierzmy z menu kontekstowego polecenie Set Startup Projects, aby wyświetlić okno dialogowe zawie­ rające trzy przyciski opcji. Gdy klikniemy przycisk Multiple Startup Projects, będziemy mogli wskazać, które projekty mają zostać uruchomione w momencie rozpoczęcia debugowania . W tym przypadku zależy nam na zmianie wartości parametrów Action dla projektów C h a t "+ C l i e n t oraz ChatHost z None na Start. (Wartości None parametru Action dla projektu ChatServer "+ L i brary nie będziemy zmieniać - nie ma potrzeby uruchamiania go, ponieważ bibliote­ ka usługi jest już udostępniana przez nasz projekt ChatHost ) . Chcemy również, aby usługa uru­ chamiała się z pewnym wyprzedzeniem, aby na pewno działała, zanim klient podejmie próbę jej użycia. Powinniśmy więc wskazać projekt ChatHost i kliknąć widoczny na prawo od pola listy przycisk oznaczony strzałką skierowaną w górę, aby powiadomić środowisko Visual Studio, że projekt ten ma zostać uruchomiony jako pierwszy. (Teoretycznie nie jest to zbyt pewna metoda, ponieważ nie ma żadnej gwarancji, że serwer będzie miał wystarczająco du­ żo czasu, aby zdążyć się uruchomić. W praktyce jednak rozwiązanie to wydaje się działać cał­ kiem dobrze w przypadku tego rodzaju ćwiczeń z debugowania) . Na rysunku 13.8 zostało przedstawione omawiane okno dialogowe z odpowiednio zdefiniowanymi ustawieniami.

500

I

Rozdział 13. Sieci

I 1( 1�1

Solu tion ' WcfC ha t' P roperty Pa g es

.(;_o nfi g u rati o n : "'

IN/A

C o m m o n Propertie> Startup P roject

... 1

P.latform:

I N/A

O Cyrrent selectio n

Project D'ependencies D'ebug Source Files

O �ingle rtartup prnject

C o d e An a lysis Setti n g >

@ Multip l e startup p roject>:

I> C onfigu ratio n Prop erties

... 1 I C.Qnfiguration Manager.„ I

I ChatHost Project Ch at H o

>t

... 1 Action Start

ChatC l ient

Start

Ch

N

atServerli b ra ry

B

one

r

OK

G G

] I Anuluj I I Zastosuj I

Rysunek 13.8. Ustawienia umożliwiające jednoczesne uruchamianie wielu projektów Gdy uruchomimy program, naciskając klawisz F5, na ekranie komputera pojawią się dwa okna konsoli, z których jedno będzie związane z programem klienta, a drugie z usługą. ••

, .'

._„�;

Jeśli czytając ten rozdział, Czytelnik wykonuje kolejne przedstawione w nim działania, niewykluczone, że w tym momencie zgłoszony został wyjątek AddressAl readylnUseExcepti on z komunikatem błędu mówiącym, iż inna aplikacja zarejestrowała już ten URL w HTTP.SYS. Oznacza to, że w systemie działa już kopia programu ChatHost - gdzieś na pulpicie Czytelnik znajdzie zapewne okno konsoli, w którym uruchomiona będzie aplikacja udostępniająca usługę. Możliwe też, że nadal pracuje program WCF Service Host. Błąd ten pojawia się, gdy uruchamiamy drugą kopię usługi, ponieważ próbuje ona nasłuchiwać komunikatów nadchodzących na ten sam adres, którego używa pierwsza, a tylko jeden program może w danej chwili odbierać żądania zgłaszane na określony URL.

Ś rodowisko Visual Studio wyświetli w panelu Output odpowiedni komunikat dzięki wywo­ łaniu metody Debug . Wri t e l i n e znajdującemu się w metodzie PostNote, podobnie jak miało to miejsce wcześniej, gdy korzystaliśmy z programu WCF Test Client, potwierdzając tym samym, że obiekt pośredniczący był w stanie wywołać operację oferowaną przez usługę. (Prawdopo­ dobnie będziemy się musieli dokładnie przyjrzeć, aby odnaleźć odpowiedni fragment tekstu - komunikat może być głęboko zagrzebany pomiędzy wieloma innymi powiadomieniami, które są wyświetlane w panelu Output) . Zwróćmy uwagę na fakt, że w kodzie przedstawionym na listingu 13.5 nie musieliśmy infor­ mować obiektu pośredniczącego, jakiego adresu ma on używać. Jest tak dlatego, że okno dialo­ gowe Add Service Reference zaimportowało więcej danych niż tylko definicję kontraktu. Dodało ono nieco informacji do związanego z projektem ChatCl i ent pliku App.config, który został poka­ zany w całej swojej strasznej postaci na listingu 13.6. Platforma WCF

I

501

Listing 13.6. Wygenerowany dla strony klienta plik App.config

Podobnie jak miało to miejsce w przypadku konfiguracji usługi, którą zajmowaliśmy się wcześniej, również tutaj pojawia się element end poi n t zawierający odpowiedni adres, wiąza­ nie i kontrakt, jednak z uwagi na to, że element ten należy do strony klienta, znajduje się on wewnątrz elementu c l i ent zamiast s erv i c e . Obiekt pośredniczący uzyskuje adres właśnie z tej definicji punktu końcowego.

ł

502

I

:

Odpowiedni adres możemy podać obiektowi pośredniczącemu również z poziomu kodu, jeśli tylko mamy na to ochotę. Oferuje on różne przeładowane wersje kon­ struktora, a niektóre z tych przeładowań przyjmują łańcuch URL w roli argumentu. Jeśli jednak się na to nie zdecydujemy, adres poszukiwany będzie w pliku konfigu­ racyjnym.

Rozdział 13. Sieci

Zwróćmy uwagę na to, że punkt końcowy ma również atrybut b i n d i ngCon f i gurat i on . Odwo­ łuje się on do elementu b i n d i ng znajdującego się wcześniej w pliku, który zawiera informację na temat tego, jak ma być dokładnie skonfigurowane wiązanie wsHt t p B i n d i n g . W przypadku usługi nie mieliśmy z niczym takim do czynienia, ponieważ korzystaliśmy tam po prostu z usta­ wień standardowych. Jednak okno dialogowe Add Service Reference zawsze generuje wpis kon­ figuracji wiązania; jest tak nawet wówczas, gdy używamy ustawień standardowych. Nasza aplikacja „czatu" zapewnia już możliwość wysyłania notatki od klienta do serwera, nadal nie jest jednak ukończona. Klient wymaga jeszcze kilku dodatkowych funkcji. Aby nasza komunikacja miała nieco mniej jednostronny charakter, musimy być w stanie zobaczyć notatki napisane przez innych ludzi. Z uwagi na to, że nasze konwersacje nie zawsze będą wyjątkowo krótkie, powinniśmy też móc pisać więcej niż tylko jedną notatkę. Z drugim z wymienionych problemów poradzimy sobie, modyfikując kod przedstawiony na listingu 13.5. Wywołanie kierowane do obiektu pośredniczącego umieścimy wewnątrz pętli i poprosimy użytkownika o podanie imienia, aby umożliwić wysyłanie notatek osobom, które niekoniecznie noszą imię Łukasz. Odpowiednie zmiany zostały zaprezentowane na listingu 13.7.

Listing 13.7. Program klienta z pętlą wprowadzania notatek s t at i c vo i d Mai n (s tri ng O arg s ) { ChatServ i ceCl i ent chatProxy = new ChatServ i ceCl i ent () ; Consol e . Wri tel i ne ( " Proszę wprowadzi ć i mi ę : " ) ; s t r i ng name Consol e . Readli n e ( ) ; wh i l e (true) { Consol e . Wr i teli ne ( " Nap i s z notat kę ( l ub naci śn i j kl awi sz Enter , aby z a kończyć dz i ał an i e -.programu) : " ) ; stri ng note = Consol e . Readli ne ( ) ; i f (stri ng . I s N u l l OrEmpty (note) ) { brea k ; =

chat Proxy . PostNote (name , note) ;

Zmodyfikujemy też serwer, aby wyświetlał on notatkę, zamiast przesyłać ją do panelu debugo­ wania - ułatwi nam to zauważanie i odczytywanie nadchodzących notatek. Zmieńmy zatem metodę Pos tNote w projekcie C hatServi ce w następujący sposób: publ i c vo i d PostNote (stri ng from , s t r i ng note) { Consol e . Wri teli ne ( " { O } : { 1 } " , from , note) ;

Gdy naciskając klawisz F5, ponownie uruchomimy obydwa programy, program klienta poprosi nas o podanie naszego imienia, a następnie umożliwi wprowadzenie dowolnej liczby notatek. Każda nowa notatka zostanie wysłana do serwera, dzięki czemu będziemy mogli zobaczyć, jak kolejne notatki pojawiają się w oknie jego konsoli. Udało nam się już znacznie poprawić naszą aplikację, lecz klient nadal nie jest w stanie zoba­ czyć notatek pisanych przez innych użytkowników. Aby mu to umożliwić, musimy zapewnić komunikację dwukierunkową.

Platforma WCF

I

503

Dwukierunkowa komunikacja z dwustronnym i kontraktami Kontrakt związany z naszą usługą czatu ma charakter jednostronny - dotyczy jedynie nota­ tek, które klient wysyła do serwera. Platforma WCF obsługuje jednak dwustronne kontrakty (ang. duplex contracts), które zapewniają serwerowi możliwość powrotnego odwołania się do klienta. (Zwróćmy uwagę na to, że w przypadku protokołu HTTP mamy do czynienia z pew­ nymi kwestiami, które mogą utrudnić tego rodzaju dwustronną komunikację. Więcej infor­ macji na ten temat Czytelnik znajdzie w zamieszczonej poniżej ramce) . Kontrakt dwustronny wymaga istnienia dwóch interfejsów - oprócz interfejsu implementowanego przez serwer należy również zdefiniować interfejs, który ma zostać zaimplementowany przez klienta, jeśli chce on korzystać z usługi . W naszym przykładzie usługa powinna powiadamiać klientów za każdym razem, gdy dowolny użytkownik wyśle notatkę. Z tego powodu interfejs przeznaczony dla strony klienta bardzo przypomina nasz bieżący interfejs serwera. Interfejs klienta został przedstawiony na listingu 13.8 .

Listing 13.8. Interfejs komunikacji zwrotnej dla kontraktu dwustronnego publ i c i nterface I ChatCl i ent { [Operat i onContract] vo i d NotePosted (stri ng from , stri ng note) ;

Zwróćmy uwagę na to, że choć metody należące do interfejsu komunikacji zwrotnej wyma­ gają zastosowania zwykłego atrybutu Opera t i onContract, sam interfejs nie musi być oznaczony za pomocą atrybutu Serv i ceContract. Jest tak, ponieważ ten interfejs komunikacji zwrotnej nie jest kontraktem sam z siebie - stanowi on jedną połowę kontraktu dwukierunkowego. Musimy zatem zmodyfikować istniejący interfejs I ChatServ i ce, aby powiązać go z tym nowym interfejsem komunikacji zwrotnej, tak jak zostało to przedstawione na listingu 13.9 .

Listing 13.9. Kontrakt dwustronny [Serv i ceContract ( Cal l bac kContract=typeof ( I ChatCl i en t ) , Ses s i onMode=Ses s i onMode . Requ i red) ] publ i c i nterface I ChatServ i ce { [Operat i onContract] bool Connect (stri ng name) ; [Operat i onContract] vo i d Pos tNote ( s t r i ng note) ; [Operat i onContract] vo i d Di s connect () ;

Ustawiając właściwość Cal l bac kContra c t atrybutu Serv i ceContract, zadeklarowaliśmy, że jest to kontrakt dwustronny, a także wskazaliśmy interfejs definiujący stronę klienta tego kontraktu. W kodzie przedstawionym na listingu 13.9 dokonaliśmy również kilku innych zmian, które okazują się niezbędne, aby nasza usługa działała zgodnie z oczekiwaniami: ustawiliśmy wła­ ściwość Sess i onMode atrybutu Serv i ceContract, a także dodaliśmy kilka kolejnych metod, które umożliwiają klientom łączenie i rozłączanie się z serwerem. Usunęliśmy też parametr n ame typu stri ng z metody PostNote - jak się wkrótce przekonamy, okazuje się on niepotrzebny. Wszystkie pozostałe modyfikacje mają związek z sesjami. 504

I

Rozdział 13. Sieci

Dwustronna komunikacja, protokół HTTP i zapory sieciowe Komunikacja dwukierunkowa w internecie jest w obecnych czasach dość problematyczna. Ogromna większość komputerów znajduje się za zaporami sieciowymi. Zapory te są zwykle skonfigurowane w taki sposób, aby odrzucać większość połączeń przychodzących. Istnieją pewne wyjątki związane z połączeniami z takimi maszynami jak serwery WWW i serwery poczty elektronicznej - administra­ torzy konfigurują zapory sieciowe tak, aby umożliwiały one komunikowanie się z tego rodzaju kompu­ terami - jednak standardowe założenie jest tu takie, że wszelkie próby nawiązania połączenia z usłu­ gami powinny być blokowane, chyba że zapora zostanie jawnie poinformowana, że ma być inaczej . Taki standardowy sposób działania jest dobry ze względów bezpieczeństwa, ponieważ ogromna większość niespodziewanych połączeń przychodzących nawiązywana jest przez hakerów. Każda maszyna bez zapory sieciowej podłączona bezpośrednio do internetu będzie narażona na ciągły strumień ruchu pochodzącego od osób poszukujących maszyn, na które dałoby się włamać. Typowa konfiguracja zapory sieciowej zabezpiecza maszyny przed tego rodzaju atakami, zapewniając dodat­ kową linię obrony na wypadek, gdybyśmy nie instalowali na bieżąco aktualizacji systemu opera­ cyjnego lub gdyby jakiś haker próbował przypuścić tak zwany atak typu zeroday wykorzystujący błąd, który nie został do tego momentu poprawiony. Problem wiążący się z tą sytuacją polega na tym, że znacznie utrudnia to dwukierunkową komu­ nikację przy użyciu protokołu HTTP. Operacje HTTP mogą zostać zapoczątkowane wyłącznie przez komputer, który pierwszy otworzył połączenie - nie da się otworzyć połączenia z serwerem WWW, a następnie oczekiwać na to, że przyśle on do nas jakiś komunikat. Protokół HTTP ma charakter asymetryczny w tym sensie, że nic nie może się wydarzyć, dopóki klient nie przyśle żąda­ nia. (Swoją drogą, protokół niższego poziomu, na którym opiera swoje działanie HTTP, czyli protokół TCP, jest w tej kwestii znacznie bardziej elastyczny - jest to zresztą jeden z powodów, dla których czasami warto rozważyć zastosowanie gniazd. Każda strona połączenia TCP może swobod­ nie przesyłać dane w dowolnym czasie niezależnie od tego, która z nich nawiązała połączenie) . Aby umożliwić w pełni dwukierunkową komunikację prowadzoną za pośrednictwem HTTP, musimy sprawić, aby na obydwu końcach połączenia działał serwer HTTP. W przypadku korzystania z dwu­ stronnej komunikacji za pomocą platformy WCF, w połączeniu z wiązaniem opartym na HTTP, WCF zapewnia działanie mechanizmu, który w gruncie rzeczy jest miniaturowym serwerem WWW pracującym w obrębie procesu klienta. Ma to oczywiście jakikolwiek sens tylko wtedy, gdy serwer jest w stanie nawiązać zwrotne połączenie z działającym po stronie klienta miniserwerem. Jeśli klient i serwer znajdują się za tą samą zaporą sieciową, nie będzie najmniejszego problemu. Jeśli jednak serwer znajduje się gdzieś w internecie i każdy może uzyskać do niego dostęp, jest niemal pewne, że nie będzie on w stanie nawiązać zwrotnego połączenia z większością klientów. Z tego powodu rozwiązanie przedstawione na listingu 13.8 będzie działało wyłącznie w prywatnych sie­ ciach. Opracowanie programu czatu pracującego za pośrednictwem internetu wymaga użycia protokołu TCP i gniazd lub też odrobiny nieczystych sztuczek, których opis wykracza poza zakres zagadnień poruszanych w tej książce. W związku z powyższym powinniśmy raczej unikać dwustronnych kontraktów w przypadku apli­ kacji, które mają się komunikować za pośrednictwem internetu.

Komuni kacja działająca w oparciu o sesje Właściwość Ses s i on Mode atrybutu Serv i ceContract określa naturę związku istniejącego pomiędzy serwerem i każdym z klientów. Standardowo zakłada się, że związek ten ma charakter krótko­ trwały, zwykle nie dłuższy niż pojedyncza operacja. Odzwierciedla to fakt, że platforma WCF jest przeznaczona do obsługi usług WWW, a protokół HTTP nie oferuje jakiegokolwiek rodzaju połączenia pomiędzy klientem a serwerem, które miałoby trwać dłużej niż pojedyncze żądanie.

Platforma WCF

I

505

'

. .

Prawdą jest, że protokół HTTP pozwala, aby pojedyncze połączenie TCP było ponow­ nie wykorzystywane p rzez wiele różnych żądań, ale ma to jedynie związek z op ty­ malizacją dotyczącą wydajności i nic nie może od tego zależeć. Zarówno klient, jak i serwer mogą swobodnie zamknąć połączenie na końcu żądania, wymuszając w ten sposób nawiązanie nowego w celu obsłużenia kolejnej op eracji bez konieczności zmiany semantyki operacji. (Nawet jeśli zarówno klient, jak i serwer chcą utrzymać połączenie p omiędzy żądaniami, znajdujący się pomiędzy nimi obiekt pośredniczący może zignorować ich wolę i zerwać łączność) . Logicznie rzecz biorąc, każde żądanie HTTP jest całkowicie niep owiązane z tymi, które nadchodzą przed nim lub po nim.

Ten bezpołączeniowy sposób działania ma duże znaczenie dla kwestii skalowalności i odpor­ ności systemu. Oznacza on, że możemy odpowiednio rozkładać obciążenie pomiędzy bardzo wieloma serwerami WWW, a nie jest szczególnie istotne, czy wszystkie żądania pochodzące od określonego klienta zostaną obsłużone przez tę samą maszynę. Zwykle da się wyłączyć z dzia­ łania jedną z maszyn pracujących w farmie WWW bez przerywania pracy któregokolwiek z klientów. Jednak brak połączeń może też mieć pewne wady - niektóre aplikacje muszą korzy­ stać z czegoś w rodzaju sesji. Dość męcząca byłaby na przykład konieczność podawania nazwy użytkownika i hasła dostępu za każdym razem, gdy przechodzimy z jednej strony na inną należącą do tej samej witryny - gdy już zalogowaliśmy się w serwisie, chcemy, aby pamiętał on, kim jesteśmy. Podobnie będzie w przypadku naszej aplikacji czatu - jeśli ma ona być w sta­ nie zwrotnie kontaktować się z klientami w celu powiadamiania ich o nadesłaniu przez kogoś notatki, musi wiedzieć, którzy z klientów są obecnie z nią połączeni . Choć protokół HTTP nie oferuje żadnego standardowego sposobu reprezentowania sesji, dostępne są różne doraźne systemy, które zostały opracowane, aby zapewniać tego typu moż­ liwość. Serwisy WWW zwykle używają do tego celu ciasteczek (ang. cookies) . (Mechanizm ten nie należy do specyfikacji HTTP, ale jest obsługiwany przez wszystkie popularne przeglą­ darki internetowe. Niektórzy użytkownicy wolą go jednak wyłączyć, dlatego ciasteczka nie zawsze muszą być wszędzie dostępne) . W przypadku standardów usług WWW obsługiwanych przez platformę WCF preferowane jest nieco inne rozwiązanie. Działa ono w sposób podobny do ciasteczek, lecz umieszcza odpowiednie informacje w wysyłanej wiadomości zamiast w nagłówkach HTTP5 . Z racji tego, że nasz kontrakt ma teraz charakter dwustronny, wymaga on możliwości utrzy­ mywania połączenia pomiędzy każdym klientem a serwerem. Informujemy o tym platformę WCF, przypisując właściwości Sess i onMode wartość Sess i onMode . Requi red . Zwróćmy jednak uwagę na fakt, że nie oznacza to jeszcze włączenia sesji . Ustawienie to mówi jedynie, że wszystko, co zechce komunikować się za pomocą tego kontraktu, powinno robić to przy zapewnieniu obsługi sesji . Pamiętajmy, że sam kontrakt jest rzeczą zupełnie odrębną od implementacji, które są z nim zgodne . Efektem zdefiniowania ustawienia jest to, że platforma WCF zgłosi błąd, gdy podejmiemy próbę użycia tego kontraktu bez włączenia sesji. Sposób jej włączania przez odpowiednią modyfikację plików konfiguracji klienta i serwera przedstawimy, gdy tylko zakończymy wprowadzanie zmian w kodzie.

5

Ogólnie rzecz biorąc, rodzina protokołów usług sieciowych WS-* stara się unikać zależności od HTTP. Może się to wydawać tendencją dość dziwną w przypadku standardów usług sieciowych, ale wiele organizacji zaan­ gażowanych w tworzenie tych specyfikacji chciało, aby formaty komunikatów dało się stosować w systemach działających w oparciu o kolejki komunikatów, jak również w protokole HTTP. Z tego powodu generalnie mają one tendencję do unikania mechanizmów specyficznych dla danego rodzaju transportu.

506

I

Rozdział 13. Sieci

Sesja zostanie ustanowiona w chwili, gdy klient po raz pierwszy nawiąże połączenie z serwe­ rem, co stawia przed naszą aplikacją kolejny problem. Platforma WCF nie wyśle komunikatu, dopóki nie będzie miała czegoś do wysłania, dlatego nasz klient czatu połączy się po raz pierwszy z usługą, gdy wyślemy naszą pierwszą notatkę . (Samo utworzenie instancji klasy C hatServ i ceProxy nie powoduje jeszcze nawiązania połączenia - nic nie zostanie przesłane sie­ cią aż do momentu, gdy po raz pierwszy spróbujemy wywołać operację) . Chcemy jednak, aby klienci mogli otrzymywać notatki od razu, bez konieczności wcześniejszego wysyłania własnych. Musimy więc zapewnić im sposób zgłaszania usłudze swojej obecności bez wysyła­ nia notatki. To właśnie jest powodem, dla którego do kodu przedstawionego na listingu 13.9 dodaliśmy metodę Connect . Zapewniliśmy również metodę Di s connect umożliwiającą klientom zgłaszanie faktu opuszczania czatu, dzięki czemu serwer nie będzie niepotrzebnie próbował wysyłać notatek do klientów, którzy nie są już połączeni z usługą. (Gdybyśmy tego nie zrobili, serwer otrzymałby wyjątek przy następnej próbie wysłania komunikatu. Choć to również powiadomiłoby go, że klient nie jest już obecny, jawne rozłączenie stanowi nieco ładniejsze rozwiązanie. Umożliwia ono ponadto dokonanie rozróżnienia pomiędzy użytkownikami, którzy celowo przerwali konwersację, oraz tymi, którzy zostali od niej odcięci w związku z jakimiś problemami na łączach) . Teraz powinniśmy zaktualizować kod serwera w taki sposób, aby implementował on zmody­ fikowany kontrakt i śledził obecność klientów.

Wywoływan ie kl ienta przez serwer Nasza usługa będzie musiała utrzymywać listę połączonych z nią klientów, aby być w stanie powiadamiać każdego z nich o odebraniu notatki. Listę tę można przechowywać w postaci prywatnych danych klasy usługi, jednak ponieważ ta jedna lista powinna być dostępna dla wszystkich sesji, musimy poinformować platformę WCF, że chcemy, aby dało się utworzyć tylko jedną instancję tej klasy. Platforma WCF oferuje kilka różnych trybów tworzenia instancji klasy usługi . Można utwo­ rzyć jedną dla każdej sesji klienta - rozwiązanie takie sprawdza się dobrze, gdy zależy nam na stanie indywidualnej sesji - jednak w naszym przypadku wszystkie notatki mają być wysyłane do wszystkich użytkowników, dlatego jedynym interesującym nas stanem jest stan globalny. Z tego powodu nie ma wielkiego sensu korzystać z oddzielnych instancji dla każ­ dego klienta . Platforma WCF jest również w stanie tworzyć nową instancję klasy usługi dla każdego żądania - jeśli w samej klasie usługi nie przechowujemy żadnego stanu, może się to okazać sensownym rozwiązaniem. Tutaj jednak potrzebujemy instancji, która będzie działała przez cały czas pracy usługi . Można to wskazać w następujący sposób: [Servi ceBehavi or ( InstanceContextMode=InstanceContextMode . Si ngl e , Concurren cyMode = Concurren cyMode . Reentrant) ]

publ i c cl ass ChatServ i ce : IChatServ i ce {

Do kodu dodaliśmy atrybut S erv i c e B e h a v i or, aby wskazać, że zależy nam na korzystaniu z pojedynczej instancji usługi . Zwróćmy uwagę, że właściwości Con curren cyMode przypisaliśmy wartość Con curren cyMode . Reentran t . Informuje ona platformę WCF, że nasza usługa ma pracować nad żądaniami związanymi z jedną sesją naraz - jeśli żądania pochodzące od różnych klien­ tów pojawią się równocześnie, WCF obsłuży je jedno po drugim. Jest to wygodne, ponieważ

Platforma WCF

I

507

oznacza, że dopóki pojedynczy klient w danej chwili będzie wykonywał tylko jedną rzecz, nie będziemy musieli tworzyć specjalnego kodu zapewniającego bezpieczeństwo wątków odpowie­ dzialnych za obsługę stanu. '



.

Alternatywą dla kontekstowego trybu pojedynczej instancji mogłoby być przecho­ wywanie stanu w polu statycznym. Umożliwiłoby to współdzielenie danych pomiędzy wszystkimi klientami, a właśnie o to nam chodzi. W takim przypadku bylibyśmy jednak zdani sami na siebie w kwestii bezpieczeństwa wątków. Właściwość ConcurrencyMode ma zastosowanie jedynie do określonej instancji usługi, jeśli zatem nie wybierzemy trybu pojedynczej instancji, platforma WCF pozwoli różnym instancjom naszej usługi działać jednocześnie. W praktyce prawdziwe aplikacje zwykle muszą przeprowadzać własną synchronizację wątków. W naszym przykładzie założyliśmy, że programy klienckie będą w danej chwili zgłaszać tylko po jednym żądaniu, co może się sprawdzić w niewielkim kon­ trolowanym przypadku, lecz jest dość ryzykowne, jeśli nie można całkowicie zaufać maszynom klientów. (Nawet w przypadku istnienia tylko jednej sesji naraz pojedyn­ cza sesja klienta mogłaby jednocześnie wywołać wiele operacji) . Być może Czytelnik zastanawia się, dlaczego nie zastosowaliśmy tu trybu ConcurrencyMode . s i ngl e, który wymusza całkowicie ścisły model indywidualny. Niestety, spowodowałoby to unie­ możliwienie zwrotnego odwoływania się do klientów w czasie obsługi żądania pocho­ dzącego od jednego z nich - blokująca transmisja wychodząca z niewspółbieżnego kontekstu jednowątkowego stwarza okazję do powstawania zakleszczeń, dlatego platforma WCF nie dopuszcza takiej możliwości.

Następnie powinniśmy dodać pole odpowiedzialne za przechowywanie stanu - kolekcję klientów połączonych aktualnie z usługą: pri vate D i ct i onary< I ChatCl i ent , stri ng> cl i entsAndNames = new D i ct i onary< I ChatCl i ent , stri ng> () ;

Jest to słownik, w którym typem klucza jest interfejs komunikacji zwrotnej klienta zdefinio­ wany przez nas wcześniej . Wartością jest imię klienta. Sposób użycia tego słownika można zobaczyć w implementacji metody Con n ect: publ i c bool Connect (stri ng name) { i f (cl i entsAndNames . Conta i nsVal ue (name) ) { li Imię jest już używane, dlatego połączenie zostaje odrzucone. return fal s e ; I ChatCl i ent cl i entCal l back = Opera t i onContext . Current . GetCal l bac kChanne l < I ChatCl i ent> () ;

li clientsAndNames to stan współdzielony, ale nie następuje tu blokada, ponieważ zakładamy, że tryb li ConcurrentMode.Reentrant zapewnia podawanie tylko jednego komunikatu naraz. cl i entsAndNames . Add (cl i entCal l ba c k , name) ; Consol e . Wri teli ne (name + " połączony " ) ; return true ;

Pierwszą rzeczą, którą tu robimy, jest sprawdzenie, czy imię użytkownika ma charakter unika­ towy. Dysponując listą połączonych klientów, mamy możliwość zapobiegania sytuacji, w której wielu użytkowników wybiera takie samo imię . Gdy nowy użytkownik próbuje podać imię wykorzystywane już przez inną osobę, metoda zwraca wartość fal se. (Wartość zwracana spraw­ dza się w tym przypadku lepiej niż wyjątek, ponieważ nie mamy tak naprawdę do czynienia z sytuacją wyjątkową) . 508

I

Rozdział 13. Sieci

Jeśli podane imię jest w porządku, pobieramy interfejs komunikacji zwrotnej użytkownika, korzystając z następującego wyrażenia: Opera t i onContext . Current . GetCal l backChannel < I ChatCl i ent> ()

Operat i onCon text to klasa WCF, której właściwość C urren t dostarcza informacji na temat ope­ racji obsługiwanej w danej chwili przez nasz program. Jedną z zapewnianych przez nią usług jest możliwość pobierania interfejsu komunikacji zwrotnej w przypadku, w którym używany jest kontrakt dwustronny. Metoda GetCal l backC h annel zwraca obiekt pośredniczący podobny do tego, z którego klient korzysta podczas komunikacji z usługą, jednak to proxy działa w prze­ ciwnym kierunku - wywołuje ono operacje po stronie klienta, który wywołał naszą metodę C on n e c t . Dodajemy go tu jedynie do słownika połączonych klientów, wiążąc jednocześnie z wybranym przez użytkownika imieniem, a następnie zwracamy wartość true, aby poinfor­ mować, że działanie kończy się sukcesem, ponieważ imię to nie było wcześniej wykorzystywane, oraz że nawiązane przez niego połączenie zostało przez nas zaakceptowane.

Teraz przyjrzyjmy się zmodyfikowanej metodzie Pos t Note: publ i c vo i d PostNote (stri ng note) { I ChatCl i ent cl i entCal l back = Opera t i onContext . Current . GetCal l bac kChanne l < I ChatCl i ent> () ; s t r i ng name = cl i entsAndNames [cl i en tCal l back] ; Consol e . Wri teli ne ( " { O } : { 1 } " , name , note) ;

li Metoda ToArray() tworzy kopię kolekcji. Pozwala to uniknąć wyjątku związanego z modyfikacją kolekcji, li która nastąpiłaby w wyniku przerwania połączenia z klientem podczas wykonywania pętli. KeyVal uePai r [] cop i edNames = cl i entsAndNames . ToArray () ; foreach ( KeyVa l uePa i r< I ChatCl i ent , s t r i ng> cl i ent i n cop i edNames) { li Uniknięcie odsyłania komunikatu do klienta, który właśnie go wysłał - nadawca zna treść notatki, li którą przed chwilą wprowadził. i f (cl i ent . Key ! = cl i entCal l bac k) { Consol e . Wri teli ne ( "Wysył an i e notatki do { O } " , cl i ent . Val ue) ; try { cl i en t . Key . NotePosted (name , note) ; } catch ( Except i on x) { Consol e . Wri teli ne ( " Bł ąd : { O } " , x) ; Di s connectCl i ent (cl i ent . Key) ;

Również tutaj zaczynamy od pobrania interfejsu komunikacji zwrotnej bieżącego klienta . Pamiętajmy, że z naszym serwerem czatu będzie zwykle połączonych wielu klientów, a to właśnie ten interfejs pozwala nam stwierdzić, który z nich przesyła w danej chwili notatkę . W kolejnym wierszu kodu poszukujemy pobranego interfejsu komunikacji zwrotnej w słow­ niku w celu sprawdzenia, jakie imię użytkownik ten przekazał oryginalnie metodzie Connect to właśnie dzięki temu mogliśmy usunąć występujący wcześniej w metodzie parametr, który określał imię wywołującego. Imię to znamy dzięki wcześniejszej komunikacji - powinniśmy je zapamiętać, aby zapewnić unikatowość użytkowników - a skoro już nim dysponujemy, klient nie musi go przekazywać za każdym razem, gdy wysyła notatkę .

Platforma WCF

I

509

Następnie przeglądamy listę wszystkich połączonych klientów znajdującą się w słowniku c l i entsAndNames, aby dostarczyć każdemu z nich nową notatkę . W tym celu na rzecz obiektu pośredniczącego wywoływana jest metoda Not e Posted. Zauważmy, że wywołanie to zostało opakowane w kod obsługi wyjątku. Gdy klient stanie się niedostępny z powodu awarii sieci, zawieszenia oprogramowania, awarii maszyny lub błędu programu, które spowodują, że roz­ łączy się on z usługą bez wywoływania metody D i s conn ect, metoda NotePosted proxy zgłosi wyjątek. Nasz kod przechwyci go i usunie klienta z listy, unikając dzięki temu podejmowania kolejnych prób wysyłania do niego nadchodzących notatek. ••

• .·

._,..�;

.______.,

.

,

Przedstawiony tu kod jest nieco uproszczony. Dotyczy to dwóch zasadniczych kwestii. Po pierwsze, moglibyśmy chcieć, aby był on trochę bardziej pobłażliwy dla ewentualnych błędów. Być może przed całkowitą rezygnacją z prób komunikacji powinniśmy dać klientowi szansę na odtworzenie połączenia. Jednym ze sposobów zapewnienia tego byłoby zastosowanie drugiej kolekcji połączeń działającej jako swoista „ławka kar" - korzystając z niej, moglibyśmy dawać „uszkodzonym" klientom jeszcze jedną szansę komunikacji po upływie określonego czasu. (Inne rozwiązanie polegałoby na wymuszeniu na kliencie próby ponownego połączenia w sytuacji, gdy nastąpiła jakaś awaria; w takim przypadku bieżąca obsługa błędów zapewniana przez serwer sprawdzi się świetnie i nic nie będzie trzeba w niej zmieniać) . Po drugie, wywoływanie każdego klienta po kolei za pomocą pętli okaże się mało wydajne, gdy liczba klientów zacznie rosnąć lub gdy niektórzy z nich będą korzystali z niezbyt szybkich połączeń. Zastosowane tu rozwiązanie będzie działało świetnie w przypadku niewielkich grup użytkowników komunikujących się za pośrednictwem prywatnej sieci, jednak w większej skali znacznie lepsze okaże się użycie mechani­ zmów asynchronicznych. Platforma WCF zapewnia pełną obsługę asynchronicznego sposobu korzystania z obiektu pośredniczącego, ale rozdział poświęcony programo­ waniu asynchronicznemu i wątkom znajduje się w dalszej części książki, dlatego nie możemy tu jeszcze przedstawić odpowiednich rozwiązań.

Kod odpowiedzialny za odłączanie klientów znajduje się w osobnej metodzie, ponieważ jest wykorzystywany zarówno w części związanej z obsługą błędów, jak również w metodzie Di s connect stanowiącej część nowego kontraktu. Odpowiednia metoda ma następującą postać: pri vate voi d D i s connectCl i ent ( I ChatCl i ent cl i entCal l back) { s t r i ng name = cl i entsAndNames [cl i en tCal l back] ; Consol e . Wri teli ne (name + " rozł ączony " ) ; cl i entsAndNames . Remove (cl i entCal l back) ;

Metoda ta po prostu usuwa klienta ze słownika. Znacznie upraszcza to kod metody Di s connect: publ i c vo i d D i s connect () { I ChatCl i ent cl i entCal l back = Opera t i onContext . Current . GetCal l bac kChanne l < I ChatCl i ent> () ; D i s connectCl i ent (cl i entCal l back) ;

Również tutaj pobieramy interfejs komunikacji zwrotnej, a następnie wywołujemy tę samą metodę pomocniczą, która odpowiada za odłączenie klienta, co kod obsługi błędów. W kodzie serwera powinniśmy wprowadzić jeszcze jedną zmianę: używane przez nas wią­ zanie wsHt t p B i n d i ng nie może obsługiwać dwustronnego sposobu działania, który jest nam tu niezbędny, dlatego musimy zmodyfikować konfigurację programu ChatHos t .

510

Rozdział 13. Sieci

Konfiguracja serwera do komunikacji dwustronnej i korzystania z sesji Jak już wspominaliśmy wcześniej, platforma WCF umożliwia nam zmianę używanego mecha­ nizmu komunikacji poprzez wybranie innego wiązania w pliku konfiguracji programu. Aby to zrobić, nie musimy zmieniać żadnego kodu. Powinniśmy jedynie zmodyfikować plik App.config związany z naszym projektem hosta, a dokładnie występujący w nim element end poi nt:

Wartość atrybutu b i ndi ng zmieniamy tu na wsDua l HttpB i nd i ng . Wiązanie to jest bardzo podobne do wiązania wsHttpBi ndi ng, zapewnia jednak dodatkowo możliwość odwołań zwrotnych. Ponadto automatycznie włącza ono sesje. (Sesje są co prawda dostępne również w przypadku wiąza­ nia wsHttpBi ndi ng, ale standardowo są one tam wyłączone, dlatego do konfiguracji należy dodać odpowiednie ustawienia, jeśli zależy nam na korzystaniu z sesji bez obsługi komunikacji dwu­ stronnej) . Nasz serwer jest już gotowy do działania w trybie dwustronnym. Teraz trzeba się zatem zająć aktualizacją kodu klienta.

Dwustronny kl ient Wprowadziliśmy kilka zmian w kontrakcie: zmodyfikowaliśmy jedyną obecną w nim już metodę, dodaliśmy dwie nowe i zamieniliśmy go w kontrakt dwustronny. Zmieniliśmy również wiązanie. Każda z tych modyfikacji z osobna wymagałaby już aktualizacji klienta, ponieważ wszystkie one mają wpływ na efekt działania operacji dodawania odwołania do usługi przepro­ wadzanej za pomocą polecenia Add Service Reference. (Zmiany te wpływały na kontrakt, konfi­ gurację lub obydwie te rzeczy) . Nie musimy jednak powtarzać całej pracy związanej z doda­ waniem odwołania do usługi. Gdy prawym przyciskiem myszy klikniemy związany z klientem element Service References widoczny w panelu Solution Explorer, w menu kontekstowym ujrzymy polecenie Update Service Reference. Wybranie go powoduje odpowiednie zmodyfikowanie wygenerowanego kodu źródłowego i konfiguracji aplikacji, uwalniając nas od konieczności budowania całości od początku. Wiąże się z tym ponowne załadowanie metadanych, dlatego usługa powinna działać w czasie wykonywania operacji, podobnie jak miało to miejsce w przy­ padku wcześniejszego dodawania odwołania. Gdy już zaktualizujemy odwołanie i ponownie spróbujemy zbudować naszą solucję, otrzy­ mamy dwa błędy kompilatora. Nie powiedzie się wywołanie metody PostNote, ponieważ prze­ kazujemy jej dwa argumenty, a nowy kontrakt wymaga podania tylko jednego. Zobaczymy również następujący komunikat błędu dotyczący wiersza, w którym konstruowany jest obiekt pośredniczący klasy C hatServi ceCl i ent: error CS 1 729 : ' ChatCl i ent . ChatServ i ce . ChatServ i ceCl i ent ' does not conta i n a cons t ructor 6 '"+that t a kes O arguments

Jako że usługa korzysta teraz z kontraktu dwustronnego, wygenerowany obiekt pośredniczący wymaga, aby klient implementował swoją połowę kontraktu. Musimy zatem zapewnić imple­ mentację interfejsu komunikacji zwrotnej i przekazać ją obiektowi pośredniczącemu. Na lis­ tingu 13.10 przedstawiona została prosta implementacja tego interfejsu. 6

Błąd CS1729: ChatCl i ent . ChatServ i ce . ChatServ i ceCl i ent nie zawiera konstruktora przyjmującego O argumentów.

Platforma WCF

511

Listing 13 .10. Implementacja interfejsu komunikacji zwrotnej strony klienta [Cal l bac kBehav i or ( ConcurrencyMode=ConcurrencyMode . Reentrant) ] cl ass ChatCal l back : IChatServ i ceCal l back { publ i c vo i d NotePosted (stri ng from , s t r i ng note) { Consol e . Wr i teli ne ( " { O } : { 1 } " , from , note) ;

'

..

Wydaje się, że interfejs komunikacji zwrotnej zmienił tu nagle swoją nazwę. Tworząc kod serwera, nazwaliśmy go I ChatCl i ent, teraz jednak nosi on nazwę I ChatServi ceCa 1 1 back. To zupełnie normalne, choć nieco zaskakujące działanie ma związek z korzystaniem z mechanizmu wymiany metadanych przy użyciu zapewnianej przez środowisko Visual Studio funkcji dodawania odwołania do usługi. Nie ma się jednak czym martwić. Dopóki w całą sprawę zaangażowana jest platforma WCF, kontrakt ma tylko jedną nazwę (która w naszym przypadku brzmi I ChatCl i ent ) , nawet gdy jest on podzielony na części związane ze stroną serwera i klienta. WCF uważa nazwę interfejsu używanego po stronie klienta za nieistotną i nie ogłasza jej za pośrednictwem mechanizmu wymiany metadanych . Gdy dodajemy lub aktualizujemy odwołanie do usługi korzystającej z kontraktu dwustronnego, środowisko Visual Studio tworzy nazwę interfejsu strony klienta, po prostu dołączając słowo Ca 1 1 back do zdefiniowanej wcześniej nazwy kontraktu.

Zwróćmy uwagę na atrybut Ca 1 1 backBehavi or. Definiuje on właściwość Con curren cyMode, podob­ nie jak miało to miejsce po stronie serwera. Również w tym przypadku przypisaliśmy jej war­ tość Con c urren cyMode . Reentrant, określając w ten sposób, że ten konkretny kod obsługi komu­ nikacji zwrotnej spodziewa się mieć do czynienia z jedną sesją naraz, lecz może sobie radzić z wywołaniami zwrotnymi pochodzącymi od serwera, w czasie gdy oczekuje na jakieś dzia­ łania z jego strony. Jest to potrzebne, aby serwer był w stanie wysyłać klientowi powiado­ mienia wewnątrz należącej do niego implementacji metody Pos tNote. Platformie WCF musimy zapewnić instancję implementacji tej komunikacji zwrotnej, dlatego zmodyfikujemy odpowiedzialny za tworzenie obiektu pośredniczącego kod znajdujący się na początku metody Mai n przedstawionej na listingu 13.7: ChatCal l back cal l backObj ect = new ChatCal l ba c k ( ) ; I n s tanceContext cl i entContext = new I n s tanceContext (cal l bac kObj ect) ; ChatServ i ceCl i ent chatProxy = new ChatServ i ceCl i ent (cl i entContext) ;

Następuje tu opakowanie obiektu komunikacji zwrotnej w obiekt klasy I n stan ceContext - repre­ zentuje on sesję i w gruncie rzeczy stanowi po stronie klienta odpowiednik obiektu zwraca­ nego po stronie serwera przez Operat i onCon t ext . C urren t . Dostarcza on różnych składowych narzędziowych umożliwiających zarządzanie sesją, lecz tutaj potrzebujemy go wyłącznie po to, aby przekazać nasz obiekt komunikacji zwrotnej do obiektu pośredniczącego - proxy nie pobiera bowiem odwołania zwrotnego bezpośrednio, lecz wymaga, abyśmy opakowali je w kontekst instancji. Powinniśmy dokonać jeszcze kilku dodatkowych modyfikacji. Pamiętajmy, że klient musi teraz powiadomić serwer, iż zamierza nawiązać połączenie - możemy sprawić, by stało się to bez­ pośrednio po poproszeni u użytkownika o podanie imienia: Consol e . Wri teli ne ( " Pros zę wprowadzi ć swoj e i m i ę : " ) ; bool o k = fal s e ; wh i l e ( l o k) {

512

Rozdział 13. Sieci

s t r i ng name = Consol e . Readli n e ( ) ; o k = chat Proxy . Connect (name) ; i f ( l ok) { Consol e . Wr i teli ne ( " To i m i ę j est j uż używane . Pros zę s próbować podać i nne . " ) ;

Kod ten sprawdza wartość zwróconą przez metodę Con n ect, aby stwierdzić, czy podane przez nas imię nie jest już wykorzystywane, i prosi o wprowadzenie innego, jeśli tak właśnie jest. Użytkownik programu może przejść wszelkie niezbędne procedury prawne w celu zmiany posiadanego imienia, a następnie spróbować podać nowe . W wywołaniu metody Pos tNote nie trzeba już za każdym razem podawać imienia, ponieważ serwer pamięta je teraz dzięki zastosowaniu sesji: chatProxy . PostNote (note) ;

Wreszcie na samym końcu metody Mai n powinmsmy jeszcze umieścić jeden dodatkowy wiersz kodu, którego zadaniem będzie informowanie serwera, że chcemy się rozłączyć: chatProxy . D i s connect () ;

W tej chwili jesteśmy już gotowi do przetestowania naszej aplikacji. Możemy uruchomić pro­ gramy klienta i serwera tak, jak robiliśmy to wcześniej, jednak przydałby się nam jeden lub dwa dodatkowe klienty, abyśmy mogli sprawdzić działanie tej obsługującej wielu użytkowni­ ków usługi . Środowisko Visual Studio nie zapewnia możliwości debugowania dwóch instancji tej samej aplikacji, dlatego dodatkowe musimy uruchomić ręcznie. W tym celu należy odszukać folder, w którym został umieszczony skompilowany program. Będzie to podfolder folderu projektu - program będzie się znajdował w podkatalogu bin\debug. Korzystając z dwóch instan­ cji aplikacji klienta, możemy podać różne imiona i zobaczyć wiadomości pojawiające się w oknie konsoli usługi w momencie nawiązywania połączeń przez kolejnych użytkowników: Serv i ce ready Łu kas z po1ączony P i otr po1 ączony

Gdy wprowadzimy notatkę w oknie jednego z klientów, zostanie ona wyświetlona we wszyst­ kich oknach konsoli pozostałych, jak również w oknie konsoli serwera. Interfejs użytkownika naszego programu ma przed sobą długą drogę, zanim będzie on mógł się stać popularnym narzędziem do prowadzenia pogawędek internetowych, ale zaprezento­ waliśmy kompletną, choć dość podstawową aplikację działającą w oparciu o platformę WCF. Oczywiście udało nam się tu tylko bardzo powierzchownie poznać WCF, gdyż jest to wystar­ czająco rozległa technologia, aby poświęcić jej osobną książkę. Jeśli chcemy zaznajomić się z tą platformą lepiej i dowiedzieć się, co jeszcze może nam ona zaoferować, dobrą pozycją na początek może być książka Learning WCF, o której wspominaliśmy już wcześniej kilkukrotnie. Teraz jednak przyjrzymy się sposobom bezpośredniego korzystania z protokołu HTTP.

Protokół HTTP Biblioteka klas platformy .NET zawiera szereg różnych klas umożliwiających bezpośrednie używanie protokołu HTTP. Niektóre z nich przeznaczone są do tworzenia rozwiązań klienc­ kich i przydają się, gdy naszym zadaniem jest pobieranie z serwerów WWW zasobów takich jak bitmapy lub też gdy mamy korzystać z pracujących w oparciu o protokół HTTP usług, Protokół HTTP

I

513

których prostej obsługi z jakichś przyczyn nie zapewnia platforma WCF. Dzięki tym klasom możemy również tworzyć rozwiązania działające z wykorzystaniem HTTP po stronie serwera. Zwykle robi się to, pisząc aplikacje WWW ASP.NET, którym przyjrzymy się w dalszej części tego rozdziału, istnieje też jednak klasa, która umożliwia innym typom programów odbiera­ nie żądań HTTP . Klasa ta nosi nazwę H t t p l i s t ener. (Nie będziemy się nią tu zajmować; wspomnieliśmy o niej głównie po to, aby zaprezentować Czytelnikowi pełne spektrum możli­ wości . Częściej stosowanym rozwiązaniem jest zastosowanie technologii ASP.NET, której w całości poświęciliśmy jeden z rozdziałów niniejszej książki) .

Kl ient WWW Najczęstszym punktem wyjścia dla kodu obsługi HTTP po stronie klienta jest klasa WebCl i ent należąca do przestrzeni nazw System . N et . Oferuje ona kilka sposobów korzystania z tego proto­ kołu, począwszy od bardzo prostych, lecz nieelastycznych metod, a skończywszy na stosun­ kowo skomplikowanych mechanizmach, które zapewniają nam pełną kontrolę nad szczegóło­ wymi ustawieniami HTTP. Zaczniemy od poznania najprostszych z nich. '

..

, ....,'.___�_

Mimo że w przykładach przedstawionych w tym punkcie wykorzystywany jest protokół HTTP, klasa WebCl i ent zapewnia również wsparcie dla innych protokołów, włączając w to obsługę łańcuchów URL https : , ftp : oraz fi l e : . Mechanizm ten ma charakter rozszerzalny, dlatego zasadniczo możemy dostosować go do dowolnego protokołu wykorzystującego schemat URL.

Pobieranie zasobów Listing 13.11 przedstawia jeden z najprostszych sposobów zastosowania klasy WebCl i ent . Kon­ struujemy tu jej instancję, a następnie używamy należącej do niej metody Down l oadStri n g w cel u pobrania danych spod podanego adresu URL. (URL ten można tu podać w postaci łań­ cucha znakowego lub obiektu klasy Uri ) .

Listing 13.1 1 . Pobieranie treści za pomocq obiektu klasy WebClient WebCl i ent cl i ent = new WebCl i ent () ; s t r i ng pageContent = cl i ent . Down l oadStri ng ( " http : //hel i on . pl / " ) ; Consol e . Wri teli ne (pageContent) ;

Wykonanie metody Down l oadSt ri ng zakończy się sukcesem oczywiście tylko wówczas, gdy łańcuch URL, spod którego mają zostać pobrane dane, rzeczywiście wskazuje treść o charak­ terze tekstowym. Łańcuch URL wykorzystany w przykładzie przedstawionym na listingu 13.11 kieruje do strony WWW napisanej w języku HTML, a więc mającej format tekstowy, dlatego tutaj rozwiązanie to sprawdza się świetnie. Co jednak, gdy mamy pobrać bitmapę lub plik ZIP? W takim przypadku należy skorzystać z metody Downl oadData, która działa zasadniczo w ten sam sposób oprócz tego, że zamiast łańcucha znakowego zwraca tablicę bajtów: byte [] data = cl i ent . Down l oadData ( " http : //hel i on . pl /i mg/rozne/rozne/sensus/ma i n . j pg " ) ;

Istnieje jeszcze trzecia prosta metoda umożliwiająca pobieranie danych, która nosi nazwę Down l oad F i l e. Korzystając z niej, można pobrać podany zasób i zapisać go w postaci pliku na lokalnym dysku twardym: cl i ent . Down l oad Fi l e ( " http : //hel i on . pl / " , @ " c : \temp\hel i on . html " ) ;

514

I

Rozdział 13. Sieci

Łańcuchy U RL i URI oraz klasa Uri Zasoby HTTP identyfikuje się za pomocą ujednoliconych lokalizatorów zasobów (ang. Uniform Resource Locator - URL), czyli łańcuchów znakowych zawierających wystarczająco dużo informacji, aby

komputer był w stanie jednoznacznie określić, gdzie powinien znaleźć odpowiednie zasoby. Spe­ cyfikacja łańcuchów URL definiuje je jako szczególny rodzaj ujednoliconych identyfikatorów zasobów (ang. Uniform Resource Identifier - URI). URI jest nieco ogólniejszą koncepcją - łańcuchy te określają jedynie pewną nazwę, która może, lecz nie musi informować o lokalizacji danego zasobu. Wszystkie łańcuchy URL są łańcuchami URI, lecz jedynie te łańcuchy URI, które wskazują położenie zasobów, są łańcuchami URL. Obydwa rodzaje identyfikatorów korzystają z tej samej składni, dlatego platforma .NET zapewnia tylko jedną klasę do ich obsługi. Jest to zdefiniowana w przestrzeni nazw System klasa Uri . Zawiera ona właściwości pomocnicze, które umożliwiają dostęp do różnych części składowych łańcuchów URI. Rozważmy następujący przykład: Uri bl og = new Uri ( " http : //www . i nteract- sw . eo . uk/i angbl og/ " ) ; Obiekt ten reprezentuje łańcuch URL określający adres internetowego bloga jednego z autorów niniejszej książki. Wartość właściwości S cheme tego obiektu to http, wartość jego właściwości Host to www . i nteract-sw . co . uk, a dostępne są tu oczywiście również inne właściwości odpowiadające wszyst­ kim pozostałym elementom składniowym występującym w łańcuchach URI. Dostępne w ramach biblioteki klas platformy .NET metody i właściwości, które wymagają poda­ nia łańcucha URL, mają sygnatury wskazujące, że przyjmuj ą one obiekty klasy U ri . (Niektóre API oferują tu również przeładowania akceptujące łańcuchy znakowe) .

Wszystkie te trzy metody blokują wykonanie programu - nie zwracają one wartości i nie kończą swojego działania, dopóki nie zakończą pobierania danych, o które poprosiliśmy (lub dopóki nie podejmą takiej próby i nie zostanie ona zakończona niepowodzeniem, w wyniku czego zgłoszą one pewnego rodzaju wyjątek) . Może to oczywiście zająć trochę czasu. Możemy przecież korzystać z wolnego łącza internetowego lub odwoływać się do bardzo obciążonego serwera, lub też po prostu pobierać szczególnie duży zbiór danych. Gdy buduje się GUI, niezbyt dobrym pomysłem jest korzystanie z blokujących API7 . Na szczęście klasa WebCl i ent oferuje asynchroniczne wersje wszystkich tych trzech metod. Korzysta się z nich, dołączając kod obsługi zdarzenia do odpowiedniego zdarzenia zakończenia działania, tak jak zostało to przedstawione na poniższym przykładzie . cl i ent . Down l oad F i l eCompl eted += OnDown l oadCompl ete ; cl i ent . Down l oad F i l eAsync (new Uri ( " http : //hel i on . pl / " ) , @ " c : \ temp\ " ) ;

s t at i c vo i d OnDown l oadCompl ete (obj ect s ende r , AsyncCompl etedEventArgs e) { Mess ageBox . Show ( " Pobi eran i e zakończone " ) ;

Wszystkie metody Down l oadXxxAsyn c zwracają sterowanie natychmiast po wywołaniu. Klasa WebCl i ent zgłasza odpowiednie zdarzenie Down l oadXxxComp l eted, gdy tylko dane zostaną pobrane. (Oznacza to, że będziemy musieli zapewnić, iż nasza aplikacja będzie działała wystarczająco 7 W przypadku aplikacji wielowątkowej zwykle można sobie pozwolić na wywoływanie blokujących API

w wątku roboczym. Jest to złym pomysłem jedynie wówczas, gdy korzysta się z wątku Ul, stanowi on jednak ten wątek, w którym odbywają się wszystkie interesujące operacje Ul, dlatego łatwo tu popełnić ten błąd.

Protokół HTTP

515

długo, aby mogło do tego dojść. Gdybyśmy zatem zamierzali skorzystać z asynchronicznych rozwiązań w aplikacji konsolowej, musielibyśmy podjąć pewne kroki w celu zagwarantowa­ nia, że program nie zakończy swojej pracy wcześniej, niż dobiegnie końca operacja pobierania) . Oczywiście, w przeciwieństwie do swoich blokujących odpowiedników, metody Down l oadStri ng "+Asyn c oraz Down l oadDataAsyn c nie są w stanie dostarczać pobranych danych w postaci wartości zwracanych, dlatego przekazują je za pomocą parametru Resul t zdarzeń związanych z zakoń­ czeniem ich działania . '

. .

Gdy będziemy pisali klienta Silverlight, przekonamy się, że klasa WebCl i ent oferuje wyłącznie asynchroniczne wersje odpowiednich metod. Ogólnie rzecz biorąc, zasada ta sprawdza się zresztą we wszystkich rozwiązaniach sieciowych tworzonych przy użyciu technologii Silverlight - z racji tego, że jest ona przeznaczona do budowania interfejsów użytkownika, w ogóle nie oferuje blokujących odpowiedników metod.

Oprócz dostarczania powiadomień dotyczących wystąpień odpowiednich zdarzeń zakończe­ nia klasa WebCl i ent zapewnia również powiadamianie o postępie procesu za pomocą zdarzenia Down l oad ProgressChanged. Jest ono zgłaszane co jakiś czas w trakcie trwania pobierania asyn­ chronicznego niezależnie od tego, z której z trzech wspomnianych powyżej metod się korzysta. Zdarzenie to udostępnia dwie właściwości o nazwach BytesRecei ved i Total Byt esToRecei ve. Infor­ mują one odpowiednio o tym, jak daleko posunął się proces pobierania oraz jak wiele danych zostało jeszcze do pobrania . '

. .

Gdy korzystamy z tych asynchronicznych metod w GUI budowanym przy użyciu WPF lub Windows Forms, nie musimy martwić się o kwestie związane z obsługą wątków. Jak Czytelnik przekona się podczas lektury kolejnych rozdziałów, nie sprawdza się to jednak w przypadku wszystkich asynchronicznych API. Te, o których tu mowa, automatycznie przejmują od nas obsługę wątków ur - dopóki będziemy rozpoczynali operacje asynchroniczne z poziomu takiego wątku, klasa WebCl i ent będzie zgłaszała w nim odpowiednie zdarzenia zakończenia i postępu.

Przesyłanie zasobów na serwer Klasa WebCl i ent oferuje metody U p l oadStri ng, U p l oadData oraz U p l oad F i l e. Odpowiadają one bez­ pośrednio metodom Down l oadStri ng, Down l oadData oraz Down l oad F i l e, jednak zamiast umożli­ wiać pobieranie danych za pomocą metody HTTP G ET, pozwalają przesyłać dane do serwera, używając w tym celu zwykle metody HTTP POST, choć istnieją również ich przeładowania, które dają możliwość wyboru innych poleceń takich jak PUT .

Strumieniowe pobieranie i przesyłanie danych na serwer Wiele API dostępnych w ramach platformy .NET współpracuje z klasą abstrakcyjną St ream zdefiniowaną w przestrzeni nazw Sys t em . I O . Ładować dane ze strumienia lub zapisywać je w nim mogą na przykład klasy XML. Ze strumieniami mogą również współpracować klasy umożliwiające kodowanie i dekodowanie bitmap w mechanizmie WPF. Trzy pierwsze wiersze przykładowego kodu zaprezentowanego na listingu 13.12 odpowiedzialne są za pozyskanie

516

Rozdział 13. Sieci

strumienia dla źródła Atom8 od obiektu klasy WebCl i ent i użycie go do zainicjalizowania obiektu klasy XDocument . Następnie zostaje tu wykorzystany mechanizm LINQ to XML w celu wydoby­ cia listy tytułów i łączy rozgłaszanych przez to źródło.

Listing 13.12. Pobieranie danych z protokołu HTTP do LINQ to XML za pośrednictwem klasy Stream WebCl i ent cl i ent = new WebCl i ent () ; Stream feedStm = c l i ent . OpenRead ( " http : //feeds . feedburne r . com/ore i l l y/news " ) ; XDocument feedXml = XDocument . Load (feedStm) ; s t r i ng ns = " ht t p : //www . w3 . org/2005/Atom " ; var entri es = from entryEl ement i n feedXml . Descendants (XName . Get ( " entry " , ns) ) sel ect new T i t l e = entryEl ement . El ement (XName . Get ( " t i t l e " , ns ) ) . Val u e , Li n k = entryEl ement . El ement (XName . Get ( " l i n k " , n s ) ) . Attri bute ( " href " ) . Val ue }; foreach (var entry i n entri es) { Consol e . Wri teli ne ( " { O } : { 1 } " , entry . T i t l e , entry . Li n k) ;

Wysyłanie danych umożliwia metoda OpenWri te. Dla protokołu HTTP lub HTTPS standardowo jest przez nią wykorzystywana metoda POST, lecz podobnie jak w przypadku metod Upl oad można tu skorzystać z przeładowań, które oprócz łańcucha URL pobierają również nazwę odpowiedniego polecenia. Ze strumieni można korzystać w sposób asynchroniczny. Używając tego samego schematu działania co dla innych opisanych tu metod, możemy zastosować metody Open ReadAsync i Open "+Wri t eAsyn c oraz odpowiadające im zdarzenia zakończenia . Strumienie wprowadzają jednak dodatkowy wymiar: abstrakcyjna klasa bazowa St ream również oferuje operacje synchroniczne i asynchroniczne. Na przykład gdy odczytujemy dane, możemy wywołać zarówno metodę Read, jak i Beg i n Read . Mamy możliwość korzystania z klasy Stream w każdym z tych trybów, niezależnie od tego, czy odpowiedni strumień uzyskaliśmy dzięki klasie WebCl i ent w sposób synchroniczny, czy asynchroniczny. Miejmy jednak na uwadze to, że próbując uniknąć bloko­ wania, aby zapewnić interfejsowi użytkownika możliwość reakcji, prawdopodobnie zechcemy otrzymać strumień w sposób asynchroniczny (a więc na przykład zastosować metodę Open Read "+Async) oraz używać go asynchronicznie . Gdy otwieramy strumień w taki sposób, zdarzenie zakończenia informuje nas, że obiekt klasy WebCl i ent jest gotów do rozpoczęcia odczytywania (lub zapisywania) danych, nie stanowi to jednak gwarancji, że będziemy w stanie natych­ miast zakończyć tę operację . Na przykład gdy skorzystamy z metody Open ReadA syn c w celu pobrania pliku o wielkości 1 GB za pośrednictwem protokołu HTTP, obiekt klasy WebCl i ent nie będzie oczekiwał z przekazaniem nam strumienia na zakończenie pobierania całego giga­ bajta. Zdarzenie Open ReadComp l eted wystąpi, gdy tylko zacznie on pobierać dane, abyśmy mogli natychmiast rozpocząć dalsze ich przetwarzanie . Gdy jednak spróbujemy odczytać dane ze strumienia z większą szybkością niż ta, z którą nasze połączenie sieciowe jest je w stanie pobie­ rać, będziemy zmuszeni do oczekiwania. Jeśli zatem zależy nam na uniknięciu blokowania, musimy także asynchronicznie korzystać ze strumienia . 8

Atom jest popularnym formatem reprezentującym zbiory elementów takich jak wpisy na blogach czy arty­ kuły publikowane na stronach WWW. Jest on podobny do standardu RSS, jednak próbuje wyeliminować niektóre związane z nim ograniczenia i niezgodności.

Protokół HTTP

517

Podczas gdy asynchroniczne metody oferowane przez klasę WebCl i ent komunikują się z właściwym wątkiem w aplikacji GUI, asynchroniczne metody strumienia tego nie robią, co oznacza, że musimy samodzielnie zatroszczyć się o kwestie związane z obsługą wątków.

Dostęp do najpotężniejszego mechanizmu oferowanego przez klasę WebCl i ent zapewniają jej metody GetWebReq uest oraz GetWeb Res pon se. Stanowią one jednak w gruncie rzeczy opakowania dla zestawu zupełnie innych klas - klasa WebCl i ent jedynie zapewnia wygodny sposób korzy­ stania z nich w naszych aplikacjach. Zajmijmy się więc klasami, które naprawdę są odpowie­ dzialne za przeprowadzanie operacji wykonywanych przez te metody.

Klasy WebRequest i WebResponse WebRequest i WebRespon se to abstrakcyjne klasy bazowe dla rodziny klas, które zapewniają najbar­ dziej precyzyjny poziom kontroli nad żądaniami WWW. Nieabstrakcyjne klasy Htt pWebRequest oraz HttpWebRes pon s e dodają szczegóły właściwe dla protokołu HTTP, a platforma .NET oferuje również specjalizowane klasy FtpWebRequest i FtpWebRespon s e oraz F i l eWebRequest i F i l eWebRespon se. W tym punkcie skupimy się na klasach związanych z protokołem HTTP.

Główne ograniczenie związane z zastosowaniem mechanizmów działających w oparciu o klasę WebCl i ent, którymi zajmowaliśmy się do tej pory, polega na tym, że skupiają się one na treści żądania lub odpowiedzi. Nie zapewniają one natomiast żadnych sposobów korzystania ze standardowych cech protokołu HTTP takich jak nagłówek typu treści, łańcuch UserAgent, usta­ wienia pamięci podręcznej czy konfiguracja proxy. Jednak wszystkie te szczegóły związane z protokołem HTTP staną się dla nas dostępne, gdy zastosujemy klasy H t t pWeb Req u e s t oraz Htt pWebResponse. Kosztem, który musimy ponieść, chcąc korzystać z tych możliwości, jest dodatkowa rozwle­ kłość kodu. Główna różnica polega tu na tym, że musimy używać jednego obiektu reprezentu­ jącego żądanie i jednego reprezentującego odpowiedź jako dodatku do strumieni reprezen­ tujących dane, które są wysyłane lub odbierane. Co więcej, jedynym sposobem uzyskania dostępu do danych za pomocą tych klas jest zastosowanie strumieni. Aby osiągnąć ten sam efekt, który daje nam kod przedstawiony na listingu 13.1 1 - czyli pobrać dane z określonego adresu URL do łańcucha znakowego - trzeba użyć znacznie bardziej skomplikowanego kodu, który został zaprezentowany na listingu 13.13.

Listing 13.13. Pobieranie larłcucha znakowego za pomocą klas HttpWebRequest oraz HttpWebResponse HttpWebRequest req = (HttpWebReques t) WebReques t . Create ( " http : //hel i on . pl / " ) ; us i ng (HttpWebResponse resp = (HttpWebResponse) req . GetRespon s e () ) us i ng (Stream respStream = resp . Get ResponseStream () ) us i ng (StreamReader reader = new StreamReader (respStream) ) { s t r i ng pageContent = reader . ReadToEnd () ; Consol e . Wri teli ne (pageContent) ;

Dwa rzutowania widoczne w dwóch pierwszych wierszach kodu przedstawionego na tym lis­ tingu wyglądają nieco niechlujnie, ale niestety są tu zwykle niezbędne. Rodzinę klas WebRequest można rozszerzać o obsługę wielu protokołów, dlatego większość należących do nich metod została zadeklarowana w taki sposób, aby zwracać typy klas abstrakcyjnych zamiast typów konkretnych - dokładny typ zwracany zależy tu od rodzaju łańcucha URL, którego użyjemy. 518

I

Rozdział 13. Sieci

Jeśli zatem potrzebujemy dostępu do cechy charakterystycznej dla protokołu, musimy zastoso­ wać odpowiednie rzutowanie. W gruncie rzeczy w przykładzie przedstawionym na listingu 13.13 nie korzystamy z niczego, co byłoby specyficzne dla jakiegoś protokołu, dlatego moglibyśmy uniknąć tych rzutowań, deklarując zmienne req oraz resp odpowiednio jako obiekty klas Web "+Req uest i WebRes pon s e . Jednak powodem, dla którego najczęściej używamy tych klas, jest to, że tak naprawdę zależy nam na dostępie do informacji specyficznych dla protokołu HTTP. Moglibyśmy na przykład chcieć zasymulować działanie określonej przeglądarki internetowej, odpowiednio ustawiając łańcuch znakowy wskazujący agenta użytkownika, tak jak zostało to pokazane na listingu 13.14.

Listing 13. 14. Modyfikacja nagłówka określającego agenta użytkownika za pomocą klasy HttpWebRequest HttpWebRequest req = (HttpWebReques t) WebReques t . Create ( " http : //hel i on . pl / " ) ; req . UserAgent = "Moz i l l a/5 . 0 ( i Pod ; U ; CPU i Phone OS 2_2_1 l i ke Mac OS X ; en- u s ) Appl eWebKi t/525 . 18 . 1 '"+ ( KHTM L , l i ke Gecko) Mobi l e/5Hl l a " ; . . . tak j a k wcześn i ej

Jak widać, wiersz zawierający łańcuch znakowy, który określa agenta użytkownika, musiał zostać zawinięty, ponieważ łańcuch ten jest zbyt długi, aby mógł się zmieścić na stronie w nor­ malny sposób. Kod ten umożliwia nam stwierdzenie, jaką odpowiedź wyśle serwis WWW, gdy żądanie nadejdzie z urządzenia iPhone firmy Apple. (W przypadku wielu serwisów WWW treść jest odpowiednio dostosowywana do wymogów różnych urządzeń) . Jak Czytelnik z pewnością się spodziewa, możliwe są tu działania asynchroniczne, dzięki którym da się uniknąć blokowania bieżącego wątku na czas oczekiwania na zakończenie operacji sieciowych. Wyglądają one jednak nieco inaczej, niż miało to miejsce w przypadku mechanizmów pracujących w oparciu o klasę WebCl i ent, z którymi mieliśmy do czynienia do tej pory. Przyczyną tego stanu rzeczy jest sposób, w jaki wywoływane przez nas metody mogą wpływać na to, kiedy żądania zostają wysłane. W momencie tworzenia żądania nie następuje jeszcze żadna komunikacja sieciowa, dlatego nie istnieje odpowiednia metoda asynchroniczna umożliwiająca przeprowadzanie tej operacji. Pamiętajmy, że obiekt żądania reprezentuje wszyst­ kie ustawienia, których chcemy użyć w swoim żądaniu HTTP, dlatego nie będzie on próbo­ wał niczego wysłać aż do czasu, gdy zakończymy konfigurację właściwości żądania i damy mu wyraźnie znać, że jesteśmy gotowi na dalsze działania . Dostępne są dwa sposoby sprawienia, aby obiekt klasy Htt pWebRequest wysłał żądanie. Można to zrobić, prosząc o obiekt odpowiedzi, lecz można również poprosić o strumień żądania metoda GetStream obiektu żądania zwraca przeznaczony tylko do zapisu strumień, który można wykorzystać do dostarczenia ciała żądania dla metody POST lub innego pokrewnego polecenia (podobnie jak ma to miejsce w przypadku metody Web Cl i ent . OpenWri te) . Strumień ten zacznie wysyłać dane za pośrednictwem sieci, gdy tylko nasz kod zapisze w nim jakieś dane. Metoda ta nie będzie zatem oczekiwać, aż zamkniemy strumień, aby wysłać wszystkie dane za jednym razem. (Nie zna ona naszych intencji, więc może się spodziewać, że planujemy wysyłanie gigabajtów danych) . Oznacza to, że do czasu, gdy zwróci strumień, metoda ta musi być gotowa na rozpoczęcie wysyłania danych, co oznacza z kolei, że początkowe fazy prze­ twarzania żądania HTTP muszą być na tym etapie zakończone . Jeśli zatem żądanie z jakichś przyczyn się nie powiedzie (na przykład dlatego, że nie działa serwer lub maszyna klienta utraciła połączenie z siecią), podejmowanie prób dostarczania danych dla żądania nie będzie miało większego sensu. Dlatego też gdy poprosimy o strumień, zostaniemy powiadomieni o ewentualnym pojawieniu się błędów tego rodzaju.

Protokół HTTP

I

519

Wynika stąd, że metoda GetSt ream jest metodą blokującą - nie zwróci ona sterowania, dopóki nie uda się jej połączyć z serwerem i nie rozpocznie się przetwarzanie żądania. Z tego powodu istnieje jej asynchroniczna wersja. Klasa WebRequest nie zapewnia jednak obsługi modelu działa­ jącego w oparciu o zdarzenia, który jest wykorzystywany w przypadku klasy WebCl i ent . Zamiast niego używa bardziej skomplikowanego, lecz nieco elastyczniejszego modelu programowania asynchronicznego (ang. Asynchronous Programming Model) pracującego w oparciu o metody. Wywołuje się w nim metodę Beg i nGet Req uestStream, przekazując delegację do metody, która zostanie wywołana zwrotnie przez żądanie, gdy będzie ono już gotowe do przetwarzania, w któ­ rym to momencie wywołamy metodę EndGet Reques tStream . Ten schemat rozpoczynania i koń­ czenia żądania jest powszechnie stosowany w platformie .NET i zostanie dokładniej opisany w rozdziale 16. Drugi sposób, w jaki można wyzwolić wysłanie żądania, polega na poproszeniu o obiekt odpo­ wiedzi - jeśli nie poprosiliśmy wcześniej o strumień żądania (na przykład dlatego, że chcemy wywołać metodę G ET, a co za tym idzie, nie istnieje żadne ciało żądania), żądanie zostanie wysłane w tym momencie. A zatem metoda Get Response ma także odpowiednią wersję asynchro­ niczną. Również ona korzysta ze schematu asynchronicznego działającego w oparciu o metody. Na listingu 13.15 przedstawiona została zmodyfikowana wersja kodu z listingu 13.13, w której obiekt odpowiedzi otrzymywany jest w sposób asynchroniczny.

Listing 13.15. Asynchroniczne uzyskiwanie odpowiedzi HttpWebRequest req = (HttpWebReques t) WebReques t . Create ( " http : //hel i on . pl / " ) ; req . Beg i nGetRespon s e (del egate ( I AsyncRes ul t asyncResul t) { us i ng (HttpWebResponse resp = (HttpWebResponse) req . EndGet Response (asyncRe s u l t) ) us i ng (Stream respStream = resp . Ge t ResponseStream () ) us i ng (StreamReader reader = new S t reamReader (respStream) ) { stri ng pageContent = reader . ReadToEnd () ; Consol e . Wr i teli ne (pageContent) ; } } , nu 1 1 ) ;

W przykładzie tym w roli wywołania zwrotnego zakończenia zastosowana została metoda anonimowa, dzięki czemu możliwe jest zachowanie struktury kodu podobnej do oryginalnej wersji synchronicznej . Musimy tu jednak być świadomi, że kod obsługi odpowiedzi przed­ stawiony na listingu 13.15 stanowi teraz oddzielną metodę i potencjalnie będzie działać jeszcze długo po tym, jak zakończy pracę metoda Beg i nGet Respon s e . Niewykluczone też, że będzie się to odbywać w innym wątku. Zatem podobnie jak miało to miejsce w przypadku modelu wyko­ rzystującego zdarzenia, będziemy musieli zapewnić, że aplikacja będzie działała wystarczająco długo, aby operacja mogła dobiec końca - to, że w tle będą działały jakieś niezakończone ope­ racje asynchroniczne, nie spowoduje, że nasz główny proces będzie nadal działać, gdy zakończą się wszystkie jego pierwszoplanowe wątki. Ten asynchroniczny schemat działania nie dba o kwestie związane z wątkami Ul (w przeciwieństwie do rozwiązania wykorzystującego zdarzenia, o którym była mowa wcześniej). Wywołanie zwrotne zakończenia odbędzie się prawdopodobnie w ramach jakiegoś przypadkowego wątku i wszelkie próby aktualizacji interfejsu użytkownika z poziomu tego kodu zawiodą. W rozdziale 16. pokażemy, jak poradzić sobie z tym problemem.

520

I

Rozdział 13. Sieci

Listing 13.14 prezentuje tylko jedną z cech protokołu HTTP, które da się dostosowywać, a mia­ nowicie łańcuch U s erAgen t . W rzeczywistości dostępnych jest wiele podobnych ustawień, a duża ich część jest dość mało znana, dlatego nie będziemy się nimi wszystkimi tu zajmować. Temu służy dokumentacja MSDN. Przyjrzymy się jednak najbardziej typowym przypadkom.

Uwierzyteln ianie Protokół HTTP oferuje klientowi różne sposoby uwierzytelniania się wobec serwera. Zauważmy, że większość dostępnych publicznie serwisów WWW w rzeczywistości nie korzysta z żadnego z nich. Witryna internetowa zapewniająca interfejs logowania, za pomocą którego możemy podać swoją nazwę użytkownika i hasło dostępu, wpisując je bezpośrednio do odpowiednich pól na stronie, sama w ogóle nie używa mechanizmu uwierzytelniania HTTP, lecz zamiast tego zwykle wykorzystuje ciasteczka (więcej informacji na ten temat znajdzie Czytelnik w dal­ szej części rozdziału) . Uwierzytelnianie HTTP jest używane w dwóch podstawowych przypad­ kach. Z najbardziej widocznym mamy do czynienia, gdy przeglądarka internetowa otwiera niewielkie okno z prośbą o dane uwierzytelniające, zanim przejdzie do odpowiedniej strony WWW - jest to sytuacja rzadziej spotykana niż logowanie odbywające się za pośrednictwem formularza dostępnego na stronie, jednak istnieją serwisy WWW działające właśnie w ten sposób. Mniej zauważalne jest uwierzytelnianie HTTP wykorzystywane w zintegrowanych rozwiązaniach bezpieczeństwa, na przykład w takich, w których maszyna klienta należy do domeny Windows, a tożsamość użytkownika jest automatycznie dostępna dla serwera WWW pracującego w intranecie i należącego do tej samej domeny. W tym przypadku nie ma koniecz­ ności jawnego logowania się w serwisie intranetowym, a i tak doskonale wie on, z kim na do czynienia - zawdzięcza to właśnie zastosowani u niejawnego uwierzytelniania HTTP. Klasa HttpWebRequest standardowo nie będzie próbowała uwierzytelniać klienta wobec serwera nawet w przypadku zintegrowanych rozwiązań uwierzytelniania. (Mamy tu zatem do czy­ nienia z inną polityką standardową, niż ma to miejsce w przypadku przeglądarki Internet Explorer. IE automatycznie uwierzytelnia nas w przypadku serwerów pracujących w naszej sieci lokalnej zapewniającej zintegrowaną autoryzację, nie robi tego jednak klasa HttpWebRequest ) . Gdy piszemy kod klienta i chcemy, aby identyfikował on użytkownika wobec serwera, musimy ustawić właściwość Credent i a l s żądania. W przypadku autoryzacji zintegrowanej istnieje specjalny obiekt uwierzytelniający, który repre­ zentuje tożsamość użytkownika . Zapewnia go klasa Credent i a l Cache. Przykładowy kod przed­ stawiony na listingu 13.16 prezentuje sposób skorzystania z niego w celu włączenia mechani­ zmu zintegrowanej autoryzacji. (Sprawdzi się on oczywiście jedynie wówczas, gdy serwer jest przygotowany do jego użycia, dlatego kod ten tylko informuje klasę Htt pWebRequest, że może korzystać ze zintegrowanego uwierzytelniania, jeśli serwer o nie poprosi . Gdy okaże się, że serwer w ogóle nie wymaga autoryzacji, nie wystąpi tu żaden błąd) .

Listing 13.16. Umożliwianie korzystania ze zintegrowanego uwierzytelniania HttpWebRequest request = (HttpWebRequest) WebReques t . Create ( " http : // i ntraweb/ " ) ; reques t . Creden t i al s = Credent i al Cache . Defaul tCreden t i al s ;

Uwierzytelnianie HTTP nie zawsze jest zintegrowane z zabezpieczeniami systemu Windows. Mechanizm ten obsługuje również autoryzację polegającą na podaniu nazwy użytkownika i hasła dostępu. Specyfikacja HTTP dopuszcza dwa sposoby korzystania z tego rozwiązania.

Protokół HTTP

I

521

Uwierzytelnianie podstawowe (ang. basie authentication) po prostu przesyła odpowiednią nazwę użytkownika i hasło dostępu jako część żądania, a zatem jeśli nie używamy protokołu HTTPS, metoda ta nie jest zbyt bezpieczna . Drugi sposób, uwierzytelnianie skrótowe (ang. digest authen­ tication), jest znacznie lepszy, lecz nie zdobył dużej popularności. W praktyce uwierzytelnianie podstawowe przeprowadzane za pośrednictwem protokołu HTTPS wydaje się najczęściej sto­ sowanym rozwiązaniem. W przypadku obydwu tych rodzajów autoryzacji należy podać odpo­ wiednią nazwę użytkownika i hasło dostępu w sposób zaprezentowany na listingu 13.17.

Listing 13.17. Przekazywanie danych uwierzytelniających dla podstawowej lub skrótowej autoryzacji HttpWebRequest request = (HttpWebRequest) WebReques t . Create ( 11 https : // i ntraweb/ 11 ) ; reques t . Credent i al s = new NetworkCredent i al ( 11 uzyt kown i kl 11 , 11h@ s 1 0 11 ) ;

Rozwiązanie to nie umożliwia nam określenia, czy ma być używane uwierzytelnianie pod­ stawowe, czy skrótowe, ponieważ wybór ten należy do serwera . Co za tym idzie, nie wiemy, czy hasło zostanie wysłane wprost, czy też w formie zaszyfrowanej, dlatego powinniśmy poda­ wać w ten sposób dane uwierzytelniające jedynie wówczas, gdy wykorzystywany jest protokół HTTPS. Możemy wymusić zastosowanie uwierzytelniania skrótowego, opakowując obiekt klasy NetworkCredent i al w obiekt klasy Credent i al Cache, co pozwala określić wymaganą metodę autoryzacji. Jednak nawet w takim przypadku z pewnością warto będzie zachować ostroż­ ność przy używaniu uwierzytelniania skrótowego bez dodatkowego zabezpieczenia w postaci protokołu HTTPS - choć samo to uwierzytelnianie może być wystarczająco bezpieczne, nie­ które serwery implementują ten mechanizm w niezbyt bezpieczny sposób .

Korzystanie z proxy Ż ądania WWW standardowo będą sprawdzały ustawienia przeglądarki Internet Explorer w celu stwierdzenia, czy ma być używane proxy WWW. Niewykluczone jednak, że będziemy chcieli zmienić ten standardowy sposób działania, dlatego istnieje kilka metod, które nam to umożliwiają. ' .' !..---�

:

Przed wprowadzeniem platformy .NET 2.0 ustawienia proxy IE nie były brane pod uwagę, dlatego możemy jeszcze czasami natrafić na kod, który wykonuje sporo działań mających na celu ustalenie, czy proxy ma być używane, czy też nie. Kod taki jest zwykle dość stary lub został napisany przez kogoś, kto nie wiedział, że w platfor­ mie .NET 2.0 poprawiono ten błąd.

Aby zmienić standardowy sposób działania proxy, powinniśmy dodać kilka wpisów do pliku App.config. Kod przedstawiony na listingu 13.18 powoduje, że żądania WWW przestają korzy­ stać z proxy skonfigurowanego jako standardowe.

Listing 13.18. Konfigurowanie standardowego sposobu działania proxy

Standardowy sposób działania w sytuacji, gdy nie określono żadnych ustawień, polega na włączeniu korzystania z domyślnego proxy, ale aplikacja nie będzie korzystać z danych

522

I

Rozdział 13. Sieci

uwierzytelniających użytkownika do identyfikowania go wobec serwera pośredniczącego. (Uwierzytelnianie użytkownika wobec proxy odbywa się niezależnie od uwierzytelniania go wobec serwera WWW) . Niektóre firmy wymagają od użytkowników uwierzytelniania wobec proxy w celu uzyskania dostępu do internetu - w takim przypadku powinniśmy zmienić tę konfigurację, przypisując atrybutowi us eDefaul tCredent i al s elementu defaul t Proxy wartość true. Ten sposób działania możemy też zmienić w kodzie. Klasa HttpWebRequest posiada właściwość Proxy, której możemy przypisać wartość n u l l w celu wyłączenia używania proxy. Możemy też przypisać jej obiekt klasy WebProxy określający odpowiednie proxy i ustawienia, tak jak zostało to przedstawione na listingu 13.19 .

Listing 13.19. Wskazywanie określonego proxy HttpWebRequest request = (HttpWebRequest) WebReques t . Create ( 11 https : // i ntraweb/ 11 ) ; reques t . Proxy = new Web Proxy ( 11 http: // corpwebproxy/ 11 ) ;

Kontrolowan ie sposobu działania pam ięci pod ręcznej System Windows zapewnia pamięć podręczną zasobów WWW każdemu użytkownikowi z osobna w celu uniknięcia konieczności ciągłego pobierania od nowa często używanych bit­ map, arkuszy CSS, kodów JavaScript, stron HTML i innych treści . Z tej pamięci podręcznej korzysta przeglądarka Internet Explorer, ma jednak też do niej dostęp kod opracowany przy użyciu platformy .NET. Nasze programy standardowo nie będą używać pamięci podręcznej, możemy jednak włączyć korzystanie z niej, ustawiając odpowiednio właściwość CachePo l i cy obiektu żądania, tak jak zostało to zaprezentowane na listingu 13.20 .

Listing 13.20. Określanie polityki korzystania z pamięci podręcznej HttpRequestCachePo l i cy cachePol i cy = new HttpRequestCachePo l i cy (HttpRequestCachelevel . Cache l fAvai l abl e) ;

HttpWebRequest request = (HttpWebRequest) WebReques t . Create ( 11 https : // i ntraweb/ 11 ) ; reques t . CachePol i cy = cachePol i cy ;

Standardowym ustawieniem jest tu BypassCache, co oznacza, że nie tylko żądania nie będą zaglą­ dały do pamięci podręcznej, lecz również wszelkie zasoby, które pobierzemy, nie będą w niej umieszczane . W kodzie przedstawionym na listingu 13.20 zastosowano z kolei inne ustawie­ nie, dzięki któremu używana będzie kopia zasobu przechowywana w pamięci podręcznej, gdy tylko kopia taka będzie dostępna . W przeciwnym przypadku odpowiedni zasób będzie pobierany i umieszczany w pamięci podręcznej (chyba że nagłówki znajdujące się w odpowiedzi HTTP będą wskazywać, że zasób nie powinien być zapisywany w tej pamięci) . Enumeracja HttpRequestCachelevel oferuje również różne inne opcje związane z wykorzystaniem pamięci podręcznej . Gdy chcemy wymusić, aby zasób był pobierany od nowa i aby wynik tego działania znalazł się w pamięci podręcznej, możemy tu wybrać ustawienie Re l oad. Możemy również zażądać sprawdzania świeżości - protokół HTTP umożliwia klientom poinformo­ wanie serwera, że przechowują one w pamięci podręcznej swoją wersję zasobu i chcą pobrać go tylko wówczas, gdy dostępna jest nowsza wersja. Takie działanie mechanizmu możemy osiągnąć, korzystając ze składowej Reval i date. (Dostępne są tu również rzadziej używane opcje, które mogą zainteresować zwłaszcza programistów dobrze zaznajomionych z niuan­ sami mechanizmu działania pamięci podręcznej HTTP i które wymagają pełnej kontroli nad sposobem jego pracy) .

Protokół HTTP

I

523

Korzystanie z ciasteczek Dopóki poruszamy się w granicach nakreślonych przez specyfikację HTTP, każde żądanie jest całkowicie niezwiązane z jakimikolwiek poprzednimi żądaniami pochodzącymi od tego samego klienta. Zwykle jednak serwisowi WWW przydaje się możliwość rozpoznawania serii żądań nadchodzących z jednego źródła i z tego powodu powszechnie stosowany jest zapew­ niający ją mechanizm, który nosi nazwę ciasteczek9 • Ciasteczka umożliwiają działanie takich funkcji serwisów WWW jak koszyki zakupowe, w przypadku których aplikacja WWW musi w jakiś sposób utrzymywać stan użytkownika - chodzi tu oczywiście o to, że użytkownik spodziewa się widzieć w swoim koszyku tylko te rzeczy, które w nim umieścił, nie zaś rzeczy umieszczone przez innych zalogowanych użytkowników w ich koszykach. Ciasteczka są też często wykorzystywane do zarządzania logowaniami - gdy użytkownik wprowadzi w for­ mularz HTML swoją nazwę i hasło dostępu, za uwierzytelnianie go od tej pory odpowiada już ciasteczko. Gdy korzystamy z przeglądarki internetowej, ciasteczka działają bez konieczności jakiejkol­ wiek interwencji z naszej strony (oczywiście aż do czasu, gdy zechcemy je wyłączyć) . Gdy jednak tworzymy kod, aby używać ciasteczek, musimy podjąć ku temu pewne kroki. Standar­ dowo platforma .NET w ogóle nie korzysta z ciasteczek i nie ma dostępu do magazynu ciaste­ 0 czek używanego przez program Internet Explorer1 . Nie zapewnia nam też implementacji wła­ snego magazynu tego rodzaju. Ignorowanie ciasteczek zwykle nie powoduje żadnych problemów. Czasami może się jednak okazać, że musimy napisać program uzyskujący dostęp do serwisu, którego prawidłowe dzia­ łanie jest od nich uzależnione. W takim przypadku aby to zapewnić, powinniśmy opracować odpowiedni kod działający po stronie klienta . Podstawowa koncepcja stojąca za mechanizmem ciasteczek polega na tym, że gdy klient odbiera odpowiedź od serwera, może ona zawierać informacje, które mają być zapamiętane przez klienta i przesłane z powrotem przy okazji zgłoszenia kolejnego żądania. Klient nie ma tu robić niczego ponad dosłowne przekazanie informacji z powrotem - w ciasteczkach nie ma żad­ nych przydatnych danych, które mógłby on wydobyć. (A w każdym razie nie powinno ich tam być, choć historia zna niechlubne przypadki, w których cała idea została niewłaściwie zrozumiana i wykorzystana. Na przykład pewien sklep internetowy popełnił błąd i umiesz­ czał w ciasteczkach ceny towarów dodawanych do koszyka, umożliwiając w ten sposób spryt­ nym klientom przyznawanie sobie zniżek przez ręczną edycję swoich ciasteczek) . Klient ma jedynie odbierać i przechowywać ciasteczka, które otrzymuje, tak jak zostało to przedstawione na listingu 13 .21 .

Listing 13.2 1 . Pobieranie ciasteczek z odpowiedzi serwera Cooki eContai ner contai ner = new Cooki eContai ner () ;

Uri address = new Uri ( " http : //hel i on . pl / " ) ; HttpWebRequest req = (HttpWebReques t) WebReques t . Create (addres s ) ;

9 Ciasteczka są tak powszechnie obsługiwane, że choć z technicznego punktu widzenia nie stanowią części specyfikacji HTTP, równie dobrze mogłyby nią być. 10 Aplikacje Silverlight są tu wyjątkiem. W kwestii zgłaszania żądań HTTP polegają one na przeglądarce interne­ towej, a co za tym idzie żądania te będą wysyłały wszelkie ciasteczka, które normalnie wysyłałaby w takich przypadkach używana przeglądarka.

524

I

Rozdział 13. Sieci

HttpWebResponse res p

=

(HttpWebResponse) req . Get Response () ;

Cooki eCol l ecti on cooki es = resp . Cooki es ; contai ner .Add (addre s s , cooki es) ;

Korzystamy tu z obiektu zapewnianej przez platformę .NET klasy Cook i eConta i ner, aby zapa­ miętać, które ciasteczka otrzymaliśmy od poszczególnych serwerów, z jakimi się kontaktujemy, a także z którymi adresami są one związane . Gdy przymierzamy się do wystosowania kolej­ nego żądania, możemy przekazać ten kontener: Uri address = new Uri ( " http : //hel i on . pl / " ) ; HttpWebRequest newReq = (HttpWebReques t ) WebReques t . Create (addres s ) ; newReq . Cooki eConta i ner = cont a i ner;

Za każdym razem, gdy otrzymujemy odpowiedź, serwer może nam zwrócić zupełnie nowe ciasteczka lub zmodyfikować wartości istniejących, dlatego koniecznie należy aktualizować swój kontener ciasteczek przy każdej okazji, gdy dostarczana jest odpowiedź. Można to robić za pomocą kodu przedstawionego na listingu 13.21 . To tyle na temat protokołu HTTP. Wreszcie możemy się przyjrzeć gniazdom.

Gn iazda Gniazda to najpotężniejszy mechanizm sieciowy dostępny w ramach platformy .NET - proto­ kół HTTP opiera swoje działanie na gniazdach, a w większości przypadków podobnie jest z platformą WCF. Gniazda zapewniają mniej lub bardziej bezpośredni dostęp do leżących u podstaw tych rozwiązań usług TCP /IP - umożliwiają nam one w gruncie rzeczy posługi­ wanie się naturalnym językiem sieci. Może to oferować pewne korzyści związane z elastyczno­ ścią i wydajnością przekraczające to, co jest nam w stanie zapewnić komunikacja odbywająca się w oparciu o protokół HTTP, jednak wadą tych rozwiązań jest konieczność wykonania przez nas dodatkowej pracy. Ponadto w środowiskach korporacyjnych komunikacja ze światem zewnętrznym z doraźnym użyciem gniazd jest zazwyczaj blokowana, ponieważ zapory ogniowe mogą być skonfigurowane w taki sposób, aby przepuszczać jedynie taki ruch, którego się spodziewają i który rozumieją. Jednak w przypadkach, w których tego typu ograniczenia nie istnieją, a także tam, gdzie elastyczność lub (stosunkowo niewielkie) zwiększenie wydajności jest warte dodatkowego wysiłku, gniazda mogą się sprawdzić. Podstawowa koncepcja gniazd jest obecna w komputerowym świecie od dziesiątek lat i wystę­ puje w wielu różnych systemach operacyjnych. Centralna idea polega tu na tym, aby przed­ stawić komunikację sieciową za pomocą pewnego rodzaju obiektów abstrakcyjnych takich jak pliki wejścia-wyjścia. Mieliśmy już z czymś takim do czynienia w przypadku klasy WebCl i ent, która może zapewnić obsługę strumieni. Strumienie te są jednak związane z ciałem żądania lub odpowiedzi HTTP. W przypadku gniazd strumienie znajdują się na niższym poziomie, zawierając wszystkie dane . (Gdybyśmy skorzystali ze strumienia związanego z gniazdami w celu połączenia się z serwerem WWW, w strumieniu tym moglibyśmy zobaczyć wszystkie szczegóły dotyczące protokołu HTTP, nie tylko ciało komunikatu) . Oprócz obiektów abstrakcyjnych takich jak pliki wejścia-wyjścia API gniazd oferują standar­ dowy zestaw operacji umożliwiających nawiązywanie połączeń oraz pozwalających na kon­ trolowanie różnych aspektów działania tych połączeń.

Gniazda

I

525

Aby dobrze zrozumieć gniazda, musimy dysponować pewną wiedzą na temat protokołów sieciowych, na których opierają one swoje działanie, dlatego poza przedstawieniem możliwości odpowiednich API w kolejnym punkcie zamieszczono bardzo krótki przegląd rodziny proto­ kołów TCP /IP. Jeśli Czytelnik już je zna, może śmiało zrezygnować ze szczegółowej lektury następnego punktu i jedynie przejrzeć go pobieżnie, nieco więcej uwagi poświęcając przykła­ dom prezentującym zastosowanie tych protokołów . ••



. •

._,..�;

.

L---...iJ'> '

Gniazd można używać również w połączeniu z innymi protokołami nienależącymi do rodziny TCP /IP. Możemy je na przykład wykorzystywać do komunikowania się z lokalnymi urządzeniami za pośrednictwem połączeń IrDA (czyli portu podczerwieni) lub Bluetooth. Istnieją także inne protokoły sieciowe, których możemy używać, jednak to właśnie te, które należą do rodziny TCP /IP, są najczęściej stosowane.

Protokoły I P, 1 Pv6 oraz TCP W internecie wykorzystuje się rodzinę protokołów znanych zwykle pod wspólną nazwą TCP /IP. Protokołem niższego poziomu jest IP, czyli protokół internetowy (ang. Internet Protocol) . Sta­ nowi on środek, dzięki któremu odbywa się cały ruch sieciowy w internecie. Gdy płacimy za połączenie internetowe, kupujemy tak naprawdę możliwość przesyłania informacji ze swojego komputera do internetu i w przeciwnym kierunku właśnie za pośrednictwem IP. Głównym zadaniem protokołu IP jest zapewnianie możliwości przenoszenia pakietów danych (jak w świecie sieci nazywane są pojedyncze komunikaty) pomiędzy różnymi sieciami kom­ puterowymi (stąd właśnie wzięła się nazwa internet) . Na przykład dane wysłane na zewnątrz przez port sieciowy jakiegoś serwera WWW znajdującego się w pewnym centrum kompute­ rowym w jakiś sposób muszą odnaleźć swoją drogę do naszej domowej sieci Wi-Fi. Sieci te są połączone za pośrednictwem routerów, których zadanie polega na określeniu, dokąd należy wysłać pakiety IP w następnej kolejności . Istnieją ściśle zdefiniowane reguły dotyczące tego, jak operacje te mają się odbywać, dzięki czemu dane trafiają w końcu do maszyny, do której miały dotrzeć. Proces ten jest uzależniony od adresu IP numeru identyfikującego maszynę w taki sposób, aby routery mogły określić, jak należy przekierować do niej odpowiednie komunikaty. -

Jeśli zamierzamy korzystać z gniazd, będziemy musieli posługiwać się adresami IP, ponie­ waż to właśnie one umożliwiają identyfikację maszyny, z którą chcemy się skomunikować. Zwykle możemy je po prostu traktować jako nieprzezroczyste identyfikatory opakowane w obiekty klasy I PAd dress należącej do przestrzeni nazw Sy s t em . N e t . Istnieje jednak pewien aspekt adresowania IP, o którym warto wiedzieć: chodzi o rozróżnienie pomiędzy adresami 1Pv4 a adresami 1Pv6 . Więcej informacji na ten temat znajdzie Czytelnik w zamieszczonej poniżej ramce . Podczas gdy protokół internetowy do identyfikowania maszyn wykorzystuje liczby, użytkow­ nicy są bardziej przyzwyczajeni do nazw takich jak helion.pl (http://helion.pl) czy www.microsoft. com (http://www.microsoft.com) . W internecie działa system noszący nazwę usługi nazw dome­ nowych (ang. Domain Name Service, w skrócie DNS), do którego nasz dostawca usług inter­ netowych zapewnia nam dostęp w ramach używanego przez nas połączenia . Zadanie tego systemu polega na przekształcaniu nazw tekstowych na adresy IP wymagane do komunikacji z maszynami (lub hostami, jak zwykle określa się jednostki związane z adresami IP) . W kodzie

526

I

Rozdział 13. Sieci

Ad resy I Pv4 i I Pv6 Istniej ą dwa rodzaje adresów IP, ponieważ obecnie używane są też dwie wersje IP . Najczęściej wykorzystywana jest wersja 4. (Poprzednie numery dotyczyły wersji, które były używane we wczesnych, eksperymentalnych latach istnienia internetu, a co za tym idzie nie mamy szans na zetknięcie się z nimi w czasach obecnych) . Z protokołem IPv4 wiąże się pewien kłopot: adresy są w nim liczbami 32-bitowymi, co oznacza, że da się za jego pomocą zdefiniować unikatowe adresy tylko dla około 4 miliardów komputerów. Może się wydawać, że to dużo, ale okazuje się to liczbą niewystarczającą, jeśli weźmiemy pod uwagę to, jak wiele komputerów i innych urządzeń ma obecnie dostęp do internetu, oraz to, jak szybko przybywa nowych. Już w tej chwili używa się niezbyt zgrabnych sztuczek mających na celu umożliwienie wielu maszynom korzystania z tych samych adresów, a ograniczona przestrzeń adresów IP stanowi naprawdę poważny problem. W przypadku standardu IPv6 adres jest liczbą 128-bitową, dzięki czemu przestrzeń adresów jest wystarczająco wielka, aby wystarczyła w dającej się przewidzieć przyszłości, jednak z protokołem tym również wiąże się pewien kłopot. Starsze komputery i routery nie obsługują IPv6. W przy­ padku komputerów z problemem tym można sobie zwykle poradzić za pomocą odpowiednich aktualizacji oprogramowania - w systemie Windows XP da się zainstalować obsługę protokołu IPv6 (a system Windows Vista i nowsze wersje standardowo mają ją wbudowaną od początku) . Jednak obsługa zapewniana przez systemy operacyjne to nie wszystko - aktualizacji mogą też wymagać aplikacje . Większy problem stanowi to dla routerów. Struktura IPv4 jest sprzętowo „wrośnięta" w wiele z tych urządzeń, dlatego trzeba je zastąpić w celu zapewnienia obsługi standardu IPv6. Powoduje to, że zastosowanie nowej wersji protokołu może się wydawać niezbyt atrakcyjnym wyborem. Czy chcielibyśmy, aby nasz serwer WWW miał adres niedostępny dla wszystkich użytkowników, którzy nie zaktualizowali swoich połączeń sieciowych i internetowych do IPv6? W rzeczywistości nie jest aż tak źle, ponieważ istnieje specjalna klasa adresów IPv6, które faktycz­ nie są odpowiednikami adresów IPv4, dzięki czemu możliwe jest zapewnienie, aby serwer korzy­ stający z protokołu IPv6 był dostępny dla klientów używających IPv4. Oznacza to jednak, że każda publiczna usługa, której możemy chcieć używać, powinna być dostępna za pośrednictwem IPv4, dlatego nie ma zbyt silnych bodźców, które powodowałyby, żeby użytkownicy końcowi czy admi­ nistratorzy sieci korporacyjnych pragnęli wyrzucać swoje doskonałe routery IPv4 i zastępować je urządzeniami obsługującymi IPv6. To zaś powoduje, że firmy telekomunikacyjne nie mają zbyt wielu klientów, którzy wymagaliby dostarczania routerów DSL zdolnych obsłużyć protokół IPv6. W konsekwencji przejście na nowy standard odbywa się niewiarygodnie powoli. Tym niemniej problem dotyczący przestrzeni adresów IPv4 wcale nie zamierza zniknąć i dlatego powinniśmy pisać swoje oprogramowanie w taki sposób, aby było ono w stanie obsługiwać zarówno adresy IPv4, jak i IPv6, jeśli chcemy, aby nadal pracowało ono prawidłowo, gdy standard IPv6 przyjmie się na większą skalę. Platforma .NET stara się uczynić to zadanie stosunkowo łatwym do wykonania. Zapewniana przez nią klasa I PAddress może przechowywać obydwa rodzaje adresów. W przypadku większości apli­ kacji kod strony klienta nie musi nawet wiedzieć, z którym rodzajem adresów ma do czynienia. Czasami jednak jesteśmy zmuszeni do korzystania z adresów IP w ich postaci numerycznej, a wów­ czas rozróżnienie to ma zasadnicze znaczenie.

przedstawionym na listingu 13.22 w celu wyszukania adresów IP odpowiadających określo­ nej nazwie hosta wykorzystywana jest klasa Dns należąca do przestrzeni nazw Sys t em . Net . DNS jest w stanie powiązać wiele adresów z jedną nazwą; system może na przykład przecho­ wywać zarówno adres 1Pv4, jak i 1Pv6. Zaprezentowany poniżej kod zawiera pętlę przechodzącą przez wszystkie te adresy i wyświetlającą na ekranie ich typy oraz wartości . (Gdy na rzecz

Gniazda

I

527

obiektu klasy I PAddress wywołamy metodę ToStri ng, co dzieje się tutaj wewnątrz wywołania metody Con sol e . Wri tel i n e, zwróci ona adres numeryczny w postaci standardowego łańcucha znakowego) .

Listing 13.22 . Pobieranie adresów IP związanych z nazwq hosta I PHos t Entry hostDns Entry = Dns . GetHo s t Entry ( " l ocal host " ) ; foreach ( I PAddress address i n hostDn s En t ry . Addre s s l i st) { Consol e . Wri tel i ne ( " Typ : { O } , Adres : { 1 } " , address . Address Fami l y , addres s) ;

Przedstawiony tu przykładowy kod wyszukuje szczególną nazwę hosta l ocal host odnoszącą się zawsze do maszyny lokalnej, na której działa program. W obydwu protokołach, IPv4 oraz IPv6, zdefiniowano specjalne adresy, które są zarezerwowane właśnie po to, aby umożliwiać odwoływanie się do maszyny lokalnej . Zatem gdy uruchomimy kod zaprezentowany na lis­ tingu 13.22, na ekranie komputera ujrzymy dwa adresy, z których jeden związany jest z proto­ kołem IPv6, drugi zaś z IPv4: Typ : I nterNetworkV 6 , Adres : : : 1 Typ : I nterNetwork, Adres : 127 . 0 . 0 . 1

.

. ·

i.......��

'

Przez wiele lat IPv4 był jedyną używaną wersją protokołu IP, dlatego zwykle nie oznaczało się go za pomocą odpowiedniego numeru wersji. To właśnie dlatego właści­ wość AddressFami l y związana z adresem IPv4 jest wyświetlana po prostu jako I nterNetwork, nie zaś jako I nterNetworkV4.

Wiele wpisów DNS nie zawiera adresów IPv6 (na przykład w chwili pisania tej książki dome­ nie w3.org odpowiada jedynie adres IPv4), a zatem gdy zmodyfikujemy kod przedstawiony na listingu 13.22 w taki sposób, aby wyszukiwał on tego rodzaju adresy, w wyniku wywołania metody Get Hos t E n try zwrócony zostanie tylko jeden adres: Typ : I nterNetwork, Adres : 128 . 30 . 52 . 45

Uzbrojeni w adres IP maszyny, z którą chcemy nawiązać komunikację, dysponujemy już wystar­ czającymi informacjami, aby dało się dostarczyć pakiety IP do maszyny docelowej za pośred­ nictwem internetu. Wciąż musimy jednak poradzić sobie z kilkoma kwestiami . Po pierwsze, pojawia się tu pytanie, skąd maszyna odbierająca pakiet ma wiedzieć, co powinna z nim zrobić, gdy dotrze on już na miejsce. Po drugie, mamy do czynienia z kłopotem polegającym na tym, że internet z samej zasady swojego działania nie jest w stanie zagwarantować nam dostarcze­ nia danych. Rozwiązanie obydwu tych problemów oferuje nam TCP (ang. Transmission Control Protocol protokół kontroli transmisji) . -

Internet nie gwarantuje dostarczenia wszystkich pakietów IP. Po prostu nie może tego zrobić. Wyobraźmy sobie, że korzystamy z maszyny, która komunikuje się z internetem za pośred­ nictwem łącza o przepustowości 100 Mb/s, i z pełną prędkością próbujemy wysyłać dane do maszyny, która łączy się z siecią za pomocą modemu pracującego z maksymalną przepusto­ wością 56 kb/ s . (Czy Czytelnik pamięta jeszcze takie urządzenia? W niektórych częściach świata nadal są one w powszechnym użyciu. Jeśli będziemy mieli ku temu okazję, powinni­ śmy spróbować skorzystać z jakiegoś nowoczesnego serwisu WWW, używając wdzwanianego połączenia o przepustowości 56 kb/s, a następnie uświadomić sobie, że w swoim czasie modemy tego rodzaju były uważane za naprawdę szybki sprzęt) . Gdy wysyłamy dane do takiej maszyny

528

I

Rozdział 13. Sieci

korzystającej z połączenia o bardzo ograniczonej przepustowości, znajdujące się po drodze routery próbują początkowo radzić sobie z występującą między maszynami różnicą prędkości. Router łączący sieć szybką z wolniejszą będzie tymczasowo przechowywał w swojej pamięci pakiety nadchodzące z szybkiej sieci. Będą one ustawiane w kolejkę, w której będą oczekiwały na odbywające się powoli wysyłanie do sieci docelowej . W końcu jednak pamięć ta może się wyczerpać i zacznie wówczas dochodzić do utraty kolejnych pakietów. W godzinach największego ruchu w sieci pakiety mogą być tracone nawet wówczas, gdy obydwie strony połączenia są w stanie pracować z taką samą prędkością - niewykluczone, że droga, jaką musi pokonać transmisja w internecie pomiędzy dwoma komunikującymi się sieciami, zawiera bardzo obciążone odcinki, które po prostu nie są w stanie zapewnić przepu­ stowości umożliwiającej przeniesienie całego ruchu sieciowego generowanego przez wszyst­ kich klientów ISP. Dlatego gubienie pakietów może być też powodowane przez przeciążenie sieci nawet wówczas, gdy nie występuje niedopasowanie prędkości działania obydwu końców połączenia . Wynika z tego, że IP nie jest protokołem pewnym - mamy tu do czynienia z czymś, co bywa czasami określane mianem usługi na możliwie najwyższym poziomie (ang. best effort service) . Inter­ net zrobi, co tylko może, aby spełnić nasze oczekiwania, próbując dostarczyć dane do celu, nie ma jednak żadnej gwarancji, że mu się to uda. (Ze swoim ISP możemy co prawda zwykle podpisać umowę określającą poziom oferowanych usług, w której znajdują się odpowiednie gwarancje dotyczące ilości danych pomyślnie dostarczanych do i od granic obsługiwanej przez tego ISP infrastruktury sieciowej, nie mamy jednak żadnego zapewnienia związanego z dostarczeniem któregokolwiek z pojedynczych pakietów, a nasz ISP nie jest też w stanie zagwa­ rantować, co stanie się z danymi, gdy opuszczą już one jego sieć i zostaną przekazane do sieci zarządzanej przez kogoś innego) . Ż eby było jeszcze zabawniej, IP nie gwarantuje nawet tego, że dostarczy komunikaty w tej samej kolejności, w jakiej zostały one wysłane. Dostawcy usług internetowych mogą korzy­ stać z wielu różnych tras w obrębie swojej sieci, aby zapewnić niezawodność dostarczania danych w obliczu potencjalnych awarii poszczególnych połączeń lub aby zapewnić po prostu przepustowość wystarczającą do szybkiego przesyłania dużych zbiorów danych. Gdy zatem wysyłamy ciąg pakietów IP do tego samego komputera, nie wszystkie one wybiorą tę samą drogę poprzez sieć - pakiety mogą podążać dwiema różnymi drogami, a nawet rozdzielić się, wybierając wiele różnych ścieżek prowadzących do celu. Niektóre z nich mogą okazać się szybsze, co oznacza, że pakiety mogą docierać do swojego celu w innej kolejności, niż ta, w któ­ rej je wysłaliśmy. Tworzenie aplikacji korzystających z sieci może być dużym wyzwaniem, gdy nie mamy pojęcia, czy określony komunikat w ogóle zostanie odebrany, a także nie wiemy, w jakiej kolejności pojawią się te, które dotrą do celu. Życie ma nam uprościć protokół kontroli transmisji, czyli Transmission Control Protocol - a więc to, co w nazwie TCP / IP składa się na skrót TCP. Protokół ten opiera swoje działanie na IP i dodaje do niego kilka użytecznych możliwości . Zapewnia on obsługę połączeń, to znaczy zamiast zarządzać przesyłaniem każdego pakietu z osobna, traktuje każdą transmisję jako część pewnej sekwencji komunikacji odbywającej się w ramach połączenia. TCP umieszcza odpowiednie numery sekwencyjne w każdym pakiecie IP, dzięki czemu da się wykryć sytuacje, w których pakiety docierają do punktu przeznaczenia w niewłaściwej kolejności . Poza tym maszyna odbierająca dane potwierdza odbiór każdego

Gniazda

I

529

komunikatu. Klienty wykorzystują to w celu określenia, jak szybko komunikaty są przeka­ zywane, co pozwala im na wybranie takiej prędkości wysyłania danych, która odpowiada zdolności sieci do ich dostarczania. Unikają w ten sposób problemów związanych z niedostoso­ waniem prędkości działania sieci i jej przeciążeniem. Klienty używają tego mechanizmu również do rozpoznawania sytuacji, w których dane nie dotarły na miejsce i należy je ponownie wysłać. Dzięki tym możliwościom TCP oferuje usługę transmisji danych pozwalającą na przesyłanie ich we właściwej kolejności, z prędkością nieprzekraczającą przepustowości dostępnych tras sieciowych oraz w sposób zapewniający niezawodność w obliczu okazjonalnej utraty pakie­ tów. Gniazdo jest zwykle po prostu API nałożonym na połączenie TCP, które ma charakter strumieniowy - program może zapisywać dane w strumieniu gniazda, a kod odpowiadają­ cy za komunikację TCP /IP działający na komputerach znajdujących się na obydwu końcach korzysta z TCP w celu zapewnienia, że program uruchomiony po stronie odbierającej używa innego strumienia gniazda, z którego jest w stanie odczytywać tę samą sekwencję bajtów, jaką my zapisaliśmy w swoim strumieniu. Programy te nie muszą mieć pojęcia o dostarczaniu pakietów w niewłaściwej kolejności ani o ich utracie. Dopóki nie mamy do czynienia z bez­ nadziejnie stratnymi sieciami, wydaje się, jakbyśmy korzystali z doskonale pewnej transmisji dostarczającej dane w odpowiednim porządku. Gniazda TCP mają charakter symetryczny w tym sensie, że obydwie strony mogą wysyłać i odbierać dane . Kierunki te są też od siebie niezależne - komunikacja może być w pełni dwustronna, dlatego nie zachodzi konieczność przejmowania łącza przez każdą ze stron po kolei. TCP rozwiązuje także problem informowania komputera odbierającego dane o tym, co należy z nimi zrobić. Pojedynczy komputer jest dzięki temu w stanie zapewniać obsługę wielu róż­ nych usług sieciowych - niewielka firma może na przykład wykorzystywać jedną maszynę jako intranetowy serwer WWW, serwer plików i serwer poczty elektronicznej . W tym celu TCP wprowadza koncepcję numerów portów. Usługa działająca na maszynie docelowej będzie związana z określonym numerem . Istnieje centralne ciało administracyjne o nazwie IANA (ang. Internet Assigned Numbers Authority - organizacja zarządzająca przydzielonymi nume­ rami internetowymi), które (poza inną działalnością) zajmuje się przypisywaniem i publiko­ waniem numerów portów związanych z najbardziej popularnymi usługami. IANA wyznaczyła na przykład port 80 jako port TCP, na którym serwery HTTP zwykle przyjmują nadchodzące żądania. Gdy serwer WWW (lub klasa WebCl i ent, o której była mowa wcześniej) pobiera zasób za pośrednictwem HTTP, robi to, otwierając połączenie TCP z portem 80 .

L---LI"'.

:

Pojedynczy komputer klienta może otworzyć kilka równoległych połączeń z tą samą usługą. Przeglądarki internetowe bardzo często korzystają z tej możliwości, aby równocześnie pobierać różne obrazy, arkusze CSS i pliki JavaScript, dzięki czemu są w stanie szybciej wyświetlić otwieraną stronę WWW. Możliwość rozróżnienia tych połączeń zapewnia to, że do każdego z nich przypisany jest zarówno numer portu po stronie klienta, jak i numer portu po stronie serwera. Jednak aby nawiązać połą­ czenie, musimy znać sam numer portu serwera, ponieważ port klienta jest zwykle dynamicznie wybierany dla nas przez system operacyjny.

Przyjrzyjmy się rzeczywistemu przykładowi zastosowania opisanego tu mechanizmu. Zamie­ rzamy połączyć się z usługą, korzystając z bardzo starego i bardzo prostego protokołu o nazwie Daytime Protocol. Nie uległ on zmianie od czasu publikacji jego specyfikacji, która miała miejsce w 1983 roku. Definicję tego protokołu możemy znaleźć w dokumencie pod tytułem RFC867, który jest dostępny pod adresem http://wwwfaqs.org/rfcs/rfc867.html. Protokół ten jest nadzwyczaj

530

I

Rozdział 13. Sieci

prosty: klient otwiera połączenie TCP z portem 13 serwera, który oferuje usługę udostępniania informacji o bieżącym czasie, serwer zaś odsyła tę informację w postaci tekstowej, a następnie zamyka połączenie . Specyfikacja dość ogólnikowo traktuje kwestię formatu - mówi na ten temat tylko tyle: There i s no spec i f i c syntax for the day t i me . It i s recommended that i t be l i mi ted to the ASC I I pri nt i ng characters , space , carri age return , and l i ne feed . The dayt i me s houl d be j us t one l i ne . 0

Następnie prezentowane s ą w niej przykłady kilku najpopularniejszych formatów, jednak w praktyce serwery mają niemal pełną swobodę w ustalaniu postaci udostępnianej przez siebie informacji. Jest to przykład usługi, do której dostęp nie może odbywać się za pomocą klasy WebCl i ent lub którejkolwiek z klas należących do rodziny Web Req uest - typy te spodziewają się otrzymania danych „opakowanych" w HTTP (lub jakiś inny protokół wysokiego poziomu taki jak FTP), zaś Daytime Protocol korzysta ze zwykłego protokołu TCP, jedynie w bardzo prosty i bezpo­ średni sposób. Zatem aby uzyskać dostęp do tego rodzaju usługi, musimy zastosować gniazda. Amerykański rządowy Narodowy Instytut Standaryzacji i Technologii (ang. National Institute of Standards and Technology - NIST) wymienia kilka serwerów oferujących usługę udostęp­ niania informacji o bieżącym czasie. Jedna z takich maszyn znajduje się w Redmond w stanie 1 Waszyngton, a jej nazwa DNS to t i me-nw . n i st . gov 2 . Skorzystamy właśnie z niej . Na początek musimy odnaleźć odpowiedni adres IP; zrobimy to, korzystając z podobnego sposobu jak w kodzie przedstawionym na listingu 13.22: I PHos t Entry hostDns Entry = Dns . GetHostEntry ( " t i me-nw . n i s t . gov " ) ; I PAddres s serverip = hos tDnsEntry . Addre s s l i s t [O] ;

Następnie powinniśmy otworzyć połączenie TCP z portem 13 (czyli tym, za pośrednictwem którego dostępna jest usługa informowania o czasie) tej maszyny. Aby to zrobić, będziemy potrzebowali obiektu klasy Soc ket .

Łączen ie się z usługam i za pomocą klasy Socket W przestrzeni nazw Sys t em . Net . So c k e t s zdefiniowana została klasa Soc ket, która w ramach platformy .NET udostępnia podstawowe możliwości związane z gniazdami oferowane przez system operacyjny. Z klasy tej korzystamy, chcąc otworzyć połączenie TCP ze zdalną usługą: Socket dayt i meSocket = new Socket ( serveri p . Addre s s Fami l y , Soc ketType . Stream , Protocol Type . Tcp) ;

11 Nie istnieje żadna ścisła składnia informacji o bieżącym czasie. Zaleca się używanie w niej wyłącznie druko­

wanych znaków ASCII oraz znaków odstępu, powrotu karetki i nowego wiersza. Informacja o czasie powinna mieścić się w jednym wierszu.

12 Wymieniony tu serwer nie jest obecnie polecany z uwagi na duże obciążenie. W razie problemów z połącze­

niem można skorzystać z innej maszyny zapewniającej usługę informowania o bieżącym czasie. Listę adresów polecanych serwerów tego rodzaju wraz z odpowiednimi rekomendacjami można znaleźć na przykład pod adresem http://tf.nist.gov/tf-cgi/servers.cgi# - przyp. tłum.

Gniazda

I

531

' ..

Klasa Socket implementuje interfejs ! Di sposabl e, dlatego w pewnym momencie będziemy musieli wywołać metodę Di spose. Zwykle poradzilibyśmy sobie z tą koniecznością, pisząc odpowiednią instrukcję us i ng, jednak w przypadku gniazd sytuacja przedstawia się dość nietypowo, ponieważ przeważnie istnieją one dłużej, niż trwa wykonanie którejkolwiek z metod. Nie ma jednego słusznego, ogólnego sposobu radzenia sobie z tą kwestią, ponieważ chwila, w której należy zwolnić gniazdo, będzie zawsze zale­ żeć od tego, jak nasza aplikacja z niego korzysta. W kodach zaprezentowanych na kilku kolejnych listingach Czytelnik nie zobaczy więc operacji zwalniania gniazda, gdyż chcemy tu przedstawić te aspekty API, które nie zmieniają się w zależności od sposobu użycia gniazd. Należy jednak pamiętać, że zawsze będziemy musieli znaleźć właściwe miejsce na umieszczenie wywołania metody Di spose.

Konstruktor obiektów klasy Soc ket wymaga podania trzech informacji. Musi on znać rodzinę adresów, z której korzystamy, identyfikując serwer (na przykład IPv4 lub IPv6) . Powinien także wiedzieć, jakiego stylu komunikacji się spodziewamy - zależy nam na komunikacji typu strumieniowego. (Niektóre protokoły obsługują inne style komunikacji, ale w przypad­ ku TCP zawsze należy tu podać argument Stream) . Wreszcie należy tu wybrać określony protokół, którego chcemy używać - w tym przypadku będzie to TCP. ' .. •

11...,' ..-----11�_

Jeśli konstruktor ten wydaje się Czytelnikowi bardziej skomplikowany, niż jest to konieczne, to jest tak, ponieważ gniazda nie są przeznaczone wyłącznie do współpracy z TCP /IP. Stanowiące podstawę tego mechanizmu API gniazd systemu Windows (WinSock) pojawiło się, zanim TCP /IP zyskał pozycję dominującą, dlatego radzi sobie ono z obsługą wielu różnych protokołów. System Windows wspiera nawet niezależ­ nych dostawców, którzy dodają obsługę zupełnie nowych protokołów.

Zauważmy, że nie określamy tu jeszcze, z czym chcemy się łączyć. Informacja ta nie jest pobie­ rana przez konstruktor, ponieważ nie wszystkie gniazda działają w ten sam sposób - nie­ które protokoły umożliwiają korzystanie z modeli transmisji wykraczających poza proste połą­ czenia punkt-punkt. Z tego powodu klasa Soc ket wymaga, abyśmy określili rodzaj gniazda, z którego chcemy korzystać, zanim podamy, z czym zamierzamy się komunikować. Tę infor­ mację powinniśmy dostarczyć dopiero w momencie nawiązywania połączenia z usługą: dayt i meSocket . Connect (serverl p , 13) ;

Pamiętajmy, że 13 to numer portu przypisany przez organizację IANA usłudze informowa­ nia o bieżącym czasie . Od usługi tej mamy zamiar pobrać aktualną godzinę podaną w formie tekstowej, dlatego powinniśmy zadeklarować zmienną, w której przechowamy wynik tej operacji: s t r i ng data ;

W przypadku gniazd wszystkie dane reprezentowane są jako bajty. (A dokładniej jako oktety będące ośmiobitowymi bajtami. Dawno temu w niektórych komputerach stosowane bowiem były bajty o innych wielkościach i czasami możemy jeszcze natrafić na pozostałości tego stanu rzeczy. Na przykład niektóre części internetowego systemu poczty elektronicznej nie gwa­ rantują przesyłania bajtów ośmiobitowych i mogą przyciąć nasze dane do siedmiu bitów na bajt) . Specyfikacja Daytime Protocol mówi, że usługa zwróci tekst zakodowany zgodnie ze standardem ASCII, dlatego potrzebujemy też jakiegoś sposobu przekonwertowania strumienia bajtów zawierających znaki ASCII na postać łańcucha znakowego platformy .NET. Operację tę można wykonać tak, jak zostało to przedstawione na listingu 13.23.

532

I

Rozdział 13. Sieci

Listing 13.23. Pobieranie danych ASCII z gniazda TCP us i ng (Stream t i meServ i ceStream = new NetworkStream (dayt i meSoc ket , true) ) us i ng (StreamReader t i meServ i ceReader = new StreamReader ( t i meServ i ceStream , Encod i ng . ASCI I ) ) { data = t i meServ i ceReader . ReadToEnd ( ) ;

W kodzie tym przeprowadzanych jest kilka działań. Najpierw konstruujemy obiekt klasy Network "+Stream - klasa ta dziedziczy po klasie Stream i to właśnie dzięki temu platforma .NET umożliwia nam traktowanie połączenia wykorzystującego gniazda w dokładnie taki sam sposób jak każdego innego strumienia . Ogólnie rzecz biorąc, zastosowanie strumieni jest tu opcjo­ nalne, ponieważ klasa Socket oferuje metody, za pomocą których można bezpośrednio odczy­ tywać i zapisywać dane. Jednak w tym przykładzie skorzystanie z rzeczywistego obiektu klasy Stream jest lepszym rozwiązaniem, ponieważ możemy podłączyć go do obiektu klasy Stream "+Reader. Ten ostatni pobiera strumień zawierający tekst i jest w stanie przekonwertować bajty należące do tego strumienia na odpowiednie obiekty klasy stri ng. W kodzie przedstawio­ nym na listingu 13.23 następuje wywołanie metody ReadTo End należącej do klasy StreamReader odczytuje ona wszystkie dane ze strumienia aż do samego końca i zwraca je w postaci poje­ dynczego łańcucha znakowego.

Ziarn istość odczytu gniazda Należy mieć się na baczności, aby korzystając z gniazd TCP, nie popełnić klasycznego błędu osoby początkującej . Programiści często stwierdzają, że gdy zapiszą w gnieździe powiedzmy 20 bajtów, a następnie po stronie odbierającej dane dokonają operacji odczytu, prosząc o większą liczbę bajtów (na przykład 1000), operacja ta zwróci zwykle jedynie 20 bajtów, zamiast oczekiwać na nadejście wszystkich wymaganych danych. Wiele osób błędnie zakłada, że protokół TCP gwarantuje, iż dane zostaną dostarczone w porcjach o takiej samej wielkości, w jakiej zostały wysłane. Jednak w praktyce zdarza się, że gdy klient wysyła 20-bajtową porcję danych, po stronie obierającej w wyniku pierw­ szego odczytu zwróconych może zostać tylko sześć pierwszych bajtów z tej porcji, w wyniku następ­ nego kolejnych 13, a w wyniku ostatniego - pozostały bajt. Bywa nawet jeszcze ciekawiej, ponieważ mechanizm może zdecydować o dołączeniu tego ostatniego bajtu do samego początku kolejnej paczki danych wysłanych przez klienta. Gniazda TCP próbują jedynie dostarczyć wszystkie bajty w kolejności, w której zostały one orygi­ nalnie wysłane. W kodzie nie możemy więc przyjmować żadnych założeń dotyczących ziarnistości nadchodzących danych, które są zwracane przez gniazdo. W TCP nie funkcjonuje żadne pojęcie komunikatu czy też ramki - protokół ten zapewnia tylko przesyłanie liniowej sekwencji bajtów. To nasz kod musi być w stanie radzić sobie z danymi wychodzącymi z gniazda w porcjach o cał­ kowicie przypadkowej wielkości. (Pewnym sposobem ułatwienia sobie tego zadania może być pobieranie danych z gniazda po jednym bajcie, jednak w przypadku komunikacji szerokopasmowej rozwiązanie to z pewnością okaże się niezbyt wydajne. Lepsze wyniki możemy uzyskać, pobierając dane z gniazda w nieco większych porcjach).

Zauważmy, że w pierwszym wierszu kodu przedstawionego na listingu 13.23 konstruktorowi klasy N etworkStream podawany jest drugi argument o wartości t ru e . Informujemy ją w ten sposób, że chcielibyśmy, aby przejęła obiekt klasy Soc k et na własność - gdy zakończymy korzystanie z obiektu klasy N etworkSt ream i wywołamy na jego rzecz metodę D i spose, samo­ czynnie zamknie on dla nas również obiekt klasy Soc ket . Stanie się to w naszym przypadku na końcu bloku instrukcji u s i n g . Działanie to jest bardzo ważne: zawsze powinniśmy zamykać połączenia, gdy tylko zakończymy ich używanie, ponieważ gdybyśmy tego nie zrobili, zupeł­ nie niepotrzebnie blokowalibyśmy zasoby serwera . Gniazda

I

533

Gdy pobierzemy już dane i zamkniemy gniazdo, będziemy mogli wreszcie wyświetlić wynik na ekranie komputera: Consol e . Wri teli ne (data) ;

Na listingu 13.24 przedstawiony został cały kod naszego przykładu.

Listing 13.24. Korzystanie z obiektu Socket w celu pobrania danych z serwera udostępniającego informacje o czasie I PHos t Entry hostDns Entry = Dns . GetHostEntry ( " t i me-nw . n i s t . gov " ) ; I PAddres s serverlp = hos tDnsEntry . Addre s s l i s t [O] ; Socket dayt i meSocket = new Socket ( serveri p . Addre s s Fami l y , Soc ketType . Stream , Protocol Type . Tcp) ; dayt i meSocket . Connect (serverl p , 13) ; s t r i ng data ; us i ng (Stream t i meServ i ceStream = new NetworkStream (dayt i meSoc ket , true) ) us i ng (StreamReader t i meServ i ceReader = new StreamReader ( t i meServ i ceStream) ) { data = t i meServ i ceReader . ReadToEnd ( ) ; Consol e . Wri teli ne (data) ;

Gdy uruchomimy program, na ekranie komputera powinno pojawić się mniej więcej coś takiego: 55892 1 1 - 1 1 -27 12 : 24 : 3 1 OO O O 5 13 . 2 UTC (N I ST) *

Nie ma to co prawda zbyt dużego znaczenia dla sposobu korzystania z gniazd, jednak Czytel­ nik jest z pewnością ciekaw, co właściwie oznaczają poszczególne elementy ciągu zwracanego przez serwer . Pierwsza wartość określa liczbę dni, które upłynęły od północy 1 7 listopada 1858 roku. Geśli Czytelnik chciałby wiedzieć, do czego w ogóle może się przydać ta informacja, powinien poszukać w sieci hasła „Zmodyfikowana data juliańska") . Zestaw kolejnych trzech liczb określa rok, miesiąc i dzień (w naszym przykładzie jest to 27 listopada 2011 roku), następ­ nie zaś podany jest aktualny czas UTC (czyli tzw. czas uniwersalny, czas pierwszej strefy lub jak lubią go określać Brytyjczycy - czas Greenwich) . Wartość OO oznacza, że w miejscu, w któ­ rym znajduje się serwer, w użyciu jest czas zimowy (wartość 50 oznaczałaby czas letni), a dwa kolejne zera wskazują odpowiednio, że w tym miesiącu nie będzie dodawana sekunda kom­ pensująca i że serwer nie spodziewa się obecnie wystąpienia żadnych problemów w swoim działaniu. Następna liczba informuje, że serwer celowo podaje czas zwiększony o 513,2 mili­ sekundy, aby skompensować opóźnienia związane z transmisją danych w internecie. I to już wszystko, co musimy zrobić, aby skorzystać z usługi za pomocą gniazd - wystarczy tylko skonstruować odpowiednio skonfigurowane gniazdo, wywołać metodę Connect, a następ­ nie odczytać dane. Gdy usługa, której używamy, spodziewa się odbierać jakieś dane, możemy również zapisać je w obiekcie klasy N etworkSt ream. Musimy też oczywiście przygotować się na wystąpienie ewentualnych błędów - metoda Con n ect zgłosi wyjątek, gdy nie będzie w stanie połączyć się z usługą. Powinniśmy także być przygotowani na wystąpienie błędów za każdym razem, gdy próbujemy odczytać lub zapisać dane przy użyciu gniazda, bowiem nawet jeśli uda nam się prawidłowo połączyć z usługą, niektóre części sieci mogą w późniejszym czasie ulec jakimś awariom, zrywając tym samym to połączenie. Również tego rodzaju problemy plat­ forma .NET sygnalizuje, zgłaszając odpowiednie wyjątki.

534

I

Rozdział 13. Sieci

Do tej pory zapoznaliśmy się tylko z połową historii. A co będzie, gdy zechcemy napisać pro­ gram, który implementowałby usługę podobną do używanej przez nas przed chwilą? Również temu zadaniu da się sprostać, korzystając z klasy Soc ket, wymaga to jednak nieco więcej pracy.

I mplementowanie usług za pomocą klasy Socket Aby zaimplementować usługę wykorzystującą protokół TCP, musimy zapewmc, ze nasz program będzie gotów odbierać nadchodzące żądania. Gdy komputer odbiera przychodzące żądanie połączenia TCP z portem o określonym numerze i żaden program nie oczekuje w danej chwili połączeń z tym portem, żądanie to po prostu jest odrzucane . Pierwszą rzeczą, o którą musimy zatem zadbać, jest utworzenie gniazda nasłuchującego połączeń przychodzą­ cych. Można to zrobić w sposób przedstawiony na listingu 13.25 .

Listing 13 .25. Nasłuchiwanie przychodzących połqcze1i TCP us i ng (Socket dayt i meli s tener = new Soc ket ( Address Fami l y . I nterNetworkV6 , Soc ketType . Stream , Protocol Type . Tcp) ) dayt i me l i s tene r . SetSocketOpt i on (Soc ketOpt i on level . I Pv 6 , (SocketOpti onName) 27 , O) ; I PEndPo i nt day t i meEndpo i nt = new I P EndPo i nt ( I PAddres s . I Pv 6Any , 13) ; dayt i me l i s tener . B i nd (dayt i meEndpo i nt) ; dayt i me l i s tener . Li s ten (20) ;

Podobnie jak miało to miejsce w przypadku strony klienta, tworzymy tu obiekt klasy Soc ket, również określając odpowiednią rodzinę adresów, typ gniazda oraz używany protokół . (W przedstawionym powyżej przykładzie zależy nam na tym, żeby obiekt klasy Soc ket istniał tak samo długo, jak trwa działanie naszej metody Ma i n, dlatego instrukcja u s i ng zapewnia nam odpowiedni sposób poradzenia sobie z koniecznością zwolnienia gniazda) . Podczas gdy w przy­ padku klienta mogliśmy po prostu użyć dowolnego typu adresu, który otrzymaliśmy w wyniku wywołania metody Dns . GetHostEntry, teraz, gdy piszemy kod serwera, powinniśmy określić, na jakiego rodzaju adresie zamierzamy nasłuchiwać zgłoszeń. W programie przedstawionym na listingu 13.25 wybrana została rodzina I n t erN etwor k V 6, dzięki czemu możemy używać protokołu 1Pv6. Jeśli chcemy obsługiwać jedynie 1Pv4, możemy po prostu podać tu rodzinę I nt erN etwork . W gruncie rzeczy nasz kod radzi sobie z obsługą obydwu typów adresów wywołanie metody SetSoc ketOpt i on występujące po wywołaniu konstruktora powoduje, że gniazdo działa w trybie podwójnym, co oznacza, że jest ono w stanie przyjmować połączenia nawiązywane zarówno za pomocą 1Pv4, jak i 1Pv6 . (Magiczna liczba 27, która pojawia się w tym wywołaniu, odpowiada wartości zdefiniowanej w SDK Windows, które nie ma obecnie swojego ekwiwalentu w postaci elementu enumeracji Soc ketOpt i on N ame. Jest to więc po prostu rodzaj magicznego zaklęcia, które musimy znać, aby umożliwić gniazdu akceptowanie przycho­ dzących połączeń nawiązywanych za pomocą obydwu wersji protokołu IP) . Gniazda działające w podwójnym trybie są obsługiwane jedynie przez środowisko Windows Vista i nowsze wersje systemu Windows. Jeśli zatem chcemy przyjmować przychodzące połączenia zarówno za pomocą IPv4, jak i IPv6 we wcześniejszych wer­ sjach Windows, będziemy musieli utworzyć dwa gniazda i nasłuchiwać połączeń przy użyciu obydwu.

Gniazda

I

535

Następnie należy wywołać metodę B i n d . To właśnie za jej pomocą nasza aplikacja zgłasza przejęcie kontroli nad określonym numerem portu TCP. W tym celu budujemy obiekt klasy I PEndPo i n t, podając numer portu 13 - czyli numer odpowiedzialny za udostępnianie usługi informowania o czasie - a także wskazując adresy lokalnej maszyny, na których chcemy nasłu­ chiwać żądań. Komputery zwykle mają wiele różnych adresów - w gruncie rzeczy maszyna podłączona do sieci przeważnie ma przynajmniej dwa adresy 1Pv4 oraz dwa adresy 1Pv6. Wcześniej mieliśmy już do czynienia ze specjalną nazwą maszyny l ocal host, która odpowiada specjalnym adresom 1Pv4 oraz 1Pv6. Posiada je nawet maszyna zupełnie odłączona od sieci adres 1Pv4 127.0 .0.1 oraz adres 1Pv6 : : 1 zawsze odnoszą się do maszyny lokalnej . Oprócz tego zwykle otrzymuje ona odpowiedni adres 1Pv4 i 1Pv6, gdy łączy się z siecią. Można utworzyć gniazda, które nasłuchują wyłącznie na tych adresach lokalnych. Może nie wydawać się to szczególnie przydatne, ponieważ oznacza, że nie będzie się dało połączyć z takimi gniazdami za pośrednictwem sieci. W rzeczywistości jednak okazuje się to bardzo użyteczne dla programistów. Na swojej maszynie możemy bowiem uruchamiać usługi, które nie są dostępne za pośrednictwem sieci, lecz z których mogą korzystać programy działające lokalnie na tej maszynie. Może to rozproszyć obawy administratorów IT, którym nie podoba się idea uruchamiania serwerów WWW na zwykłych komputerach stacjonarnych, ponieważ (całkiem słusznie) uznają tego rodzaju rozwiązania za dość ryzykowne pod względem bezpie­ czeństwa. Gdy skonfigurujemy usługę w taki sposób, aby nasłuchiwała jedynie na tych adresach lokalnych, nie będzie ona widoczna w sieci, a co za tym idzie zmniejszy się prawdopodobień­ stwo, że będzie stanowić jakieś zagrożenie . Właśnie tak działa testowy serwer WWW, który środowisko Visual Studio jest w stanie zapewnić projektom WWW ASP.NET - wykorzy­ stuje on wyłącznie adres lokalny, a zatem jest dostępny jedynie dla przeglądarek interneto­ wych działających na tej samej maszynie . Zwróćmy jednak uwagę, że rozwiązanie to nie jest szczególnie przydatne poza maszyną programisty. Gniazdo lokalne nie może zostać zabez­ pieczone, dlatego będzie dostępne dla każdego użytkownika, który zaloguje się na tej maszy­ nie . W przypadku stanowiska pracy programisty nie ma z tym żadnego problemu, jednak w przypadku systemów serwerowych może stanowić sytuację ryzykowną. Z tego powodu powinno się unikać stosowania gniazd lokalnych. W kodzie przedstawionym na listingu 13.25 wybrany został specjalny adres I PAddres s . I Pv6Any, co oznacza, że gniazdo to będzie przyjmowało przychodzące połączenia kierowane na wszyst­ kie adresy 1Pv6 komputera . A z racji tego, że skonfigurowaliśmy gniazdo do pracy w trybie podwójnym, będzie ono akceptowało również połączenia przychodzące na wszystkie należące do tego komputera adresy 1Pv4. Jeśli jakiś inny program działający na komputerze korzysta już z portu 13 TCP, wywołanie metody B i n d spowoduje zgłoszenie wyjątku - określony numer portu może być w posiada­ niu tylko jednego procesu pracującego na danej maszynie w danym czasie . Jeśli wywołanie metody B i n d się powiedzie, port będzie należał od tej chwili do nas, dzięki czemu będziemy mogli wywołać metodę Li sten, aby wskazać, że jesteśmy gotowi do obsługi żądań przychodzą­ cych połączeń. Jak możemy się przekonać, spoglądając na ostatni wiersz kodu przedstawionego na listingu 13.25, metoda L i s t en przyjmuje jeden argument. Określa ona maksymalne zaległości (ang. backlog) dla tego gniazda . Dzięki zaległościom dopuszczalna jest sytuacja, w której nowe połączenia nadchodzą szybciej, niż nasz serwer jest je w stanie obsłużyć. Jak już niebawem zobaczymy, aby zaakceptować każde przychodzące połączenie, musimy wykonać kilka działań, a w chwi­ lach dużego obciążenia możemy pozostać mocno w tyle. Jeśli zatem nowe żądanie połączenia 536

I

Rozdział 13. Sieci

pojawi się, zanim uda nam się zaakceptować poprzednie, to powędruje ono do kolejki zale­ głości. Gdy liczba żądań dodanych do zaległości osiągnie wartość, którą przekazaliśmy meto­ dzie L i s t en, system operacyjny zacznie odrzucać wszystkie kolejne żądania i będzie to robił aż do czasu, gdy nasza aplikacja zacznie nadążać z ich obsługą. Nasze gniazdo znajduje się obecnie w stanie nasłuchu, co oznacza, że gdy programy klientów zaczną próbować łączyć się z naszym komputerem za pośrednictwem portu 13, system ope­ racyjny będzie wiedział, że połączenia te są kierowane do naszego programu. Kolejną rzeczą, którą powinien on zrobić, jest zaakceptowanie tych połączeń. Odpowiedzialny za to kod został przedstawiony na listingu 13.25. Działa on w pętli, dzięki czemu może przyjmować żądania połączeń tak długo, jak długo pracuje sam program.

Listing 13.26. Akceptowanie połączeń przychodzących wh i l e (true) { Socket i ncomi ngConnect i on = dayt i me li s tener . Accept () ; us i ng (NetworkStream connect i onStream = new NetworkStream ( i ncomi ngConnect i on , t rue) ) us i ng (StreamWri ter wri ter = new StreamWri ter (connect i onStream) ) { wri ter . Wri t e l i ne (DateT i me . Now) ;

W kodzie tym na rzecz nasłuchującego obiektu klasy Soc ket wywoływana jest metoda Accept . Jeśli w danej chwili nie ma klientów próbujących nawiązać połączenie z usługą, wywołanie to zablokuje działanie programu - sterowanie nie zostanie zwrócone aż do czasu, gdy pojawi się klient. Gdy przynajmniej jeden klient podejmie próbę skorzystania z usługi, metoda odda sterowanie, zwracając przy tym kolejny obiekt klasy Soc ket . API tej klasy zaprojektowano w taki sposób, aby umożliwiało obsługę wielu równoczesnych połączeń z jedną usługą i to właśnie dlatego każde wywołanie metody Accept skutkuje zwróceniem nowego obiektu klasy Soc ket . Prowadzi to do sytuacji, w której nasz serwer dysponuje po jednym obiekcie klasy Soc ket dla każdego z połączonych klientów oraz jednym nasłuchującym obiektem tego typu. ••

• .·

.._„�;

.

......___� ·

W rzeczywistości za pomocą nasłuchującego gniazda nigdy nie wysyła ani nie odbiera się żadnych danych. Nie reprezentuje ono połączenia TCP - jego jedyne zadanie polega na zwracaniu nowego gniazda dla każdego z przychodzących połączeń, które zaakceptujemy. Wykorzystywanie obiektów tej samej klasy Socket w tych dwóch całkowicie różnych celach jest trochę dziwne, ponieważ przyjmowanie przychodzących połączeń wydaje się zadaniem zupełnie innego rodzaju niż reprezentowanie aktyw­ nych połączeń TCP. Jednak to właśnie w taki sposób gniazda działały przez dziesię­ ciolecia. Platforma .NET jedynie kontynuuje tę nieco ekscentryczną tradycję.

W kodzie przedstawionym na listingu 13.26 zdecydowaliśmy się zajmować jednym klientem naraz - w pętli akceptowane jest pojedyncze połączenie, następuje odesłanie odpowiedzi, zamknięcie połączenia, a potem przejście do kolejnego klienta . Wynika stąd, że nasz serwer w danej chwili może korzystać z najwyżej dwóch aktywnych obiektów klasy Socket: jednym z nich będzie ten, który jest odpowiedzialny za obsługę bieżącego klienta, a drugim ten, który nasłuchuje przychodzących połączeń. Nie musimy korzystać z takiego rozwiązania - bardzo czę­ sto akceptuje się nowe połączenia za pomocą nasłuchującego gniazda, gdy istnieją już inne otwarte połączenia, które z niego pochodzą. (Na przykład serwer WWW wcale nie nalega na zakoń­ czenie przetwarzania dowolnego obsługiwanego w danej chwili żądania przed rozpoczęciem

Gniazda

I

537

pracy nad kolejnym. Bardzo typową sytuacją jest dla niego posiadanie setek otwartych połą­ czeń przychodzących jednocześnie) . Jednak z uwagi na fakt, że konkretnie ta usługa jest w stanie poradzić sobie z całą niezbędną pracą, a następnie natychmiast zamknąć połączenie, nie ma szczególnych powodów, dla których miałaby otwierać wiele różnych połączeń naraz. Kod odpowiedzialny za to zadanie w tym przypadku bardzo przypomina kod klienta, który został przedstawiony na listingu 13.24. Podobnie jak miało to miejsce wcześniej, tworzymy tu obiekt klasy N etworkSt ream, przekazując konstruktorowi wartość t rue, co ma wskazywać, że chcemy zamknąć gniazdo w momencie zwolnienia strumienia. Tym razem tworzymy jednak obiekt klasy StreamWri t er zamiast StreamReader, ponieważ teraz implementujemy serwer, ten zaś będzie wysyłał dane, zamiast je odbierać. Wywołujemy metodę Wri tel i ne strumienia, prze­ kazując jej bieżąca datę i czas, a więc to, na czym - jak z pewnością Czytelnik pamięta miało przede wszystkim polegać całe zadanie naszej usługi . Ukończony kod programu został zaprezentowany na listingu 13.27.

Listing 13.27. Kompletna usługa informująca o bieżącym czasie us i ng (Socket dayt i meli s tener = new Soc ket ( Address Fami l y . I nterNetworkV6 , Soc ketType . Stream , Protocol Type . Tcp) ) dayt i me l i s tene r . SetSocketOpt i on (Soc ketOpt i on level . I Pv 6 , (SocketOpti onName) 27 , O) ; I PEndPo i nt day t i meEndpo i nt = new I P EndPo i nt ( I PAddres s . I Pv 6Any , 13) ; dayt i me l i s tener . B i nd (dayt i meEndpo i nt) ; dayt i me l i s tener . Li s ten (20) ; wh i l e (true) { Socket i ncomi ngConnect i on = dayt i me l i stener . Accept () ; us i ng (NetworkStream connect i onStream = new NetworkS t ream ( i ncomi ngConnect i on , true) ) us i ng (StreamWri ter wri ter = new StreamWri ter (connect i onStream , Encod i ng . AS C I I ) ) { wri ter . Wri teli ne (DateT i me . Now) ;

Za pierwszym razem, gdy uruchomimy ten kod, możemy spodziewać się wyświetlenia przed­ stawionego na rysunku 13.9 okna dialogowego z ostrzeżeniem (chyba że wcześniej wyłączymy działanie zapory systemu Windows) . Standardowo program Zapora systemu Windows powia­ damia nas, gdy aplikacje ni stąd, ni zowąd zaczynają nasłuchiwać przychodzących połączeń sieciowych. Program, który ma uzasadnioną potrzebę przyjmowania połączeń, zwykle reje­ struje się w zaporze sieciowej w momencie swojej instalacji, więc to, że zupełnie nieznana jej aplikacja nagle zaczyna nasłuchiwać przychodzących połączeń, może być oznaką jakichś problemów. Dokładnie tego typu działania podejmuje bowiem zwykle szkodliwe oprogra­ mowanie, gdy chce udostępnić naszą maszynę hakerom w celu umożliwienia im rozsyłania za jej pomocą spamu lub uruchamiania ataków wykorzystujących rozproszoną odmowę usługi, czyli DDoS. W tym przypadku wiemy oczywiście, że kod ten powinien mieć prawo do odbierania połączeń, ponieważ dopiero co go napisaliśmy. Powodem, dla którego nasz program nie przeszedł oficjalnej drogi rejestrowania się podczas instalacji, jest właśnie to, że został opracowany dosłownie przed chwilą i nie mieliśmy jeszcze czasu zająć się tworzeniem specjalnego pliku MSI instalatora Windows. Jako programiści powinniśmy się więc spodziewać

538

I

Rozdział 13. Sieci

fi

A l ert zabez p ieczeń systemu W i n d o�

� \I

a o

Z p ra

systemu Windows zabl okowała niektóre funkcj e tego

p rog ra m u

Zapora systemu Windolflls zablokolfll a ła niektóre funkcje programu Socketserver 111•e wszystkich sieciach pu b l icznych i prywatnych . ['!azwa:

�yda1111 ca :

Ści�żka :

M*'§$@$1 Microsoft

C : '!/.!lsers'łukasz\doruments\! nowa ksiazka\kody oryginalne \chapter 1 3\socketserver \J:lin\debug\sock.etserver . exe

Zezwól programowi Socketserver na połączenia w tych .s ie ciach :

� Sieci pcywatne, takie jak sieci domo1111e lub firmowe

[Cl Sieci p!!_bliczrne, takie jak 1111 portarn lotniczych i kawiarniach (niezal ecarne,

ponie111• aż takie .sieci na ogół mają .słabe zabezpieczenia lub rnie mają ich wcale) .

Jakie rvzyko wiaże sie z zezwoleniem proąramowi na dostepprzez zaporę?

I

i& Zezwalaj ng_ dostęp

I I

Anuluj

Rysunek 13.9. Ostrzeżenie zapory związane z nasłuchiwaniem połączeń widoku tego rodzaju ostrzeżeń przy uruchamianiu swoich programów, które mają nasłuchi­ wać przychodzących połączeń. (Nie pojawiały się one w przypadku wcześniejszego przykładu zastosowania WCF, ponieważ używana w nim była zarezerwowana specjalnie na tę okazję przestrzeń adresów czasu projektowania, którą tworzy środowisko Visual Studio, gdy insta­ lujemy je na swoim komputerze. Mechanizm ten działa jednak wyłącznie w przypadku pro­ tokołu HTTP - niestety nie ma jego odpowiednika przeznaczonego dla gniazd) . Musimy tu jedynie kliknąć przycisk Zezwalaj na dostęp, a ostrzeżenie to przestanie być wyświetlane dla bieżącego programu. Aby przetestować działanie tego programu, możemy skorzystać z programu klienta, który napisaliśmy wcześniej . Najprościej będzie uruchomić dwie kopie środowiska Visual Studio: jedną dla klienta i jedną dla serwera. (Moglibyśmy też skonfigurować środowisko w taki spo­ sób, aby uruchamiało obydwa projekty, tak jak robiliśmy to wcześniej) . Najpierw powinniśmy uruchomić serwer. Następnie przejdźmy do kodu klienta, zmodyfikujmy wiersz, w którym określana jest nazwa maszyny - należy zastąpić występującą tam nazwę t i me - n w . n i st . gov nazwą l ocal host - a potem uruchommy ten program. Powinien on wyświetlić na ekranie bieżący czas i datę. Format tych danych będzie inny niż ten, którego używał serwer NIST będzie miał on standardową postać wykorzystywaną przez typ DateT i me . Nie ma w tym oczywiście nic dziwnego, ponieważ specyfikacja Daytime Protocol stanowi, że możemy korzy­ stać z dowolnego formatu, jaki nam odpowiada, jeśli tylko jest on kodowany zgodnie ze stan­ dardem ASCII i dane mieszczą się w jednym wierszu. I to już właściwe wszystkie podstawowe informacje na temat używania gniazd. Gniazda oferują również asynchroniczne wersje wszystkich niezbędnych metod - w rzeczywistości obsłu­ gują one zarówno styl wykorzystujący zdarzenia, jak i asynchroniczny styl stosujący metody, o którym była mowa wcześniej . Z uwagi na to, że Czytelnik widział już, jak działa tego rodzaju kod, nie będziemy go tu prezentować ponownie, jednak do kwestii programowania asynchro­ nicznego wrócimy jeszcze w dalszej części tej książki .

Gniazda

I

539

I n ne możl iwości związane z siecią W rozdziale tym poruszyliśmy zagadnienia związane z najczęściej używanymi rodzajami komunikacji sieciowej, jednak dla porządku powinniśmy jeszcze wspomnieć o tym, że dostępne są również inne, bardziej wyspecjalizowane API sieciowe . Przestrzeń nazw Sys t em . Net . Ma i l oferuje na przykład odpowiednie typy umożliwiające wysyłanie wiadomości poczty elektronicz­ nej za pośrednictwem mechanizmu SMTP, a spokrewniona z nią przestrzeń nazw System . Net . M i me obsługuje funkcje MIME zapewniającego standardowy sposób reprezentowania załączników do e-maili . Przestrzeń nazw Sys t em . N et . PeerTo Peer umożliwia dostęp do funkcji związanych z sieciami równorzędnymi (typu peer-to-peer) oferowanymi przez system Windows. (Istnieją również odpowiednie wiązania WCF obsługujące ten mechanizm) . Przestrzeń nazw Sys t em . "+Net . Network I n format i on zapewnia typy pozwalające na sprawdzanie stanu sieci przy użyciu informacji na temat interfejsu sieciowego, a także mechanizmów ICMP TCP /IP takich jak ping. Infrastruktura TLS/SSL umożliwiająca protokołowi HTTPS przesyłanie zaszyfrowanych danych jest również dostępna bezpośrednio poprzez przestrzeń nazw Sys t em . Net . Securi ty.

Podsu mowan ie W rozdziale tym przyjrzeliśmy się trzem różnym sposobom korzystania z komunikacji siecio­ wej . Platforma WCF działa na dość wysokim poziomie, umożliwiając nam pisanie serwerów oferujących operacje, które mogą być wywoływane przez programy klienckie, przy czym te zdalne wywołania mają postać wywołań metod. Przyjrzeliśmy się tu także obsłudze operacji HTTP zapewnianej przez klasy WebCl i ent, HttpWeb Req uest oraz Htt pWebRe s ponse. Poznaliśmy wreszcie sposób posługiwania się mechanizmami bardzo niskiego poziomu, korzystając bez­ pośrednio z możliwości przesyłania bajtów za pośrednictwem sieci i protokołu TCP zapewnianej przez klasę Soc ket . Istnieje jeszcze jedna szczególnie popularna forma komunikacji, o której do tej pory nie wspominaliśmy: wiele aplikacji musi porozumiewać się z bazą danych. Tą tema­ tyką zajmiemy się już w następnym rozdziale.

540

I

Rozdział 13. Sieci

ROZDZIAŁ 14.

Bazy danych

Bazy danych stanowią jeden z najważniejszych wynalazków w dziedzinie informatyki. Umożli­ wiają one aplikacjom przechowywanie ogromnych ilości danych, oferując przy tym możliwo­ ści przeszukiwania milionów elementów i wybieranie z nich jedynie tych, które są nam nie­ zbędne, w czasie mierzonym w ułamkach sekund. Wysokiej jakości baza danych może być skalowana do ogromnych rozmiarów i zapewniać równoczesną obsługę bardzo wielu użytkow­ ników końcowych, gwarantując jednocześnie bardzo niezawodne przechowywanie danych nawet w obliczu ewentualnych awarii systemu. I nawet jeśli nie jest nam potrzebna ta skalo­ walność, bazy danych nadal wydają się interesującym rozwiązaniem, gdy nasz program musi zapamiętywać dane na pewien okres czasu - aplikacje przechowujące wartościowe informacje zwykle wykorzystują bazy danych. Platforma .NET zapewnia kilka różnych sposobów komunikacji z bazami danych. W rozdziale tym przyjrzymy się głównie najnowszemu mechanizmowi dostępu do danych, czyli Entity Framework, a także temu, jak współdziała on z możliwościami LINQ oferowanymi przez język C#. Najpierw jednak dokonamy szybkiego przeglądu wszystkich możliwości bazodanowych zapewnianych przez platformę .NET, aby umieścić Entity Framework w odpowiednim kon­ tekście.

Krajobraz możl iwości dostępu do danych w ramach platformy . N ET Mechanizm Entity Framework, będący głównym zagadnieniem omawianym w tym rozdziale, został po raz pierwszy wydany jako część składowa dodatku Service Pack 1 dla środowiska Visual Studio 2008, który pojawił się na rynku po mniej niż roku od wprowadzenia na rynek początkowej (poprzedzającej wydanie pakietu serwisowego) wersji środowiska Visual Studio 2008. Miało to niebagatelne znaczenie, ponieważ ta pierwsza edycja wprowadziła już zupeł­ nie nowe rozwiązanie dostępu do danych - LINQ to SQL. W latach późniejszych firma Microsoft opracowała jednak mnóstwo nowych technologii dostępu do danych. Choć tempo wprowadzania tych zmian może się chwilami wydawać zaskakujące i dość zniechę­ cające, każde z kolejnych rozwiązań wnosi naprawdę przydatne rozszerzenia, a mimo nowych API, usługi dostępu do danych, które pojawiły się w platformie .NET 1 .0, nadal spełniają powierzone im zadania i do dziś sprawdzają się doskonale. Nie mamy tu więc do czynienia

541

ze stanem ciągłej rewolucji - nowe możliwości po prostu dodają kolejne warstwy funkcjonal­ ności. Wynika z tego, że odpowiednia wiedza na temat wszystkich tych mechanizmów przydaje się, gdy zachodzi konieczność wyboru najlepszego rozwiązania dla danej aplikacji, dlatego przyjrzymy się zastosowaniu ich wszystkich oraz temu, jak poszczególne elementy wykorzystują w swoim działaniu inne.

Klasyczny mechanizm ADO.NET Platforma .NET w wersji 1 . oferowała zestaw usług dostępu do danych o nazwie ADO.NET1 . Wydaje się, że w ostatnich latach ADO.NET wyrosło na zjawisko o nieco bardziej ogólnym charakterze - wraz z dodawaniem nowych możliwości dostępu do danych większość z nich (choć nie wszystkie) zaczęła się pojawiać w części dokumentacji poświęconej właśnie temu mechanizmowi. Aby dobrze zrozumieć znaczenie poszczególnych warstw, warto zacząć od poznania dwóch części, które dostępne były już w pierwszej wersji standardu: interfejsów umoż­ liwiających odpytywanie i aktualizowanie baz danych oraz klas zapewniających możliwość korzystania z danych bez połączenia z bazą.

I nterfejs I DataReader i spółka W ADO.NET zdefiniowana jest rodzina interfejsów zapewniających jednolity sposób prze­ prowadzania podstawowych operacji takich jak wykonywanie zapytań, wstawianie nowych wierszy do tabel bazy danych oraz aktualizowanie lub usuwanie wierszy istniejących. Nie­ które funkcje dostępu do danych są wspólne dla wielu różnych systemów programistycz­ nych - jeśli znamy technologię ODBC lub związany z językiem Java mechanizm JDBC, możemy traktować te interfejsy ADO.NET jako zapewniane przez platformę .NET odpowiedniki tych API. Interfejsy te zapewniają najbardziej bezpośredni i najwydajniejszy sposób uzyskiwania dostępu do podstawowych usług oferowanych przez relacyjne bazy danych, co jest powodem tego, że inne możliwości dostępu do danych, którymi będziemy zajmować się w niniejszym rozdziale, nie zastępują tej części ADO.NET. Te ostatnie są zbudowane na tych niskopoziomowych funk­ cjach i zapewniają usługi wyższego poziomu. Z uwagi na fakt, że nie na nim zamierzamy się tu skupiać, nie będziemy się szczególnie zagłę­ biać w sposób działania tego fragmentu ADO.NET i przedstawimy jedynie jego pobieżny opis. W tabeli 14.1 zebrane zostały najważniejsze klasy bazowe ADO.NET reprezentujące różne elementy, które są potrzebne do korzystania z bazy danych.

Na listingu 14.1 przedstawiony został typowy schemat komunikacji. Rozpoczyna się ona od utworzenia obiektu połączenia. W tym przypadku będzie to obiekt klasy Sql Con nect i on, ponie­ waż nasz kod łączy się z oprogramowaniem SQL Server, jednak w przypadku innych rodzajów baz danych należałoby użyć innych typów dziedziczących po klasie DbConnect i on takich jak Oracl eConnect i on . Następnie budowany jest obiekt polecenia, a jego właściwości CommandText przy1 Nazwa ta jest nieco myląca. ADO.NET jest w pewnym sensie następcą mechanizmu ADO (ang. ActiveX Data

Objects - obiekty danych ActiveXt czyli systemu dostępu do danych funkcjonującego przed nastaniem ery plat­ formy .NET. A zatem ADO.NET jest odpowiedzialny za te same operacje w ramach .NET za jakie odpowiedzialny był ADO w przypadku środowiska Visual Basic 6. Są to jednak dość odmienne technologie - mechanizm ADO.NET nie wykorzystuje w swoim działaniu rozwiązania ADO ani technologii ActiveX. ADO.NET może używać OLE DB, czyli interfejsu umożliwiającego pracę ADO, jednak preferowane są tu własne mechanizmy ADO.NET - rozwiązanie OLE DB jest wykorzystywane głównie w przypadku źródeł danych starszych typów.

542

I

Rozdział 14. Bazy danych

Tabela 14. 1 . Abstrakcyjne klasy bazowe ADO.NET umożliwiające podstawowy dostęp do danych Klasa

Reprezentowany obiekt

DbConnecti on

Połączenie z bazą danych

DbComnand

Polecenie, które ma zostać wykonane przez bazę danych

DbParameter

Parametr polecenia

DbDataRecord

Pojedynczy wiersz danych zwróconych przez zapytanie; tę samą koncepcję reprezentuje też interfejs

DbDataReader

lterator u możliwiający przechodzenie przez pełen zbiór wyników zwróconych przez zapytanie ( potencjalnie jest to wiele wierszy i wiele zbiorów wierszy ) ; implementuje

DbTransacti on

I DataRecord

I DataRecord

Tran sakcja bazy danych

pisywane jest zapytanie, które chcemy wykonać. W tym przykładzie zastosowane zostało pole­ cenie sparametryzowane - jego zadaniem jest wybranie adresów w podanym stanie USA, dlatego dostarczamy mu obiekt parametru określający stan. Następnie wykonujemy to pole­ cenie, wywołując metodę ExecuteReader, używamy zwróconego przez nią obiektu czytnika do przejścia przez wiersze otrzymane w wyniku przetworzenia zapytania i wyświetlamy uzy­ skane w ten sposób wartości . (W przykładzie tym przyjęte zostało założenie, że dysponujemy instancją SQL Servera o nazwie . \SQLEXPRESS . Jeśli zainstalowaliśmy pełną edycję rozwiązania SQL Server lub jego edycję programistyczną, podajemy tu jedynie . zamiast nazwy . \SQLEX PRESS . Więcej informacji na temat instalacji używanych tu przykładów znajdzie Czytelnik w podpunk­ cie „Pobieranie i uruchamianie bazy danych przy użyciu systemu SQL Server 2008 Express" umieszczonym w dalszej części tego rozdziału) .

Listing 14. 1 . Podstawowy dostęp do danych przy użyciu mechanizmu ADO.NET s t r i ng s q l Connecti onStri ng = @ " Data Source= . \sql expres s ; " + " I n i t i al Catal og=AdventureWorks LT2008 ; I ntegrated Securi ty=True " ; s t r i ng s tate = " Ca l i forn i a " ; us i ng (DbConnect i on conn new Sql Connect i on (s q l Connect i onStri ng) ) us i ng (DbCommand cmd = conn . CreateCommand () ) { cmd . CommandText = " S E LECT Addres s l i nel , Addres s l i ne2 , C i ty FROM Sal es LT . Address WHERE " + " StateProv i nce=@state " ; DbParameter s t ateParam = cmd . CreateParamete r ( ) ; s tateParam . ParameterName = " @state " ; s tateParam . Val ue = state; cmd . Parameters . Add ( s tateParam) ; =

con n . Open () ; us i ng (DbDataReader reader = cmd . ExecuteReader () ) { whi l e (reader . Read () ) { s t r i ng addres s l i ne l = reade r . GetStri ng (O) ; li W przypadku pola AddressLine2 dopuszcza się występowanie wartości pustej, dlatego należy się li przygotować zarówno na otrzymanie ła11cucha znakowego, jak i wartości DBNull. s t r i ng addres s li ne2 = reader . GetVal ue ( l ) as stri ng ; s t r i ng c i ty = reader . GetStr i ng (2) ; Consol e . Wri teli ne (addres s li ne l ) ; Consol e . Wri teli ne (addres s li ne2) ; Consol e . Wri teli ne (c i ty) ;

Krajobraz możliwości dostępu do danych w ramach platformy .NET

I

543

Być może Czytelnik zastanawia się teraz, dlaczego bawimy się tu z obiektem parametru, skoro o wiele prościej byłoby zwyczajnie umieścić nazwę stanu bezpośrednio w łańcuchu znako­ wym definiującym zapytanie SQL. W tym przykładzie rozwiązanie to sprawdziłoby się akurat doskonale, ponieważ nazwa stanu nie ulega w nim zmianie i mogłaby zostać na stałe zakodo­ wana w programie, jednak przedstawiona na listingu technika może mieć zastosowanie również w takich przypadkach, w których wartość pobierana jest w czasie działania aplikacji . Gene­ ralnie budowanie zapytań SQL przy użyciu łączenia łańcuchów znakowych jest dość niebez­ pieczną praktyką - jeśli jakikolwiek tekst pochodzi spoza kodu (a więc jest na przykład pobie­ rany z formularza dostępnego za pośrednictwem strony internetowej lub stanowi element łańcucha URL), nasz program stanie się podatny na atak określany mianem zastrzyku SQL (ang. SQL injection) . Wyobraź sobie, że kod przedstawiony na listingu 14.1 stanowi część apli­ kacji WWW, a używana w nim nazwa stanu pochodzi z fragmentu łańcucha URL mającego na przykład postać: http://example.com/showinfo ?state=California. Użytkownicy mogą dowolnie modyfikować łańcuchy URL - można je po prostu podawać w polu znajdującym się na pasku adresu - a zatem jakaś podstępna osoba mogłaby zdecydować się na odpowiednią zmianę tej części łańcucha . Gdyby kod po prostu pobierał wartość z łańcucha URL i łączył ją bezpo­ średnio z pozostałymi częściami zapytania SQL, dawałoby to w gruncie rzeczy każdej osobie z dostępem do internetu możliwość uruchamiania dowolnych poleceń SQL w obrębie naszej bazy danych. Zapytania SQL mogą zawierać wiele różnych poleceń, dlatego użytkownicy byliby w stanie wprowadzać dodatkowe komendy, które byłyby wykonywane po poleceniu S E L E C T . Zastosowanie parametrów to jeden ze sposobów uniknięcia tego rodzaju zagrożeń, ponieważ wartość parametru nie będzie w takich sytuacjach traktowana jako kod SQL. Wynika z tego, że bardzo dobrym pomysłem jest wyrobienie sobie zdrowego nawyku korzystania z parametrów wszędzie tam, gdzie jakaś część zapytania musi ulegać zmianie w czasie wyko­ nania programu. API, z którego tu skorzystaliśmy, bezpośrednio odzwierciedla kroki niezbędne do komunikacji z bazą danych, dlatego musimy napisać mnóstwo kodu, aby połączyć w jedną całość zapytania, parametry i kolumny tabel należące do świata baz danych ze światem języka C#. Za przy­ kład wyprzedzający nieco swój czas niech posłuży nam kod zaprezentowany na listingu 14.2, w którym odpowiednie operacje przeprowadzane są przy użyciu mechanizmu Entity Frame­ work. Zwróćmy uwagę, że zamiast budować obiekt parametru przeznaczony dla sparametry­ zowanego zapytania, możemy tu po prostu skorzystać z klauzuli where języka LINQ oraz składni operatora porównania języka C#. (Entity Framework wykonuje właściwe sparametryzo­ wane zapytanie pod maską, więc rozwiązanie to zapewnia ochronę przed atakami typu SQL injection) . Zauważmy także, że wszystkie kolumny tabel bazy danych są w tym przypadku dostępne w postaci właściwości obiektu, dzięki czemu nie musimy wywoływać metody GetStri ng czy też podobnych metod pomocniczych, aby uzyskać odpowiednie wartości znajdujące się w tych kolumnach) . = =

Listing 14.2 . Rozwiązanie wykorzystujące technikę LINQ to Entities zamiast mechanizmu ADO.NET s t r i ng s tate = " Ca l i forn i a " ; us i ng (var context = new AdventureWorks LT2008En t i t i es () ) { var addresses from address i n context . Addresses where addres s . StateProv i nce == state sel ect addres s ; =

foreach (var addres s i n addresses) {

544

I

Rozdział 14. Bazy danych

Consol e . Wr i teli ne (addres s . Addres s li nel) ; Consol e . Wr i teli ne (addres s . Addres s li ne2) ; Consol e . Wr i teli ne (addres s . C i ty) ;

Program przedstawiony na listingu 14.1 kosztem większej złożoności oferuje nam jedną oczywistą korzyść: mamy w nim pełną kontrolę nad zapytaniem SQL. Na listingu 14.2 nie widzimy samego kodu SQL, ponieważ jest on generowany niejako w tle. Ogólnie rzecz biorąc, API ADO.NET niskiego poziomu daje nam bardziej bezpośredni dostęp do elementów bazy danych - na przykład korzystając z rozwiązania SQL Server, możemy tak zorganizować pro­ gram, aby otrzymać informację, gdy wykonane przez nas wcześniej zapytanie zwróci tym razem inne wyniki z powodu zmian, które w międzyczasie zaszły w bazie danych. (Może się to oka­ zać przydatne w systemach buforowania. Technikę tę może wykorzystywać pamięć podręczna ASP.NET, jednak musi ona być używana z należytą ostrożnością, ponieważ wymaga od nas zapewnienia, że połączenie z bazą danych było otwarte przez cały czas, co może powodować poważne problemy w kwestii skalowalności) . Inną potencjalną zaletą rozwiązania zaprezentowanego na listingu 14.1 jest to, że nie wymaga ono, aby aplikacja dostosowywała się do sposobu działania mechanizmu Entity Framework. Nie we wszystkich programach metoda korzystania z bazy danych stosowana przez ten mecha­ nizm musi się bowiem okazać najlepsza . Zastosowanie tej części ADO.NET starego typu zwykle sprowadza się do przypadków, w któ­ rych wymagana jest kontrola nad pewnymi specyficznymi aspektami dostępu do danych. Cza­ sami może też być podyktowane faktem, że oferuje ona korzyści związane z większą wydajnością działania w określonych, dość szczególnych sytuacjach. Jednak dla większości programistów ten styl dostępu do danych będzie niepotrzebnie niskopoziomowy i nadmiernie rozwlekły. Interfejsy te nie stanowią jedynej części składowej pierwszej wersji ADO.NET. Rozwiązanie to oferuje jeszcze inne elementy, których zadanie polega na zarządzaniu danymi po zakończe­ niu przetwarzania zapytania, w wyniku którego zostały one pobrane .

Zbiory danych ADO.NET W ramach ADO.NET zdefiniowana jest klasa DataSet stanowiąca kolekcję obiektów klasy Data "+Tab l e. Ta ostatnia pełni rolę funkcjonującej w pamięci kopii pewnych danych tabelarycz­ nych. Zwykle są one pobierane z tabeli lub widoku bazy danych, choć da się również zbu­ dować obiekt klasy DataTabl e na podstawie dowolnego źródła danych - klasa ta zapewnia bowiem metody tworzenia nowych wierszy zupełnie od podstaw. Klasa DataSet może zapewniać wygodny sposób ładowania niewielkiego podzbioru treści umieszczonych w bazie danych do obiektów obecnych w kodzie pracującym po stronie klienta, umożliwiając w ten sposób lokalne przeglądanie informacji przy użyciu wiązania danych. Umożliwia również wykonywanie pewnych podstawowych operacji związanych z odbywa­ jącym się po stronie klienta przetwarzaniem, które zwykle przeprowadza się w obrębie samej bazy danych - da się w ten sposób na przykład wyszukiwać, filtrować i sortować dane. W przy­ padku aplikacji Windows GUI przerzucenie tego rodzaju zadań na stronę klienta może zwiększyć zdolność programu do reagowania na działania użytkownika, który nie musi dzięki temu czekać na odpowiedź bazy danych, aby móc zobaczyć pierwsze wyniki. Właśnie to rozumie się pod pojęciem operacji wykonywanej bez połączenia (ang. disconnected operation) - chodzi tu o możliwość korzystania z danych nawet po tym, jak połączenie z bazą danych zostanie zamknięte . Krajobraz możliwości dostępu do danych w ramach platformy .NET

I

545

Obiekty klasy DataSet poddają się serializacji, dzięki czemu da się je zapisywać na dysku kom­ putera oraz przesyłać za pośrednictwem sieci . Wykorzystywana jest tu reprezentacja XML, co teoretycznie umożliwia zapewnienie dostępu do danych zapisanych w obiektach klasy DataSet kodowi niewykorzystującemu platformy .NET. Jednak choć jest to z całą pewnością wyko­ nalne, w praktyce nie wydaje się być szczególnie popularnym rozwiązaniem. Powodem tego stanu rzeczy może być fakt, że reprezentacja XML jest stosunkowo złożona i unikatowa dla klasy Dat aSet, dlatego nie cieszy się dużym wsparciem poza platformą .NET. Ś rodowisko Visual Studio jest w stanie generować klasy pochodne, aby budować tak zwane silnie typowane (ang. strongly typed) klasy DataSet, których tabele oferują obiekty wierszy z wła­ ściwościami .NET reprezentującymi kolumny odpowiedniej tabeli bazy danych. Silnie typo­ wane klasy DataSet są często używane do redukowania ilości kodu niezbędnego do zapewnienia należytego połączenia pomiędzy kodem C# i bazą danych. Jednak od czasu wprowadzenia rozwiązań LINQ to SQL oraz LINQ to Entities to zastosowanie klas DataSet stało się mniej popularne, ponieważ sposoby wykorzystujące technologię LINQ oferują te same korzyści, a są zwykle prostsze w użyciu. Z tego powodu klasy Dat aSet są obecnie raczej nielubiane. Interfejsy dostępu do danych niskiego poziomu ADO.NET zapewniały najważniejszy sposób dostępu do danych w ramach platformy .NET, zanim na rynku pojawiły się platforma .NET 3.5 oraz środowisko Visual Studio 2008, a wraz z nimi technologia LINQ.

LI NQ i bazy danych Jak przekonaliśmy się już w rozdziale 8., technologia LINQ umożliwia przeprowadzanie różnych działań na kolekcjach danych; do operacji tych należą na przykład filtrowanie, sor­ towanie i grupowanie. W rozdziale tym korzystaliśmy wyłącznie z obiektów, jednak w tego typu zadaniach doskonale sprawdzają się właśnie bazy danych. Co więcej, jednym z powo­ dów powstania LINQ była chęć ułatwienia używania baz danych z poziomu kodu. Jak widać na listingu 14.2, technologia LINQ umożliwia bezproblemowe mieszanie poleceń odpowiedzial­ nych za dostęp do danych z kodem opracowanym w języku C# - przedstawiony tu przykład współpracy z bazą danych bardzo przypomina przykłady obiektowe, z którymi mieliśmy do czynienia w poprzednich rozdziałach tej książki . W kodzie przedstawionym na listingu 14.2 wykorzystany został mechanizm LINQ to Entities dostawca LINQ współpracujący z rozwiązaniem Entity Framework. Technologia Entity Frame­ work czekała na swoją premierę aż do czasu pojawienia się dodatku Service Pack 1 dla śro­ dowiska Visual Studio 2008, lecz istnieje jeszcze jeden starszy dostawca bazy danych LINQ o nazwie LINQ to SQL, który pojawił się już w pierwszym wydaniu Visual Studio 2008. Mechanizm LINQ to SQL współpracuje jedynie z oprogramowaniem SQL Server oraz SQL Server Compact 3.5 i ma dość wąskie zastosowanie. Jego zadaniem jest zmniejszanie wysiłku wiążącego się z pisaniem kodu dostępu do danych przy jednoczesnym zapewnianiu możli­ wości korzystania z wygodnej składni języka C# w przypadkach związanych z używaniem danych przechowywanych w bazie danych zarządzanej przez system SQL Server. Entity Framework jest pod tym względem podobny, oferuje jednak kilka dodatkowych moż­ liwości . Przede wszystkim został on zaprojektowany w taki sposób, aby obsługiwać wiele różnych baz danych pochodzących od rozmaitych producentów. Mechanizm ten korzysta z otwartego modelu dostawcy, dzięki czemu możliwe jest zapewnienie wsparcia dla dowolnej bazy danych; bez większego kłopotu da się również uzyskać dostawców dla większości popu-

546

I

Rozdział 14. Bazy danych

Dostawcy bazy danych LI NQ Działanie dostawców bazy danych LINQ bardzo różni się od pracy dostawcy LINQ to Objects, mimo że w zapytaniach związanych z obydwoma tymi rodzajami mechanizmu używa się tej samej składni. W przypadku rozwiązania LINQ to Objects klauzula where wykonuje całą swoją pracę w obrę­ bie platformy .NET - sposób działania jest tu podobny do sposobu działania pętli zawierającej instrukcję i f. Jednak próba zastosowania tego mechanizmu w stosunku do bazy danych skończy­ łaby się prawdziwą katastrofą. Jeśli nasza klauzula where ma wybrać jeden wiersz spośród 20 milionów, z całą pewnością nie chcemy, aby kod C# przechodził w pętli przez te całe 20 milionów wierszy! Chcemy natomiast, aby filtrowanie przeprowadziła sama baza danych, która może skorzystać ze swoich indeksów w celu wydajnego zlokalizowania odpowiedniego wiersza. Tak się składa, że mechanizm ten działa dokładnie tak, jak byśmy sobie tego życzyli - klauzula LINQ where widoczna na listingu 14.2 jest ostatecznie tłumaczona na klauzulę WHERE języka SQL. Jak Czytelnik z pewnością pamięta, język C# konwertuje wyrażenie zapytania LINQ na postać serii wywołań metod, a w efekcie tych wywołań budowany jest obiekt zapytania, który wie, jak zwrócić odpowiedni wynik. Technologia LINQ korzysta z wykonania opóźnionego (ang. deferred execution) zapytanie nie zaczyna zwracać wyników aż do czasu, gdy o nie poprosimy. Dostawcy LINQ współpracujący z bazami danych działają podobnie, jednak zamiast współpracować bezpośrednio z interfejsem I Enumerabl e, używają specjalistycznego typu dziedziczącego po interfejsie I Enumerabl e noszącego nazwę I Queryabl e. Z racji tego, że I Queryabl e dziedziczy po I Enumerabl e, w dalszym ciągu możemy wyliczać zawartość tej kolekcji w standardowy sposób, jednak dopiero wtedy, gdy to robimy, mechanizm generuje odpowiednie zapytanie bazy danych; nie ruszy on bazy danych, dopóki nie poprosimy o elementy. Nadal więc mamy tu do czynienia z wykonaniem opóźnionym, ale - co najważniejsze - gdy w końcu wykonamy to zapytanie, cały łańcuch przetwarzania repre­ zentowany przez nasze zapytanie LINQ jest przekształcany w pojedyncze zapytanie SQL, aby baza danych mogła wykonać całą niezbędną pracę. W skrócie sprawy przedstawiają się zatem tak: podczas gdy mechanizm LINQ to Objects wylicza wszystkie obiekty pochodzące ze źródła i uruchamia łańcuch przetwarzania wewnątrz naszej apli­ kacji .NET, dostawcy bazy danych LINQ przenosi przetwarzanie do bazy danych.

larnych baz. Poza tym Entity Framework pozwala reprezentacji .NET mieć strukturę różniącą się od schematu używanej bazy danych, jeśli jest to konieczne . Możemy zdefiniować model koncepcyjny, którego encje niekoniecznie muszą bezpośrednio odpowiadać wierszom poszcze­ gólnych tabel . Jednostka taka może zawierać dane, które są przechowywane w wielu różnych tabelach w samej bazie danych. Encja ta może być dzięki temu reprezentowana przez poje­ dynczy obiekt. Możliwe jest oczywiście korzystanie z modelu koncepcyjnego dokładnie odpowiadającego modelowi bazy danych - swobodnie możemy tworzyć proste odwzorowanie, w którym jedna encja reprezentuje jeden wiersz jednej tabeli. Wykorzystywany w ten sposób Entity Framework w połączeniu z LINQ to Entities sprawia, że mechanizm LINQ to SQL wydaje się niepo­ trzebny. Po co nam zatem obydwa te rozwiązania? Głównym powodem istnienia LINQ to SQL jest to, że był on dostępny, gdy środowisko Visual Studio 2008 trafiło na rynek, podczas gdy prac nad rozwiązaniem Entity Framework firmie Microsoft nie udało się jeszcze wówczas ukończyć. Technologia LINQ stanowiła ważną część tego wydania, a ponieważ jedną z głównych przyczyn jej powstania było umożliwienie dostępu do danych, wprowadzanie nowej wersji środowiska bez mechanizmu zapewniającego dostęp do danych za pośrednictwem LINQ byłoby nieco rozczarowujące . Mechanizm LINQ to SQL

Krajobraz możliwości dostępu do danych w ramach platformy .NET

I

547

został opracowany przez inny zespół programistów (odpowiedzialny był za niego zespół LINQ, nie zaś grupa związana z narzędziami umożliwiającymi dostęp do danych) i był on gotowy wcześniej, częściowo również dlatego, że miał znacznie mniej ambitne cele do osiągnięcia. Firma Microsoft stwierdziła, że choć obydwie technologie są w pełni obsługiwane, większość wysiłków należy skupić na Entity Framework. Środowisko Visual Studio 2010 wprowadza kilka nowych możliwości związanych z rozwiązaniem LINQ to SQL, jednak w dłuższej perspekty­ wie czasowej to właśnie LINQ to Entities będzie rozwijane w większym stopniu. Z tego też powodu w niniejszym rozdziale skupimy się głównie na mechanizmie Entity Frame­ work (choć trzeba przyznać, że wiele omawianych tu koncepcji ma równie dobre zastosowa­ nie w przypadku obydwu tych rozwiązań) . Niemniej jednak autorzy naprawdę lubią LINQ to SQL. W przypadkach, w których korzystamy z systemu SQL Server i w których nie potrze­ bujemy koncepcyjnego modelu i możliwości odwzorowywania oferowanych przez Entity Framework, wolimy raczej zastosować LINQ to SQL z racji prostoty tego rozwiązania oraz dlatego, że nauczyliśmy się już, jak go używać. Jeśli jednak Czytelnik chce nauczyć się tylko jednej technologii dostępu do danych wykorzystywanej w obrębie platformy .NET, lepszym wyborem na dłuższą metę okaże się z pewnością Entity Framework.

Technologie dostępu do danych n ieopracowane przez firmę Microsoft Od czasu, gdy firma Microsoft wprowadziła mechanizm Entity Framework na rynek, pojawiły się na nim i przyjęły różne niezależne rozwiązania umożliwiające odwzorowywanie relacyjnych danych w modele obiektowe . Nie zamierzamy omawiać ich dokładnie w niniejszej książce, przyda nam się jednak z pewnością wiedza, że Entity Framework nie jest jedynym graczem na tym polu. Prawdopodobnie najbardziej znanym rozwiązaniem alternatywnym wobec Entity Framework jest NHibernate (http://nhforge.org/) . Jest to współpracująca z platformą .NET wersja mechani­ zmu Hibernate stanowiącego popularny mechanizm ORM (ang. Object Relational Mapper mechanizm odwzorowujący relacyjne bazy danych w obiekty) dla języka Java. NHibernate funkcjonował już od dobrych kilku lat, zanim na rynku pojawił się Entity Framework (a jego poprzednik współpracujący z językiem Java jest nawet jeszcze starszy), z wielu względów jest on zatem bardziej dojrzałym i lepiej wyposażonym mechanizmem ORM. Z drugiej jed­ nak strony NHibernate jest starszy niż technologia LINQ (język Java nie oferuje zaś niczego podobnego do LINQ), dlatego jak do tej pory zapewniana przezeń obsługa tej technologii jest dość ograniczona .

-

Dostępnych jest też bardzo wiele innych rozwiązań ORM współpracujących z platformą .NET, z których część oferowana jest za darmo, a część na zasadach komercyjnych. Są one zbyt liczne, aby próbować je tu chocby wymienić, co możemy łatwo stwierdzić, przeprowadzając szybkie wyszukiwanie w sieci .

WCF Data Services Większość komunikacji z bazami danych odbywa się za pośrednictwem specjalistycznych protokołów, charakterystycznych dla producenta. Zapory sieciowe są zwykle konfigurowane w taki sposób, aby uniemożliwiać im przekazywanie danych, i mają ku temu dobre powody: 548

I

Rozdział 14. Bazy danych

pod względem bezpieczeństwa bezpośrednie udostępnianie bazy danych w internecie może wyglądać na bardzo kiepski pomysł . Tym niemniej niektórzy ludzie chcą korzystać właśnie z takiego rozwiązania i istnieją pewne scenariusze, w przypadku których nie jest to aż tak złą koncepcją, jak mogłoby się początkowo wydawać, szczególnie gdy jesteśmy w stanie sprawo­ wać wystarczająco silną kontrolę nad tym, co jest widoczne na zewnątrz. Korzystając z WCF Data Services, możemy udostępniać relacyjny magazyn danych za pośred­ nictwem protokołu HTTP i formatu XML lub JSON. Możemy przy tym wybierać dane, które mają być widoczne, a także określać, kto może uzyskiwać do nich dostęp . Co więcej, model, który przedstawiamy na zewnątrz, wcale nie musi być taki sam jak struktura używanej bazy danych. W gruncie rzeczy w cały proces nie musi być nawet w ogóle zaangażowana jakakol­ wiek baza danych, istnieje bowiem model dostawcy umożliwiający udostępnianie za pomocą tego mechanizmu dowolnych danych, o ile tylko jesteśmy w stanie znaleźć sposób takiego ich przedstawienia, aby wyglądały na dane o charakterze relacyjnym. Komponentu WCF Data Services będziemy zwykle używać w połączeniu z mechanizmem Entity Framework. Możemy dzięki temu zdefiniować jednostki, które mają być prezentowane za pośrednictwem HTTP, a także skorzystać z oferowanych przez ten mechanizm usług odwzo­ rowywania w celu wypełnienia luki pomiędzy nimi a podstawowym źródłem danych. Z tego powodu dokładniej przyjrzymy się tym usługom w dalszej części niniejszego rozdziału, gdy zakończymy już prezentację mechanizmu Entity Framework. Główne zadanie komponentu WCF Data Services jest nieco inne, niż ma to miejsce w przy­ padku innych przedstawionych do tej pory możliwości związanych z dostępem do danych. Chodzi tu przede wszystkim o prezentowanie danych za pośrednictwem sieci, podczas gdy w pozostałych przypadkach chodziło o konsumowanie danych. Istnieje tu jednak również odpowiedni działający po stronie klienta komponent, który zapewnia możliwość wykonywa­ nia zapytań LINQ dla tego rodzaju usług. Choć jest on częścią technologii WCF Data Services, ma charakter opcjonalny - nie jesteśmy zmuszeni używać go w kodzie klienta . Klient tego typu zaś niekoniecznie musi wymagać funkcjonowania mechanizmu WCF Data Services po stronie serwera - części działające po stronie klienta mogą być wykorzystywane w połączeniu z dowolnymi usługami, które udostępniają dane w ten sam sposób.

Technologia Si lverl ight i dostęp do danych Silverlight wykorzystuje poważnie ograniczoną wersję platformy .NET, aby zmniejszyć do minimum całkowitą ilość danych, które trzeba pobrać w celu używania tej technologii, a także w jak największym stopniu skrócić czas niezbędny do jej instalacji. Wersja ta nie oferuje zatem zbyt wielu możliwości w zakresie obsługi mechanizmów dostępu do danych. W rzeczywisto­ ści ilość danych nie jest tu jedynym powodem istnienia ograniczeń. W przypadku aplikacji klienta Silverlight zwykle nie ma wielkiego sensu próbować łączyć się bezpośrednio z bazą danych, ponieważ Silverlight jest technologią WWW przeznaczoną do wykorzystywania po stronie klienta, a większość administratorów systemów stara się sprawić, aby zarządzane przez nich bazy danych nie były dostępne przez internet za pośrednictwem ich naturalnych protokołów. Bezpośrednie połączenie z serwerem bazy danych może być oczywiście dobrym rozwiązaniem w przypadku aplikacji działających w intranecie, nie jest ono jednak obsługiwane . Technolo­ gia Silverlight oferuje możliwość korzystania z LINQ, nie są jednak dostępni dostawcy LINQ

Krajobraz możliwości dostępu do danych w ramach platformy .NET

I

549

to SQL ani LINQ to Entity Framework, ponieważ brakuje tu używanego przez nich podstawo­ wego mechanizmu dostępu do bazy danych. Jedynym obsługiwanym przez Silverlight mecha­ nizmem dostępu do bazy danych jest klient WCF Data Services.

Bazy danych Pełna wersja platformy .NET umożliwia współpracę z szerokim spektrum baz danych. Prosty mechanizm dostępu do danych ADO.NET, od którego prezentacji zaczęliśmy, wykorzystuje interfejsy, aby umożliwić producentom baz danych opracowanie ich własnych implementacji specyficznych dla poszczególnych baz. Podobnie jest w przypadku Entity Framework, który również nie jest uzależniony od żadnego określonego rodzaju bazy danych - korzysta on z otwartego modelu dostawcy, który został opracowany w taki sposób, aby umożliwiać doda­ nie obsługi dowolnej relacyjnej bazy danych. Firma Microsoft zapewnia oczywiście odpowied­ niego dostawcę dla własnej bazy danych, czyli systemu SQL Server, jednak inni producenci robią dokładnie to samo w przypadku swoich baz takich jak: Oracle, MySQL, PostgreSQL, SQLite, Sybase oraz DB2. W tej książce będziemy posługiwać się bazą SQL Server. Przedstawione tu przykłady współ­ pracują właśnie z tym dostępnym za darmo systemem. (Niektóre edycje środowiska Visual Studio standardowo automatycznie instalują oprogramowanie SQL Server 2008 Express) . Wyda­ nie Express systemu SQL Server korzysta z tego samego silnika bazy danych, którego używają „prawdziwe" wersje rozwiązania, dotyczą go jednak pewne ograniczenia związane z wielko­ ścią bazy, jak również brak w nim niektórych bardziej zaawansowanych funkcji. Choć jest to nieco okrojona wersja, bez trudu poradzi sobie ona z obsługą całkiem poważnych serwisów WWW . Może być też wykorzystywana w aplikacjach klienckich napisanych przy użyciu WPF lub Windows Forms, w których będzie odpowiadała za przechowywanie lub buforowanie danych po stronie klienta, choć rozwiązanie takie może komplikować proces instalacji programu tego rodzaju - instalowanie kopii oprogramowania SQL Server nie należy bowiem do zadań trywialnych.

Pobieranie i uruchamianie bazy danych przy użyciu systemu SQL Server 2008 Express Jeśli Czytelnik chce samodzielnie uruchamiać przykłady przedstawione w niniejszym roz­ dziale, nie tylko będzie potrzebował zainstalowanej kopii oprogramowania SQL Server 2008 Express, lecz będzie również musiał zainstalować przykładową bazę danych. Skorzystamy tu z odchudzonej wersji bazy danych Adventure Works dostępnej na stronie http://msftdbprodsamples. codeplex com/. .

Pobranie i zainstalowanie tych przykładowych danych jest zadaniem nieco podchwytliwym, ponieważ dostępnych jest wiele różnych wersji bazy Adventure Works. Istnieją wersje pełne i lekkie współpracujące z systemami SQL Server 2005 oraz SQL Server 2008, a każda z wersji oprogramowania SQL Server dostępna jest w postaci różnych edycji, z których część umieszcza swoje pliki danych w innych miejscach niż pozostałe. Z uwagi na fakt istnienia wszystkich tych odmian bazy łatwo może dojść do sytuacji, w której nie uda się uruchomić przykładowej bazy danych, mimo że proces instalacji zostanie przeprowadzony bez żadnych widocznych błędów. Co więcej, zestaw działań wymaganych do zainstalowania bazy zmienia się od czasu do czasu wraz z tym, jak pojawiają się jej nowe wersje. Planowaliśmy tu zamieścić szczegółowy opis odpowiedniej procedury, ale w trakcie pisania tej książki w instalatorze bazy danych wprawa-

550

I

Rozdział 14. Bazy danych

dzono zmiany, które spowodowały, że opracowana przez nas procedura okazała się zupełnie bezużyteczna. Z uwagi na to, że historia ta może się powtórzyć w czasie, który upłynie pomię­ dzy wydaniem niniejszej książki a chwilą, gdy Czytelnik będzie ją czytał, postanowiliśmy zre­ zygnować z tworzenia specjalnej instrukcji i odesłać czytelników do aktualizowanych na bieżąco informacji zamieszczonych na stronie poświęconej przykładowej bazie. Nasz szybki przegląd oferowanych przez platformę .NET możliwości związanych z dostę­ pem do danych dobiegł końca. Wiemy już też, w jaki sposób zainstalować przykładową bazę danych, która jest niezbędna w celu wykonania przedstawionych w tym rozdziale kodów. Możemy więc przejść do dokładniejszego opisu mechanizmu Entity Framework. Zaczniemy od modelu stanowiącego serce tego rozwiązania.

Model encji danych Główne zadanie mechanizmu Entity Framework (lub - w skrócie - EF) polega na ułatwieniu korzystania z poziomu kodu z danych przechowywanych w bazie. Obiekty języka C# mają zupełnie inną naturę niż informacje zapisane w relacyjnej bazie danych, a proces radzenia sobie z tymi różnicami oraz przenoszenia danych pomiędzy tymi dwoma światami nosi nazwę odwzorowywania (ang. mapping) . (Dlatego mechanizm Entity Framework jest rodzajem rozwią­ zania ORM) . Jak widać na rysunku 14.1, odwzorowywanie zachodzi w obydwu kierunkach. Gdy informacje są pobierane z bazy danych, przeprowadzane jest ich ładowanie do obiektów. Jeśli zaś w kodzie C# zachodzi modyfikacja tych obiektów lub jeśli tworzone są nowe, możemy sprawić, aby baza danych była odpowiednio aktualizowana .

Model kon()epcyjny Cu sto mer 01dm Address

Address Stleet City

(om1t iy

Postcode

Details ShipTo Billing

OrderDetail Product Qua ntity

Odwzorowanie

Model przechowywania

Zarządzane przez

mec;ha11zm i Entity framework

Rysunek 14. 1 . Modele i odwzorowywanie w mechanizmie Entity Framework Projekt bazy danych nie zawsze odpowiada bezpośrednio strukturom danych, które są wygodne, jeśli weźmie się pod uwagę kod naszej aplikacji. Istnieje wiele powodów, dla których może nam zależeć na tym, aby nasz kod współpracował z modelem różniącym się nieco od samych danych. Baza danych może zawierać informacje niewymagane przez tę część aplikacji, którą piszemy, dlatego niewykluczone, że będziemy potrzebować jedynie pewnego podzbioru danych. Informacje na temat określonej jednostki mogą być rozdzielone pomiędzy wiele różnych tabel bazy z przyczyn związanych z wydajnością. Konwencje nazw stosowane w bazie danych mogą też nie pasować do naszego kodu.

Model encji danych

I

551

Z tego powodu Entity Framework umożliwia nam kontrolowanie odwzorowywania. Możemy zdefiniować model koncepcyjny (ang. conceptual model) opisujący encje w taki sposób, w jaki chcemy z nich korzystać z poziomu języka C#, a także odwzorowania (ang. mappings) opisujące to, jak model ten odwzorowuje używany w bazie schemat składowania. Mechanizm EF wymaga od nas dostarczenia tego schematu przechowywania danych (ang. stare schema), który stanowi defi­ nicję struktury, jaką spodziewamy się znaleźć w bazie danych. Może się to wydawać zbędne w końcu baza danych zna swój własny schemat, po co więc mechanizmowi EF jego kopia? Ist­ nieje kilka powodów tego stanu rzeczy . Po pierwsze, możliwe jest zdefiniowanie modelu przed utworzeniem samej bazy danych - możemy wygenerować schemat bazy danych na podstawie schematu przechowywania. Po drugie, możemy skonfigurować różne aspekty sposobu, w jaki Entity Framework wykorzystuje tę bazę danych, takie jak to, czy używa on zapytań i procedur składowanych przy dostępie do określonych tabel. Ustawienia, które są związane z samą bazą danych, a nie z tym, co EF robi z danymi, należą raczej do schematu przechowywania niż do odwzorowań lub schematu koncepcyjnego.

Trzy części przedstawione na rysunku 14.1 - model koncepcyjny, model przechowywania oraz odwzorowania pomiędzy nimi - są razem określane mianem modelu encji danych (ang. Entity Data Model, w skrócie - EDM) . Istnieje wiele ograniczeń dotyczących modelu koncepcyjnego, ponieważ jest on uży­ teczny jedynie wtedy, gdy da się skonstruować skuteczne odwzorowanie. Istnieją granice możliwości odwzorowania, dlatego struktura istniejącej bazy danych będzie narzucać pewne związane z tym ograniczenia modelu. Programiści rozpoczynający swoją przygodę z mechanizmem Entity Framework często stwierdzają, że mają o wiele mniej swobody w projektowaniu modelu koncepcyjnego, niż początkowo przypusz­ czali. W swoim czasie Czytelnik przekona się, co można osiągnąć za pomocą odwzoro­ wań, już teraz powinien jednak mieć świadomość, że EF jest w stanie przyjąć dowolny model koncepcyjny i połączyć go w odpowiedni sposób z dowolną starą strukturą bazy danych - musi bowiem istnieć bliski związek pomiędzy bazą danych i modelem koncepcyjnym.

W przypadku gdy skorzystamy z mechanizmu EF w najprostszy z możliwych sposobów, nasz model koncepcyjny będzie taki sam jak używany model składowania, a odwzorowywanie będzie bardzo proste . Jeśli zastosujemy zapewniany przez środowisko Visual Studio kreator dodawania obsługi EF do projektu, otrzymamy właśnie tego rodzaju odwzorowanie bezpośred­ nie, w którym funkcjonować będzie po jednym typie encji dla każdej zaimportowanej tabeli lub widoku. Następnie będziemy mogli dostosować działanie tego mechanizmu do swoich potrzeb. Teraz zajmiemy się sposobem używania wspomnianego kreatora. Choć produkuje on proste odwzorowanie, w którym model koncepcyjny dokładnie odpowiada modelowi skła­ dowania, nadal musi wygenerować pełen zestaw definicji modelu i odwzorowania, dlatego bliższe przyjrzenie się efektom jego pracy może się okazać bardzo pouczające . Obsługę mechanizmu EF można dodać do dowolnego projektu .NET (z wyjątkiem projektów Silverlight) . W naszych przykładach wykorzystamy aplikację konsolową, więc utwórzmy tego rodzaju projekt. Otwórzmy okno dialogowe Add New Item, w jego lewej części wybierzmy pozycję Visual C# Items/Data, a następnie w środkowej wskażmy szablon ADO.NE T Entity Data Model i nadajmy nowemu plikowi nazwę AdventureWorksModel. Gdy do swojego projektu dodajemy model encji danych, środowisko Visual Studio pyta, czy chcemy zbudować go od podstaw, czy też oprzeć na istniejącej bazie danych. Wybierzemy tu

552

I

Rozdział 14. Bazy danych

drugą, prostszą opcję. Jeśli wcześniej poinformowaliśmy już środowisko Visual Studio o jakich­ kolwiek bazach, których używaliśmy - niezależnie od tego, czy odbyło się to za pośrednictwem paska narzędzi Server Explorer, czy też przy okazji korzystania z tego lub jakiegoś innego kre­ atora związanego z danymi - ich nazwy pojawią się na liście rozwijanej . Z poziomu kreatora możemy też jednak podać informacje na temat nowego połączenia . Na potrzeby bieżącej apli­ kacji połączymy się z przykładową bazą danych AdventureWorksLT2008 . ••

• .·

-_,..�;

,

L-------1:.JI> '

Kreator korzysta z nazwy naszego połączenia w przypadku jednego z typów, który generuje. Czytelnik zauważy wkrótce, że w wielu kolejnych przykładach pojawia się identyfikator AdventureWorksL T2008Enti ti es. Gdy zdarzy nam się nadać naszemu połączeniu inną nazwę w środowisku Visual Studio, w kodzie będziemy musieli używać właśnie jej .

Gdy wybierzemy już bazę danych, środowisko Visual Studio wyświetli w postaci widoku drzewa wszystkie tabele, widoki i procedury składowane, które możemy potraktować jako punkt wyjścia dla naszego modelu. Dla każdego elementu, który wskażemy, zostaną dodane odpowiednie elementy do schematu przechowywania, schematu koncepcyjnego oraz odwzo­ rowań. Gdy działanie kreatora się zakończy, wygeneruje on plik o rozszerzeniu edmx, który będzie zawierał definicję utworzonego modelu encji, a środowisko Visual Studio otworzy gra­ ficzny widok tego pliku. Na rysunku 14.2 został przedstawiony model koncepcyjny, który pojawi się, gdy korzystając z kreatora, wybierzemy tabele Customer, Sal esOrderHeader oraz Sal esOrder "+Det a i l , a następnie klikniemy przycisk Finish. � �

El P roperti es � Sa l esOrd erlD

C11sto me1

El P rop erti es

f ostN a m e

,=

ILa st N a m e

,=

G om pa nyN ;i m e

,=

P a ssw ord Sa lt

� Sa l esP erson � Em a i lAdd res.s � P h on e � P a ssw ord H ;i sh

� row g u i d

El N av i' g ati on P roperti es @ij Sa l e�Ord erH ea d er ,=

M od ifi ed Date

,=

Sh i pDate

,=

On l i n eOrd erF l a g

,=

P u rc h a �eOrd erN „ .

,=

Sh i pToAd d ress!D



� S a l esOrd erN u m „ .

1

� Ac c ou ntN u m b er

� B i l lToAdd ress!D � Sh i pM eth od � C r e d itCa rdAppr . . . � Su bT ota l � Ta:xAmt � F rei g ht � Tota l Du e � G om m ent ,=

Sale.sOrdeńleta il

El P roperti es

� Sta u s

� M i d d l eN a m e � Suffi:x

@

� Revi si on lN u m ber � Ord erDate � Du eDate

� Gu stom er!D � IN a m eStyl e � Titl e ,=

Sale.s0rde1He.a1 . • •

1

@

� Sa l esOrd erID � S a l e �Ord erDeta i „ . � Ord erQty � P rod u ct!D � U n itP ri c e � U n itP ri c eDi sc ou nt � Li n eTota1I � row g u i d � M od ifi edDate @ij

El N avi g ati on P roperti es Sa l esOrd erH ea d er

row g u i d

� M od ifi ed Date @ij @ij

El N avi gat i on P roperti es Ou stom er Sa l esOrd erDeta i l

Rysunek 14.2 . Model koncepcyjny obejmujący trzy encje

Model encji danych

I

553

Widok ten prezentuje jedynie model koncepcyjny. Nieco więcej informacji na temat EDM może­ my zobaczyć, korzystając z okna Model Browser, które zostało przedstawione na rysunku 14.3. Zwykle pojawia się ono na ekranie, gdy otwieramy EDM, jeśli jednak zmienimy układ okien i stracimy je z oczu, w celu przywołania go na wierzch powinniśmy po prostu kliknąć tło modelu prawym przyciskiem myszy i z menu kontekstowego wybrać pozycję Model Browser. Przeglądarka wyświetla listy związane zarówno ze schematem koncepcyjnym (ta widoczna jest tutaj w ramach węzła AdventureWorks LT2008Model ), jak i ze schematem przechowywania (węzeł AdventureWorks LT2008Model . Stare) . Wszystkie trzy wybrane tabele widoczne są w obydwu tych miejscach, a gdy je rozwiniemy, przekonamy się, że właściwości każdej z encji w modelu kon­ cepcyjnym odpowiadają bezpośrednio kolumnom tabel w schemacie przechowywania . M o· d el B rowser

Type here

..iii

. cust . Cus tome r I D == 2953 1) ; myCustomer . Sal esOrderHeaders . Load () ;

Consol e . Wri teli ne (myCus tomer. Sal esOrderHeaders . Count) ;

Platforma .NET 4 wprowadza dodatkowy sposób wykonania tej operacji, który określa się mianem późnego ładowania danych (ang. lazy loading) . Zamiast jawnie wywoływać metodę Load, możemy dzięki niemu zdać się na mechanizm EF, który automatycznie załaduje powiązane obiekty w chwili, gdy się do nich odwołamy. Kontekst zawiera odpowiednią właściwość, za pomocą której da się kontrolować działanie tego rozwiązania: dbContext . ContextOpt i ons . Lazyload i ng Enabl ed = fal s e ;

Właściwość ta ma standardowo wartość t rue; przypisanie jej wartości fal s e spowoduje przy­ wrócenie sposobu działania mechanizmu charakterystycznego dla wydań platformy .NET poprzedzających wersję 4. Jeśli jednak pozostawimy tę wartość bez zmian, kod przedstawiony na listingu 14.6 będzie w pełni równoważny kodowi zaprezentowanemu na listingu 14.7, ponie­ waż mechanizm EF automatycznie wywoła za nas metodę Load, gdy tylko podejmiemy próbę skorzystania z właściwości nawigacji. (Kolekcja ta ignoruje wywołania metody Load, jeśli encje są już załadowane, dlatego wielokrotne zgłaszanie żądania ładowania danych nie stanowi tu najmniejszego problemu) . W każdym z tych przypadków mechanizm EF musi wykonać dodatkowe odwołanie do bazy danych. Wywołanie metody S i ngl e spowoduje pobranie klienta z bazy danych przed zwróce­ niem wyniku, co oznacza, że konieczne będzie drugie żądanie, gdy Gawnie lub niejawnie) popro­ simy później o pobranie związanych z nim wierszy, ponieważ EF nie będzie miało pojęcia, że zamierzamy skorzystać z tych elementów, dopóki sobie ich nie zażyczymy. Nie musi to oczywi­ ście stanowić problemu, jednak ogólnie rzecz biorąc, im częściej odwołujemy się do bazy danych, tym wolniej działa nasz program. Aby uzyskać spójne wyniki, powinniśmy zapewnić, że zapytanie wstępne i następujące po nim operacje późnego ładowania przeprowadzane są w ramach pewnej transakcji (tak jak zostało to pokazane w dalszej części rozdziału), jednak w celu uzyskania odpowiedniej skalowalności w silnie obciążonych systemach należy zminimalizować liczbę odwołań występujących w każdej pojedynczej transakcji. Możemy zatem poinformować mechanizm EF, że życzymy sobie, aby określone, związane ze sobą encje były pobierane w tym samym momencie co główny wynik za­ pytania. Robi się to za pomocą metody I n cl ude, która jest dostępna w ramach każdego zestawu encji zapewnianego przez kontekst. Przykład jej użycia został przedstawiony na listingu 14.8. Model encji danych

I

561

Zachowajmy ostrożność, włączając mechanizm późnego ładowania danych, ponie­ waż może to czasami skutkować wieloma niepotrzebnymi odwołaniami do bazy danych. Jeden z autorów tej książki brał na przykład udział w tworzeniu pewnego projektu zawierającego kod diagnostyczny, który „pomagając" programistom, zapisywał w pliku dziennika tymczasową kopię wybranych obiektów wraz z warto­ ściami wszystkich należących do nich właściwości. Niestety, kod ten miał charakter rekurencyjny - gdy właściwość odnosiła się do innego obiektu, zapisywany był rów­ nież on, a gdy właściwość odwoływała się do kolekcji obiektów, zapisywane były wszystkie. Kod rejestrujący wyposażony był w odpowiedni mechanizm detekcji cyklu, dlatego nie miał prawa wpaść w nieskończoną pętlę, jednak w innym przypadku nie zatrzymałby się aż do momentu, w którym zapisałby wszystkie obiekty osiągalne z punktu początkowego. Niestety, włączone było późne ładowanie danych, dlatego gdy tylko kod otrzymywał jakąkolwiek encję, pobierał wszystkie związane z nią encje niezależnie od tego, jak odległy związek łączył je z tym pierwszym obiektem. Skutkiem tego program atakował bazę danych tysiącami odwołań za każdym razem, gdy gene­ rowany był wpis w pliku dziennika. Nowoczesne bazy danych działają zadziwiająco szybko, dlatego tego rodzaju pro­ blem może pozostać niezauważony, gdy pojawia się na maszynach programistów korzystających z własnej, dostępnej lokalnie instancji bazy danych. Z pewnością jednak nie życzylibyśmy sobie, aby dotyczył on mocno obciążonego produkcyjnego serwera bazy danych.

Listing 14.8. Określanie związków, które majq zostać wstępnie załadowane z bazy var customersWi thOrderDetai l s = dbContext . Customers . I ncl ude ( " Sal esOrderHeaders . Sal esOrderDetai l s " ) ;

Cus tomer myCustomer = dbContext . Cus tomers . S i ngl e (cust => cus t . Cus tomerI D == 2953 1 ) ; Consol e . Wri teli ne (myCus tomer. Sal esOrderHeaders . Count) ;

To wywołanie metody I n cl ude powoduje załadowanie powiązanych encji dostępnych za pośred­ nictwem właściwości Sal esOrderHeaders encji Cus tomer. (Zostaną one załadowane niezależnie od ustawienia późnego ładowania danych) . Informuje ono również mechanizm EF, że dla każ­ dej z tych powiązanych encji powinien on załadować wszelkie związane z nią encje widoczne za pośrednictwem właściwości Sal esOrderDetai l s . Innymi słowy, wywołanie to mówi EF, że chcemy załadować wszystkie zamówienia związane z wybranym klientem oraz wszystkie związane z nimi szczegóły. Wygeneruje ono pojedyncze zapytanie, które pobierze wszystkie nie­ zbędne informacje w ramach jednego żądania. '

. .

Jeśli Czytelnik zastanawia się, dlaczego wstępnie nie pobiera ono wszystkich powią­ zanych elementów za każdym razem, powinien rozważyć wpływ takiego działania na wydajność rozwiązania. W niektórych sytuacjach agresywne wstępne pobieranie wszystkich powiązanych elementów mogłoby prowadzić do kopiowania znacznego fragmentu bazy danych do pamięci komputera! Jednak nawet w mniej wymagających przypadkach pobieranie większej ilości danych, niż jest to konieczne, może spowolnić system lub ograniczyć skalowalność rozwiązania.

Do tej pory mieliśmy do czynienia jedynie z tak zwanymi związkami jeden-do-wielu - jeden klient może być związany z wieloma zamówieniami, a jedno zamówienie może być związane z wieloma szczegółami. Istnieją jednak również inne rodzaje związków.

562

I

Rozdział 14. Bazy danych

Krotność Krotność (ang. multiplicity) związku odnosi się do liczby uczestniczących w nim elementów po każdej stronie asocjacji. W przypadku mechanizmu Entity Framework krotność asocjacji określa naturę właściwości nawigacji, które reprezentują związek. ' .'

L---LI"'.

,

W mechanizmie Entity Framework każda asocjacja ma zawsze dwie strony niezależnie od krotności. Po jednej stronie związku mogą na przykład występować klienci, a po drugiej zamówienia. Krotność opisuje, ile elementów może znajdować się po określonej stronie, nie zaś ile w związku występuje stron. Czasami będziemy chcieli przedstawiać bardziej złożone związki - na przykład tak zwany związek potrójny (ang. ternary) dotyczy trzech rodzajów elementów uczestni­ czących. Mamy tu do czynienia z koncepcją określaną mianem stopnia (ang. degree) będącą czymś innym niż krotność. Przykładem może być plan zajęć na uniwersytecie, w przypadku którego wykładowca wykłada pewien przedmiot studentowi; w związku tym biorą udział trzy encje (wykładowca, przedmiot i student). Te związki wyższego stopnia w bazie danych modeluje się zwykle za pomocą specjalnej tabeli, która prze­ chowuje wyłącznie informacje na temat samego związku. Podobnie jest z EDM, który nie zapewnia bezpośredniej obsługi związków stopni wyższych niż drugi, dlatego zwią­ zek tego rodzaju powinniśmy przedstawić za pomocą oddzielnego typu encji w modelu koncepcyjnym, dodając asocjacje pomiędzy tą encją i wszystkimi elementami biorą­ cymi udział w danym związku.

Dla każdej strony związku można określić krotność o wartości 1, O . . 1 lub * . Pierwsza z nich, 1 , oznacza dokładnie to, co powinna - po tej stronie asocjacji zawsze występuje jeden element. Ostatnia, *, wskazuje dowolną liczbę - po tej stronie może być zero, jeden lub dowolnie wiele elementów. Krotność O 1 oznacza zero lub jeden - wskazuje na to, że asocjacja ma cha­ rakter opcjonalny, jednak gdy występuje, po tej jej stronie znajduje się tylko jeden element. •



W przypadku związku jeden-do-wielu strony mają odpowiednio krotności 1 i *. Możemy to zobaczyć na rysunku 14.2 - odcinki linii pomiędzy encjami reprezentują asocjacje, a krotno­ ści oznaczane są na obydwu końcach tych odcinków. Wynika stąd, że element występujący po lewej stronie może być związany z wieloma elementami po prawej; element po prawej jest z kolej zawsze związany z dokładnie jednym elementem po stronie lewej . W języku C# encja występująca po stronie krotności 1 byłaby wyposażona we właściwość nawigacji oferującą kolekcję w celu zapewnienia dostępu do strony z wieloma elementami. Encja znajdująca się po stronie krotności * zapewniałaby prostszą właściwość niekolekcyjną pozwalającą jedynie na powrót do pojedynczej encji, z którą jest związana. Odmianą tego związku jest sytuacja, w której po lewej stronie zamiast krotności 1 występuje krotność O 1, a po prawej mamy do czynienia z krotnością *, tak jak miało to miejsce wcze­ śniej . Jest to przypadek podobny do relacji jeden-do-wielu oprócz tego, że element po stronie wielu nie musi być związany z jakimkolwiek elementem po drugiej stronie. Moglibyśmy w ten sposób zechcieć na przykład przedstawić związek łączący kierowników i ich raporty. Zwykle powinien nam do tego wystarczyć związek jeden-do-wielu, jednak gdybyśmy prześledzili hierarchię firmy do wystarczająco wysokiego poziomu, przekonalibyśmy się, że na jej szczycie jest ktoś, kto nie ma nad sobą żadnej władzy kierowniczej - właściwość nawigacji zwróciłaby w takim przypadku wartość pustą. Z tego powodu nie sprawdziłby się tu prosty związek jeden-do-wielu i po „kierowniczej" stronie asocjacji musielibyśmy zastosować krotność O 1 zamiast krotności 1 . •





Model encji danych

I



563

Czasami występują też związki jeden-do-jednego w ich przypadku każdy element wystę­ pujący po jednej stronie jest zawsze związany z dokładnie jednym elementem po stronie drugiej . Jest to dość nietypowy rodzaj związku, ponieważ wynika z niego, że biorące w nim udział encje są ze sobą nierozerwalnie połączone i że połączenie to ma charakter wyłączny. Związki, które wydają się być związkami jeden-do-jednego, w rzeczywistości zwykle nimi nie są. Oto przykład pochodzący z kultury masowej, który opisuje związek pomiędzy mistrzem i uczniem: „Dwóch zawsze ich jest. Nie więcej, nie mniej . Mistrz i uczeń" 4 . Mistrz zawsze ma ucznia, uczeń zawsze ma mistrza, czyż nie jest to więc związek jeden-do-jednego? W rzeczywi­ stości może tu zachodzić konieczność zastosowania związku typu jeden-do-wielu, ponieważ w przypadku śmierci ucznia mistrz wybiera sobie nowego. (Uczeń ma tylko jednego mistrza, ponieważ jedyną możliwą dla niego ścieżką kariery jest awans na mistrza lub przedwczesna śmierć. Możemy być zatem przynajmniej pewni, że nie jest to związek wiele-do-wielu) . Wyra­ żone tutaj ograniczenie mówi jedynie, że mistrz może mieć jednego ucznia naraz, a więc jego stosunek do związku przypomina nieco seryjną monogamię. (Na przykład uczniami Dartha Sidiousa byli zarówno Darth Maul, jak i Darth Vader) . Jeśli zatem baza danych musi odzwier­ ciedlać pełną historię, nie zaś jedynie stan bieżący, związek jeden-do-jednego okaże się tu nie­ wystarczający. (Choć jeśli wymagamy od niej jedynie przechowywania aktualnego stanu, związek ten sprawdzi się tu wręcz doskonale) . Związki jeden-do-jednego występują w bazach danych często dlatego, że informacje dotyczące pojedynczej encji zostały podzielone pomiędzy wiele tabel, zwykle z powodów związanych z wydajnością. (Mechanizm EF umożliwia odwzorowanie ich z powrotem do postaci pojedynczej encji w modelu koncepcyjnym, dlatego związki tego rodzaju częściej występują w schemacie przechowywania niż w schemacie koncepcyjnym) . -

Warianty związków typu jeden-do-jednego, w których jedna lub druga strona występuje opcjonalnie, mogą być bardzo użyteczne5 . Moglibyśmy na przykład mieć do czynienia z encją reprezentującą klienta i encją reprezentującą konto. Organizacja (taka jak sklep rzeźniczy) mogłaby przyjąć politykę, zgodnie z którą klienci nie muszą posiadać kont, jednak gdy je mają, pojedynczy klient może dysponować tylko jednym kontem, a konta muszą należeć do dokładnie jednego klienta . (Nie jest to oczywiście jedyna dopuszczalna polityka w tym przy­ padku) . Związek zachodzący pomiędzy encją klienta a encją konta miałby krotność 1 po stronie klienta i krotność O 1 po stronie konta . •



Istnieją wreszcie związki typu wiele-do-wielu . Moglibyśmy na przykład korzystać z typu encji reprezentującego jakąś standardową część taką jak śruba M3 oraz encji reprezentującej wytwórcę części . Wielu producentów jest w stanie dostarczać śruby M3, a większość z wytwórców produkuje również coś więcej niż tylko jeden rodzaj produktów. Aby zamodelować w EDM związek występujący pomiędzy tym, co jest produkowane, oraz tym, kto to coś produkuje, można by użyć krotności * po obydwu stronach asocjacji. W kodzie zaś obydwie encje miałyby w takim przypadku właściwości nawigacji oferujące kolekcje obiektów. Jednak ze związkami typu wiele-do-wielu wiąże się w mechanizmie EF pewien problem. W bazie danych związek tego rodzaju jest reprezentowany przez oddzielną tabelę, w której każdy wiersz zawiera dwa klucze obce, po jednym dla każdej ze stron związku. Jeśli jest to wszystko, co zawiera tabela, mechanizm EF szczęśliwie umożliwi nam odwzorowanie tej tabeli w asocjacje 4

Yoda opisujący warunki zatrudnienia Sithów w filmie Gwiezdne wojny, część I: Mroczne widmo .

5

Opinie na temat tego, czy związki takie można nadal nazywać związkami jeden-do-jednego, są podzielone. Ściśle rzecz biorąc, nazywanie ich w ten sposób jest niepoprawne, jednak przekonamy się, że w praktyce związki jeden-do-jednego-lub-zera są powszechnie - choć nieformalnie - określane mianem związków jeden-do-jednego .

564

I

Rozdział 14. Bazy danych

w modelu koncepcyjnym, a właściwości nawigacji będą działały tak, jak zostało to powyżej opi­ sane. Jeśli jednak tabela ta zawiera jakieś inne dane, będziemy zmuszeni do zapewnienia jej własnej reprezentacji w postaci oddzielnej encji . Weźmy pod uwagę przedstawiony wcześniej przykład z produktem i wytwórcą. W jego przypadku przydatna może się okazać wiedza na temat tego, jakiego kodu produktu używa określony wytwórca do oznaczania pewnego pro­ duktu standardowego. Informacji tej nie ma gdzie zapisać, jeśli do dyspozycji mamy jedynie właściwości nawigacyjne związane z produktem i producentem, które nawzajem wskazują te obiekty, będzie zatem potrzebny dodatkowy typ encji do przechowywania tej właściwości, która jest specyficzna dla określonej kombinacji produktu i wytwórcy. Może się to stać dość niewygodne, gdy w tabeli związku istnieją kolumny, które nieszczegól­ nie interesują naszą aplikację, lecz na których odwzorowanie nalega mechanizm EF, ponieważ nie dopuszczają one wartości pustych i nie mają swoich wartości domyślnych. Model koncep­ cyjny nie będzie w stanie reprezentować tych tabel jako prostych asocjacji typu wiele-do-wielu, ponieważ rozwiązanie takie nie pozostawi miejsca dla odwzorowania właściwości związku. (U podstaw tego zjawiska leży ten sam problem, który uniemożliwia pominięcie niektórych kolumn w encjach) . Na koniec przyjrzymy się jeszcze jednej możliwości związanej z odzwierciedlaniem przepro­ wadzanym za pomocą mechanizmu Entity Framework: obsłudze dziedziczenia.

Dziedziczen ie Dziedziczenie stanowi dla mechanizmu ORM pewien problem, ponieważ pojęcie to, typowe dla świata zorientowanego obiektowo, nie ma żadnego bezpośredniego odpowiednika w modelu relacyjnym. Istnieją różne rozwiązania, gdyż nie ma jednej naprawdę dobrej metody poradze­ nia sobie z tym kłopotem. Mechanizm Entity Framework zapewnia obsługę odzwierciedlania dla kilku najbardziej popularnych sposobów, za pomocą których można próbować pokonać tę przepaść. Choć istnieje kilka różnych podejść do kwestii odzwierciedlania (którymi zajmiemy się już niebawem), sposób obsługi dziedziczenia w modelu koncepcyjnym jest taki sam we wszystkich tych przypadkach i bardzo przypomina dziedziczenie w języku C#. Każdy typ encji może opcjonalnie wskazać jeden inny typ encji jako swój typ bazowy. Encje posiadające typ bazowy dziedziczą po nim wszystkie właściwości. Dla encji nie można wskazać więcej niż jednego typu bazowego, możliwe jest jednak dziedziczenie po encji, która dziedziczy po innej encji (co oznacza, że można tworzyć łańcuchy dziedziczenia) . A odpowiadające im wygenerowane klasy encji, których używamy z poziomu kodu C#, będą reprezentowały te związki dziedziczenia za pomocą zwykłego dziedziczenia klas. Dla swoich bazowych typów encji będziemy musieli zdefiniować odzwierciedlenia w zwykły sposób. Wszystkie typy pochodne będą dziedziczyły te odzwierciedlenia . Powstaje tu jednak pytanie: w jaki sposób należy odzwierciedlić cechy unikatowe dla poszczególnych typów pochodnych? Pierwsza metoda odzwierciedlania wiąże się z odzwierciedlaniem wszystkich typów encji współdzielących określony bazowy typ encji w pojedynczą tabelę bazy danych. Typ encji wybrany przez mechanizm EF do reprezentowania określonego wiersza jest wskazywany na podstawie kolumny dyskryminatora (ang. discriminator) w odzwierciedleniu zapewniamy po prostu odpowiednią listę, która określa na przykład, że gdy kolumna dyskryminatora -

Model encji danych

I

565

zawiera wartość 1, typem encji jest Emp l oyee, gdy wartość ta to 2, typem jest Manager, gdy zaś wartość wynosi 3, typem jest D i rector, i tak dalej . Te typy pochodne będą prawdopodobnie miały dodatkowe właściwości odróżniające je od pozostałych, one zaś zostaną odzwierciedlone w kolumny tabeli przyjmujące wartości puste. Muszą one akceptować wartości puste, ponie­ waż kolumny te będą zawierały wartości wyłącznie wtedy, gdy będziemy korzystali z typów pochodnych, które je obsługują - należące do tabel bazy danych kolumny niedopuszczające wartości pustych muszą zostać odzwierciedlone we właściwości należące do bazowego typu encji, jeśli używamy tego stylu odzwierciedlania. Druga metoda odzwierciedlania wykorzystuje oddzielną tabelę dla każdego typu pochodnego. Typy pochodne nadal dziedziczą bazowe odzwierciedlenia, dlatego w przypadku tego roz­ wiązania pochodne typy encji będą związane z dwoma tabelami lub większą ich liczbą: tabelą unikatową dla typu pochodnego oraz wszystkimi innymi tabelami używanymi przez typ bazowy. Aby dało się zastosować ten sposób, wszystkie zaangażowane tabele muszą korzystać z tego samego klucza głównego. Żadne z tych możliwości odzwierciedlenia nie będą szczególnie użyteczne, gdy nie będziemy dysponowali jakimś sposobem pobierania danych z bazy, dlatego teraz przyjrzymy się temu, jak w ramach mechanizmu Entity Framework wykonuje się zapytania .

Zapytania Przedstawiliśmy już kilka prostych przykładów zastosowania technologii LINQ do pobierania danych z bazy za pośrednictwem mechanizmu Entity Framework. EF w tle przetwarza zapytanie LINQ w zapytanie SQL, które jest zrozumiałe dla bazy danych. W rzeczywistości istnieją dwa sposoby zmuszenia mechanizmu EF do odpytania bazy danych: jednym z nich jest LINQ, a dru­ gim coś, co określa się mianem Entity SQL. Prezentowaliśmy już kilka prostych przykładów zastosowania rozwiązania LINQ to Entities, teraz jednak przyjrzymy się im nieco dokładniej .

LI NQ to Entities Dostawca LINQ dla mechanizmu Entity Framework, LINQ to Entities, obsługuje wszystkie standardowe operatory LINQ, które zostały przedstawione w rozdziale 8., działa jednak nieco odmiennie. Idea opóźnionego wykonania (ang. deferred execution) jest tu nadal obecna, a nawet staje się ważniejsza . Chwila, w której powodujemy, że zapytanie LINQ zostaje wykonane moment, w którym po raz pierwszy próbujemy w swoim kodzie skorzystać z jego wyników jest chwilą, w której mechanizm EF musi wysłać żądanie do bazy danych. Wynika stąd, że frag­ ment kodu z listingu 14.3 przedstawiony na listingu 14.9 nie pobiera żadnych danych z bazy.

Listing 14.9. Proste wyrażenie zapytania LINQ to Entities var orders

=

from order i n dbContext . Sal esOrderHeaders where orde r . OrderDate == orderDate sel ect orde r ;

Jak zawsze w przypadku technologii LINQ, wyrażenie zapytania jedynie definiuje zapytanie orders to obiekt, który „wie", co powinien zwrócić, gdy zostanie podjęta próba skorzystania z niego. Wysłanie samego żądania do bazy danych w kodzie przedstawionym na listingu 14.3 powoduje zatem dopiero pętla foreac h .

566

I

Rozdział 14. Bazy danych

Sposób, w jaki mechanizm EF przetwarza żądanie, różni się od tego, jak działa rozwiązanie LINQ to Objects. Praca LINQ to Objects polega na tworzeniu łańcucha operatorów działających sek­ wencyjnie - kolekcja źródłowa mogłaby przejść przez operator Where, po niej zaś następo­ wałby na przykład operator OrderBy lub Group. Operator W here w rozwiązaniu LINQ to Objects pracuje w taki sposób, że przechodzi przez każdy element należący do źródła, odrzucając te, które nie pasują do kryterium filtrowania, te zaś, które do niego pasują, przechodzą do następ­ nego elementu w łańcuchu. Naprawdę nie chcemy, aby kod dostępu do danych działał w ten sposób, a - jak już wspomnie­ liśmy wcześniej - mechanizm EF pozostawia filtrowanie bazie danych, co jest rozwiązaniem znacznie bardziej wydajnym od pobierania całej tabeli, a następnie filtrowania jej elementów w kodzie programu. Sprawdzimy teraz, czy EF naprawdę działa w taki sposób, korzystając z narzędzia SQL Profiler, aby przekonać się na własne oczy, co mechanizm ten dla nas robi . '

. .

Narzędzie SQL Profiler nie stanowi części składowej systemu SQL Server 2008 Express; jest tak nawet wtedy, gdy zainstalujemy wersję rozwiązania zapewniającą zaawan­ sowane usługi i Management Studio. Aby móc z niego skorzystać, będziemy potrze­ bowali pełnej wersji SQL Servera (wystarczy edycja Developer). Program SQL Profiler współpracuje bez kłopotu z bazą danych w wersji Express, jednak jest dystrybu­ owany oraz licencjonowany wyłącznie jako część pełnych edycji. Jeśli Czytelnik jest posiadaczem odpowiedniej licencji, może zainstalować tylko narzędzia pochodzące z pełnej edycji SQL Servera na maszynie, na której obecna jest jedynie wersja Express systemu bazy danych, i rozwiązanie takie powinno sprawdzić się doskonale. (Nie­ stety, jeśli zainstalowaliśmy już wcześniej wersję Express oprogramowania Manage­ ment Studio, nie będziemy w stanie zainstalować pełnej wersji narzędzi do zarządza­ nia na tym samym komputerze) . Pełny opis narzędzia SQL Profiler wykracza poza zakres tematów poruszanych w tej książce - używamy go tu jedynie po to, aby zaprezentować, o co dokładnie mecha­ nizm Entity Framework poprosił bazę danych. Jest to jednak naprawdę przydatne narzędzie; przekonamy się o tym nawet wtedy, gdy będziemy korzystać z niego w bardzo prosty sposób jedynie po to, aby dowiedzieć się, jakie zapytania SQL są wyko­ nywane. Jeśli planujemy intensywnie używać baz danych, z pewnością warto dokład­ niej poznać jego działanie i nauczyć się jego obsługi.

Wykonując krok po kroku nasz kod w środowisku Visual Studio przy uruchomionym narzędziu SQL Profiler, możemy zauważyć, że w oknie programu nie pojawia się nic aż do momentu, w którym rozpoczyna się działanie pętli foreac h . Wtedy to profiler wyświetla komunikat Audi t Log i n, informując, że nasza aplikacja otworzyła właśnie połączenie z bazą danych. Po nim poja­ wia się komunikat RPC : Camp l eted, co wskazuje, że SQL Server przetworzył żądanie. Gdy zazna­ czymy ten komunikat, profiler wyświetli zapytanie SQL, które zostało dla nas wykonane przez mechanizm EF: exec sp executesql N ' SE LECT [Extentl] . [Sal esOrderID] AS [Sal esOrde r I D] , [Extent l] . [Rev i s i onNumber] AS [Rev i s i onNumber] , [Extent l] . [OrderDate] AS [OrderDate] , [Extent l] . [DueDate] AS [DueDate] , [Extent l] . [S h i pDate] AS [Sh i pDate] , [Extent l] . [Status] AS [Status] , [Extent l] . [Onl i neOrderFl ag] AS [Onl i neOrderFl ag] , [Extent l] . [Sal esOrderNumber] AS [Sal esOrderNumber] , [Extent l] . [PurchaseOrderNumber] AS [PurchaseOrderNumber] , [Extent l] . [AccountNumber] AS [AccountNumber] ,

Zapytania

I

567

[Extent l] . [Cus tome r I D] AS [Customer I D] , [Extent l] . [S h i pToAddres s I D] AS [Shi pToAddres s I D] , [Extent l] . [B i l l ToAddres s I D] AS [Bi l l ToAddres s I D] , [Extent l] . [S h i pMethod] AS [Sh i pMethod] , [Extent l] . [Cred i tCardApproval Code] AS [Cred i tCardApproval Code] , [Extent l] . [SubTotal ] AS [SubTotal ] , [Extent l] . [TaxAmt] AS [TaxAmt] , [Extent l] . [ Fre i ght] AS [ Fre i ght] , [Extent l] . [Total Due] AS [Total Due] , [Extent l] . [Comment] AS [Comment] , [Extent l] . [rowg u i d] AS [rowgu i d] , [Extent l] . [Mod i fi edDate] AS [Mod i fi edDate] FROM [Sal es LT] . [Sal esOrderHeader] AS [Extent l] WHERE [Extent l] . [OrderDate] = @p�l i nq�O ' , N ' @p�l i nq�O date t i me ' , @p�l i nq�0= ' 2004-06-01 00 : 00 : 00 '

Zapytanie to może się wydawać dość długie, ale ze strukturalnego punktu widzenia jest to stosunkowo prosta instrukcja S E L E CT . Jedynym powodem, dla którego jest ona tak obszerna, jest to, że jawnie wymienione są w niej wszystkie kolumny wymagane przez encję (a także to, że każda z kolumn została wskazana w dość rozwlekły sposób) . Najciekawszą częścią są tu dwa ostatnie wiersze. Przedostatni wiersz to sparametryzowana klauzula W H E R E porównująca wartość OrderDate z wartością argumentu nazwanego. Właśnie w to została zmieniona klauzula where naszego zapytania LINQ. Ostatni wiersz zapewnia z kolei odpowiednią wartość argu­ mentu nazwanego. Zwróćmy uwagę na fakt, że w ramach mechanizmu LINQ to Entities możemy swobodnie łączyć operatory w łańcuch, podobnie jak mogliśmy to robić w przypadku LINQ to Objects. Zatem zapytanie dla przedstawionego na listingu 14.3 obiektu orders moglibyśmy zbudować na przykład w następujący sposób: var orderedOrders = orders . OrderBy (order => order . OrderDate) ;

Jeśli wolimy trzymać się składni LINQ, możemy też opracować zapytanie zaprezentowane na listingu 14.10 będące odpowiednikiem powyższego.

Listing 14.10. Zapytanie łańcuchowe var orderedOrders = from order i n orders orderby orde r . OrderDate sel ect orde r ;

Instrukcja ta nie powoduje wykonania zapytania związanego z obiektem orders . Oznacza jedynie, że mamy teraz dwa zapytania - zapytanie orders, które tylko filtruje dane, oraz zapytanie orderedOrders, które je filtruje, a następnie sortuje. Zapytanie łańcuchowe możemy traktować jako skrótową formę zapytania przedstawionego na listingu 14.11, które jawnie łączy klauzule zaprezentowane na listingach 14.9 i 14.10 w jedno zapytanie.

Listing 14. 1 1 . To samo zapytanie w pełnej postaci var orderedOrders = from order i n dbContext . Sal esOrderHeaders where orde r . OrderDate == orderDate orderby orde r . OrderDate sel ect orde r ;

Niezależnie od tego, z którego ze sposobów skorzystamy w celu zbudowania drugiego zapyta­ nia, wynikiem wykonania go (na przykład poprzez iterację w pętli foreach ) będzie - jak z pew­ nością się spodziewamy - zapytanie SQL, które zawiera klauzulę O RD E R BY, jak również klau­ zulę W H E R E . (W naszym przypadku rozwiązanie to nie jest szczególnie użyteczne, ponieważ w tej 568

I

Rozdział 14. Bazy danych

przykładowej bazie danych wszystkie zamówienia zostały złożone dokładnie tego samego dnia, jednak gdybyśmy dysponowali nieco bardziej realnymi danymi, uzyskalibyśmy w ten sposób spodziewany efekt) . Wynika stąd, że zapytania LINQ to Entities działają w zupełnie inny sposób niż zapytania LINQ to Objects, z którymi mieliśmy do czynienia wcześniej. W przypadku mechanizmu LINQ to Objects wyrażenie należące do klauzuli where jest po prostu zakamuflowaną delegacją jest więc metodą, którą wywołuje operator Where dla każdego elementu w celu sprawdzenia, czy powinien się on znaleźć w wynikach zapytania. Jednak w przypadku LINQ to Entities (jak również LINQ to SQL) klauzula wh ere zapytania LINQ została przetłumaczona na konstrukcję T-SQL i wysłana do bazy danych. Wyrażenie, które zapisaliśmy w języku C#, zostaje tu w efek­ cie wykonane w zupełnie innym języku, prawdopodobnie również na zupełnie innej maszynie. Jeśli Czytelnik chce dowiedzieć się nieco więcej na temat tego, jak te dwa rodzaje zapytań są w stanie działać w tak zupełnie odmienny sposób dla różnych dostawców, powinien zapoznać się z treścią kolejnej ramki. Występująca tu translacja jest oczywiście bardzo użyteczna, jeśli chodzi o przerzucenie pracy na serwer bazy danych, wiążą się z nią jednak pewne ograniczenia. Próba dodania wywołań dowol­ nych metod w środku zapytania LINQ w przypadku mechanizmu LINQ to Entities zakończy się źle . Załóżmy na przykład, że mamy do dyspozycji następującą metodę pomocniczą: s t at i c DateT i me NextDay (DateT i me dt) { return dt + Ti meSpan . FromDays ( l ) ;

Moglibyśmy spróbować z niej skorzystać w zapytaniu LINQ w następujący sposób: var orders

=

from order i n dbContext . Sal esOrderHeaders where order . OrderDate == NextDay (orderDate)

sel ect orde r ;

W przypadku mechanizmu LINQ to Objects rozwiązanie to zadziałałoby bez kłopotu mamy tu do czynienia wyłącznie z kodem C#, a w obrębie klauzuli where można zastosować dowolne prawidłowe wyrażenie logiczne, w tym również takie, w których pojawiają się wywo­ łania metod. Jednak w przypadku LINQ to Entities, mimo że kod ten zostanie skompilowany poprawnie, mechanizm EF zgłosi wyjątek NotSupportedExcept i on w chwili, gdy zostanie podjęta próba wykonania tego zapytania. Związany z nim komunikat błędu będzie miał następującą postać: L I NQ to Ent i t i es does not recog n i ze the method ' System . DateT i me NextDay (System . DateTi me) ' method , and th i s method cannot be tran s l ated i nto a s tore express i on° .

Zapytania LINQ to Entities mogą obejmować jedynie takie rzeczy, które mechanizm EF jest w stanie przetworzyć w zapytania bazy danych, a z racji tego, że nie ma on pojęcia, jak poradzić sobie z napisaną przez nas metodą N ext Day, nie może przetłumaczyć jej wywołania w odpo­ wiednie zapytanie. Oczywiście gdy będziemy pamiętali, że zapytanie LINQ to Entities jest wykonywane w bazie danych, nie powinno nas ani trochę zdziwić, że w naszej aplikacji nie da się wywoływać dowolnych metod z wnętrza takiego zapytania. Jednak mechanizm EF radzi sobie z integracją niektórych możliwości baz danych z naszym kodem w sposób tak doskonały, że czasem łatwo jest zapomnieć, gdzie leży granica pomiędzy naszą aplikacją i bazą danych. 6

LINQ to Entities nie rozpoznaje metody System. DateTi me NextDay (System . DateT i me) i nie może ona zostać prze­ tłumaczona w wyrażenie składowania przyp. tłum. -

Zapytania

I

569

Delegacje funkcj i kontra wyrażenia Dostawcy bazy danych LINQ są w stanie tłumaczyć zapytania LINQ na odpowiednie konstrukcje SQL, ponieważ należące do nich wersje operatorów LlNQ wykorzystują możliwość wprowadzoną w języku C# 3.0 właśnie po to, aby zapewnić obsługę tego rodzaju działań. Gdy porównamy dekla­ rację operatora where właściwą dla mechanizmu LINQ to Entities z jej odpowiednikiem związanym z mechanizmem LINQ to Objects, bez trudu zauważymy różnicę. Jak przekonaliśmy się w roz­ dziale 8., technologia LINQ to Objects została zaimplementowana jako zbiór metod rozszerzeń dla interfejsu I Enumerabl e zdefiniowanego przez typ Enumerabl e w przestrzeni nazw System. Li ną. Jego operator Where został zadeklarowany w następujący sposób: publ i c s t at i c I Enumerabl e Where ( th i s I Enumerabl e source , Func predi cate) Mechanizm LINQ to Entities działa podobnie, ale wszystkie metody rozszerzeń należą do interfejsu I Queryabl e. Z racji tego, że wszystkie właściwości oferowane przez kontekst obiektu w celu zapew­ nienia dostępu do tabel implementują interfejs I Queryabl e, będziemy używali właśnie jego metod rozszerzeń zamiast tych, które związane są z mechanizmem LINQ to Objects. Zostały one zdefinio­ wane przez typ Queryabl e, który również należy do przestrzeni nazw System. Li ną. Oto jego definicja operatora Where: publ i c s t at i c I Queryabl e Where ( th i s I Queryabl e source , Expressi on predi cate) Rzecz jasna, typem pierwszego parametru jest tutaj I Queryabl e, inny jest jednak również typ para­ metru drugiego - zamiast przyjmować dla funkcji predykatu delegację, operator przyjmuje teraz wyrażenie. Typ Expressi on jest specjalnym typem rozpoznawanym przez kompilator języka C# (a także przez kompilator VB.NET). Wywołując metody spodziewające się argumentów typu Expressi on t . Start () ) ; threads . ForEach (t => t . Jo i n () ) ;

Tworzy on cztery wątki, z których każdy będzie wykonywał metodę Go. Następnie dla każ­ dego wątku jest wywoływana metoda Start . Po uruchomieniu wątków dla każdego z nich wywoływana jest metoda Joi n w celu oczekiwania na zakończenie ich wszystkich. Mogliśmy w tym miejscu zastosować cztery pętle, jednak użycie LINQ oraz wyrażeń lambda pozwoliło znacznie skrócić kod i poprawić jego czytelność. Zastosowanie metody ForEach klasy L i st jest rozwiązaniem znacznie bardziej zwartym i przejrzystym niż pętla forea c h, szczególnie jeśli dla każdego elementu listy chcemy wykonać tylko jedną operację. Kod używany do uruchomie­ nia jednowątkowej wersji przykładu jest znacznie prostszy: Count = O ; GoS i ng l e (4) ;

Wyniki zwracane w obu przypadkach są takie same - pole Count zawiera wartość 400000000 jednak kod wielowątkowy jest wykonywany znacznie wolniej . Jedną z przyczyn jest różnica w sposobie inkrementacji wartości pola Coun t . Oto, w jaki sposób robimy to w wersji jedno­ wątkowej: Count += l ;

632

I

Rozdział 16. Wątki i kod asynchroniczny

Jeśli jednak spróbujemy użyć tego samego sposobu w wersji wielowątkowej, okaże się, że nie działa ona prawidłowo. Oczywiście w wersji tej program działa szybko i sprawnie - blisko trzy razy szybciej niż wersja jednowątkowa - jednak kilkukrotna próba wykonania go poka­ zuje, że pole Count uzyskuje na końcu wartości takie jak: 1 1 0460 1 5 1 , 1 3 3 5 3 3 5 0 3 albo 1 3 3 888803 Jak widać, większa część operacji inkrementacji została utracona - tak właśnie się dzieje, kiedy podczas korzystania ze współużytkowanych informacji nie zostaną zastosowane odpo­ wiednie zabezpieczenia. I właśnie z tego powodu w wielowątkowej wersji naszego rozwiąza­ nia musimy zastosować następujące wywołanie: . . .

I n terl ocked . I n crement (ref Count) ;

Klasa I nterl oc ked należąca do przestrzeni nazw System . Threadi ng udostępnia metody pozwalające na wykonywanie pewnych prostych czynności w prawidłowy sposób, nawet jeśli większa liczba wątków próbuje je wykonywać w tym samym czasie. Zgodnie z tym, co sugeruje nazwa metody I n crement, inkrementuje ona wartość zmiennej, przy czym robi to w sposób, który blokuje wszelkie inne procesory logiczne próbujące wykonać tę samą czynność w tym samym czasie. Zmusza to procesory logiczne do wykonywania operacji inkrementacji jeden po drugim. To rozwiązanie działa dobrze - po dodaniu tego kodu wartość pola Count jest prawidłowa jednak ma ono swój koszt. Na komputerze z czterordzeniowym procesorem, przy stuprocen­ towym wykorzystaniu wszystkich czterech rdzeni, jego wykonanie trwa 15 razy dłużej niż wykonanie prostej jednowątkowej wersji. W rzeczywistości koszty użycia metody I nt erl ocked . I n c remen t nie stanowią jednak pełnego wytłumaczenia różnic pomiędzy oboma rozwiązaniami. Zastosowanie tej samej metody w wer­ sji jednowątkowej sprawia, że wersja ta działa pięć razy wolniej, ale i tak jest to trzy razy szybciej niż w przypadku wersji wielowątkowej . A zatem przyczyną znacznej części spowolnienia jest tu konieczność komunikacji pomiędzy procesorami logicznymi . Powyższych liczb nie należy traktować zbyt poważnie - jest to sztuczny, wymyślony przykład. (Gdybyśmy chcieli wykonać nasz program naprawdę szybko, wystarczyłoby zainicjalizować pole Count wartością 400000000 i usunąć pętlę) . Jednak choć szczegóły są fałszywe, to ogólny wniosek jest słuszny i ma szerokie zastosowanie: koszt rywalizacji pomiędzy procesorami logicz­ nymi, które powinny współpracować ze sobą, może działać na naszą niekorzyść. Czasami koszt ten jedynie zmniejsza korzyści, jakie niesie ze sobą realizacja współbieżna - zastosowanie procesora czterordzeniowego może przynieść nam na przykład jedynie 2,5-krotne zwiększenie szybkości działania . Jednak zdarza się także, że wszelkie potencjalne korzyści zostają całko­ wicie zaprzepaszczone - to fakt, że powyższy przykład został wymyślony, jednak znacznie gorsze rozwiązania pojawiają się w rzeczywistych systemach. '

. .

,

..___�_, '

Czasami pewne implementacje mogą działać gorzej w jednych systemach, a lepiej w innych. Na przykład niektóre algorytmy równoległe działają znacząco gorzej od swych sekwencyjnych odpowiedników, lecz w systemach wieloprocesorowych zapewniają możliwość skalowania. Zastosowanie takiego algorytmu może mieć sens wyłącznie w komputerach wyposażonych w większą liczbę procesorów - na komputerach z jednym procesorem lub procesorem dwurdzeniowym może on działać wolniej od swego jednowątkowego odpowiednika, natomiast na komputerze z 16 procesorami logicznymi może działać wielokrotnie szybciej .

Wniosek z analizy powyższych rozwiązań jest taki, że jeśli chcemy dowiedzieć się, czy rozwią­ zanie równoległe jest wydajne, musimy je porównać z rozwiązaniem jednowątkowym, urucha­ miając oba na tym samym komputerze. Sam fakt, że obciążenie procesora wskazuje na pełne Wątki

I

633

działanie współbieżne, wcale nie oznacza, że jest ono naprawdę szybkie. Co więcej - z wyjąt­ kiem przypadków, gdy nasze oprogramowanie będzie działać w jednej konkretnej konfigu­ racji sprzętowej - konieczne będzie dokonanie tego samego porównania na wielu różnych komputerach, gdyż tylko w ten sposób będziemy mogli wyrobić sobie odpowiednią opinię na temat tego, czy rozwiązanie równoległe jest faktycznie lepsze.

Tworzenie kodu wielowątkowego jest trudne Choć kod wielowątkowy zapewnia zauważalne korzyści pod względem wydajności działa­ nia, to jednak napisanie takiego kodu w prawidłowy sposób jest bardzo trudnym zadaniem. Mieliśmy już okazję zobaczyć kilka dziwacznych zachowań w bardzo prostych przykładach. Zapewnienie poprawnego działania w rzeczywistym systemie wielowątkowym może stano­ wić ogromne wyzwanie. Dlatego też przyjrzymy się dwóm klasom najczęściej pojawiających się problemów, a dopiero potem przedstawimy niektóre strategie ich unikania.

Wyścig Wszystkie nieprawidłowości, z którymi spotkaliśmy się do tej pory, były przykładami zagro­ żenia związanego z działaniem współbieżnym określanego jako wyścig (ang. race) . Jego nazwa bierze się stąd, że wynik zależy od tego, który z uczestników procesu dotrze do określonego miejsca jako pierwszy. Program z listingu 16.1 za każdym razem generuje inne wyniki, gdyż wszystkie trzy uruchamiane w nim wątki starają się wyświetlać komunikaty tekstowe w jednym oknie konsoli, a jedynym czynnikiem określającym, który z nich wyświetli swój wiersz, jest to, któremu szybciej uda się wywołać metodę Consol e . Wr i t e l i n e3 . Wątki w żaden sposób nie koordynują swojego działania, zatem wszystkie starają się wykonywać swoje operacje w tym samym czasie . Pod pewnymi względami przykład ten jest całkiem skomplikowany, gdyż znaczna część wyścigu ma miejsce tam, gdzie nie możemy go zobaczyć, czyli w metodzie Con sol e . Wri t e l i n e, której realizacja zajmuje najwięcej czasu. Znacznie łatwiej jest zrozumieć wyścig w przypadkach, gdy możemy zobaczyć cały związany z nim kod. A zatem przeanalizujmy błędną wersję programu z listingu 16.8, w której współbieżnie wyko­ nywana metoda Go używa instrukcji Count += 1 zamiast metody I nterl ocked . I ncrement. Prze­ konaliśmy się już, że korzystanie z operatora += prowadzi do utraty operacji inkrementacji, ale dlaczego tak się dzieje? Otóż operator + = musi wykonać trzy operacje: odczytać aktualną wartość pola Count, zwiększyć ją o jeden, a następnie zapisać wynik ponownie w polu. Układy scalone pamięci RAM nie dysponują możliwością wykonywania operacji, zatem w żaden spo­ sób nie da się uniknąć konieczności przeniesienia wartości z pamięci do procesora, gdzie będzie można ją zmodyfikować, a następnie przeniesienia jej z powrotem do pamięci, tak by nie została zapomniana . Zawsze trzeba będzie wykonać operację odczytu, a następnie zapisu. Zastanówmy się teraz, co się dzieje, gdy dwa wątki starają się inkrementować wartość tego samego pola Coun t . Nadajmy im odpowiednio nazwy wątek A oraz wątek B. Jedna z możliwych sekwencji zdarzeń została przedstawiona w tabeli 1 6 . 1 . W tym przypadku wszystko idzie dobrze: pole Count, które początkowo ma wartość O, jest dwukrotnie inkrementowane i w efekcie przyjmuje wartość 2 .

3

A precyzyjnie rzecz ujmując, któremu jako pierwszemu uda się uzyskać blokadę używaną wewnętrznie przez metodę Conso l e . Wri tel i ne do synchronizacji dostępu do konsoli.

634

I

Rozdział 16. Wątki i kod asynchroniczny

Tabela 16.1 . Dwie operacje inkrementacji wykonywane jedna po drugiej Pole Count

Wątek A

o

Odczyt

o

Dodanie 1

Wątek B

Count (O) (o + 1

Zapis Count

=

1)

( 1) Odczyt

Count ( 1 )

Dodanie 1

2

Zapis

(1 + 1

=

2)

Count ( 2 )

Jednak nie zawsze wszystko będzie przebiegać tak dobrze. Tabela 16.2 pokazuje, co się stanie, jeśli czynności wykonywane przez oba wątki będą częściowo wykonywane w tym samym czasie, co bez problemu może się zdarzyć w przypadku realizowania ich na dwóch procesorach logicznych. Wątek B odczytuje bieżącą wartość Count w chwili, gdy wątek A już częściowo wyko­ nał operację inkrementacji. Wątek B nie może wiedzieć, że w czasie pomiędzy momentem odczytania przez niego zawartości pola Count i zapisania jego nowej wartości wątek A zapisał swoją nową wartość Coun t . Oznacza to, że zapisanie wartości Count przez wątek B spowoduje utratę wartości wyliczonej przez wątek A.

Tabela 16.2 . Utrata inkrementacji ze względu na częściowe nakładanie się procesów na siebie Pole Count

Wątek A

o

Odczyt

o

Dodanie 1

Wątek B

Count (O) (o + 1

Zapis Count

( 1)

=

1)

Odczyt

Count (o)

Dodanie 1 Zapis

(o + 1

=

1)

Count ( 1 )

Takich różnych wariacji kolejności wykonywania operacji może być bardzo wiele; niektóre z nich będą dawały prawidłowe wyniki, inne nie. Jeśli istnieje możliwość wykonania kodu w kolejności, w której zwróci on nieprawidłowe rezultaty, to prędzej czy później się to stanie . Nie należy łudzić się nadzieją, że wysoce nieprawdopodobna sytuacja w praktyce nie może się zdarzyć. Takie nadzieje są oszukiwaniem samego siebie - wcześniej czy później problem się ujawni. Jedyna różnica związana z problemami wysoce niepraw­ dopodobnymi jest taka, że ich zdiagnozowanie i poprawienie jest niesłychanie trudne.

Przedstawiony tu przykład jest wyjątkowo prostym przypadkiem wyścigu. W rzeczywistym kodzie sprawy zazwyczaj są znacznie bardziej skomplikowane, gdyż najprawdopodobniej będziemy operowali na strukturach danych o wiele bardziej złożonych niż pojedyncza liczba całkowita. Jednak ogólnie rzecz biorąc, jeśli dysponujemy informacją dostępną dla wielu wąt­ ków i jeśli przynajmniej jeden z nich ją w jakikolwiek sposób zmienia, to o ile nie podejmiemy odpowiednich działań zapobiegawczych, zapewne dojdzie do wyścigu. Rozwiązanie dla sytuacji wyścigu jest stosunkowo oczywiste: wątki muszą być wykonywane po kolei . Gdyby wątki A i B koordynowały wykonywane przez siebie operacje, tak by każdy z nich czekał na ten drugi, jeśli wykonuje on operację inkrementacji, moglibyśmy uniknąć problemu. Dokładnie tym zajmuje się metoda I nterl ocked . I n crement, choć jest ona bardzo wyspe­ cjalizowana. Mogą się jednak pojawić sytuacje, w których będziemy musieli wykonać czyn­ ności bardziej złożone od inkrementacji wartości pola . Z myślą o nich .NET udostępnia zbiór

Wątki

I

635

mechanizmów synchronizacyjnych, których można użyć do wymuszenia realizacji wątków w odpowiedniej kolejności. Już niebawem opiszemy je znacznie dokładniej, trzeba jednak wie­ dzieć, że takie rozwiązanie może stać się przyczyną problemów zaliczanych do zupełnie innej kategorii .

Zakleszczenia i uwięzienia Kiedy kod czeka, by nie deptać innemu po piętach, istnieje możliwość zablokowania działania aplikacji, to znaczy wszystkie jej wątki mogą zostać zablokowane w oczekiwaniu na zakoń­ czenie innych. Zazwyczaj nie dzieje się tak w przypadku wykonywania krótkich operacji na pojedynczym elemencie danych. Przeważnie sytuacje tego typu występują, gdy wątek posiada­ jący wyłączny dostęp do pewnych danych oczekuje na uzyskanie dostępu do kolejnego zasobu. Standardowym przykładem takiej blokady jest przekazywanie pieniędzy pomiędzy dwoma kontami bankowymi . Nazwijmy je kontami X i Y. Wyobraźmy sobie dwa wątki, A oraz B, z których każdy stara się wykonać przelew z jednego z tych kont na drugie - wątek A prze­ kazuje pieniądze z konta X na Y, a wątek B z konta Y na X . Oba wątki będą musiały skorzystać z jakiegoś mechanizmu synchronizacji, by uzyskać wyłączny dostęp do obu kont i uniknąć dzięki temu wystąpienia opisywanego wcześniej zjawiska wyścigu (ang. race) . Jednak wyobraźmy sobie, że zachodzi następująca sekwencja zdarzeń: 1.

Początkowo żaden z wątków nie próbuje wykonywać żadnych czynności na żadnym koncie .

2 . Wątek A uzyskuje wyłączny dostęp do konta X . 3.

4. 5.

Wątek B uzyskuje wyłączny dostęp d o konta Y . Wątek A próbuje uzyskać wyłączny dostęp d o konta Y, jednak jest to niemożliwe, gdyż posiada go wątek B. Wątek A zaczyna więc czekać, aż wątek B zwolni konto Y. Wątek B próbuje uzyskać wyłączny dostęp do konta X, jednak jest to niemożliwe, gdyż posiada go wątek A. Wątek B zaczyna więc czekać, aż wątek A zwolni konto X.

Szczegóły działania mechanizmu stosowanego do uzyskania wyłącznego dostępu nie są przy tym istotne, gdyż niezależnie od nich wynik i tak zawsze jest taki sam. Wątek A oczekuje, aż wątek B zwolni dostęp do konta Y, jednak B nie ma zamiaru tego zrobić, póki nie uzyska dostępu do konta X. Niestety nie uzyska go, gdyż jest on w posiadaniu wątku A, który został zatrzymany. Żaden z wątków nie jest w stanie kontynuować działania, gdyż oczekuje na drugi. Sytuacja taka jest zazwyczaj nazywana blokadą wzajemną, a rzadziej także śmiertelnym uściskiem (ang. deadly embrace) . Takie sytuacje mogą prowadzić do zakleszczenia (ang. deadlock) lub uwięzienia (ang. livelock), przy czym ich rozróżnienie bazuje na mechanizmie używanym do zarządzania wyłącznym dostępem. Jeśli blokada wzajemna nastąpiła w sytuacji, gdy oba wątki oczekiwały na uzyskanie dostępu, to w momencie jej wystąpienia żaden z nich nie znajdował się w stanie umożliwia­ jącym jego wykonywanie . Taka sytuacja jest zazwyczaj określana jako zakleszczenie, a jej objawem jest to, że system, który ma operacje do wykonania, pozostaje bezczynny. Uwięzie­ nie jest dosyć podobne, jednak jest związane ze stosowaniem mechanizmów synchronizacji, które podczas oczekiwania zajmują procesor. Niektóre podstawowe operacje synchronizacyjne aktywnie sprawdzają dostępność zasobu, a nie fakt jego zablokowania. Takie aktywne spraw­ dzenie także może stać się przyczyną blokady wzajemnej, podobnie jak blokowanie, ma tylko inne objawy - w przypadku zakleszczenia wykorzystanie procesora jest wysokie.

636

I

Rozdział 16. Wątki i kod asynchroniczny

Te dwa opisane zagrożenia związane z pracą współbieżną - wyścig oraz blokada wzajemna - nie są jedynymi problemami, jakie mogą występować w aplikacjach wie­ lowątkowych. W przypadku systemów o wielu wątkach istnieje niemal nieskończenie wiele przyczyn potencjalnych problemów, dlatego zamieszczone tu informacje stanowią jedynie czubek góry lodowej . Na przykład prócz czynników mogących prze­ szkodzić w prawidłowym działaniu kodu istnieje całe mnóstwo takich, które mogą przyczyniać się do pogorszenia jego wydajności. Czytelnikom poszukującym wyczer­ pującej prezentacji tych problemów oraz informacji na temat sposobów radzenia sobie z nimi polecamy książkę Concurrent Programming on Windows napisaną przez Joego Duffy'ego (i wydaną przez wydawnictwo Addison-Wesley) .

Co możemy zrobić, by uniknąć tych wszystkich problemów, jakie mogą się pojawiać w wielo­ wątkowym kodzie?

Strategie tworzenia kodu wielowątkowego Istnieje kilka strategii, które mają na celu ułatwienie pisania kodu wielowątkowego, a każda z nich zapewnia nieco inny kompromis pomiędzy trudnością tworzenia kodu oraz jego ela­ stycznością.

Wstrzem ięźl iwość Oczywiście najprostszym sposobem uniknięcia ryzyka, które w nierozerwalny sposób wiąże się z tworzeniem kodu wielowątkowego, jest całkowita rezygnacja z korzystania z większej liczby wątków. Nie oznacza to jednak całkowitej rezygnacji ze wszystkich rozwiązań prezen­ towanych w tym rozdziale. W pewnych typach aplikacji niektóre wzorce asynchroniczne zapew­ niają możliwość wykorzystania zalet działania asynchronicznego przy jednoczesnym pozosta­ niu przy jednowątkowym modelu programowania.

I zolacja Jeśli mamy zamiar korzystać z większej liczby wątków, to doskonałym sposobem zachowa­ nia prostoty rozwiązania jest unikanie współdzielenia informacji pomiędzy nimi. Technologia ASP.NET zachęca do stosowania tego modelu - korzysta ona z puli wątków do współbieżnej obsługi wielu żądań, jednak każde z tych żądań jest obsługiwane przez nasz kod w ramach jednego wątku. (Jeśli chcemy używać większej liczby wątków do obsługi poszczególnych żądań, możemy się także zdecydować na jawne zastosowanie modelu asynchronicznego, niemniej jednak w prostych sytuacjach model jednowątkowy jest najlepszy) . A zatem choć aplikacja inter­ netowa, pojmowana jako całość, może korzystać z wielu wątków wykonywanych współbieżnie, to jednak pomiędzy tymi wątkami nie występuje żadna interakcja . Stosowanie tego rozwiązania wymaga pewnej dyscypliny, platforma .NET natomiast nie udostępnia żadnych mechanizmów wymuszających jej zachowanie4 . Innymi słowy, po prostu musimy zdecydować się na nieużywanie tych samych danych w różnych wątkach. W przy­ padku aplikacji internetowych nie jest to zadaniem szczególnie trudnym, gdyż protokół HTTP 4

Platforma .NET dysponuje pewnym mechanizmem izolacji: nasz kod można podzielić na tak zwane domeny aplikacji (ang. appdomain). Jednak rozwiązanie to samo w sobie zwiększa stopień złożoności kodu i zostało zaprojektowane w celu jego bardziej szczegółowego dzielenia. Poza tym raczej niezbyt dobrze nadaje się ono na narzędzie rozwiązania tego problemu. Technologia ASP.NET może korzystać z niego w celu izolowania wielu aplikacji internetowych współużytkujących ten sam proces, lecz nie używa go do izolacji poszczególnych żądań.

Wątki

I

637

z natury preferuje komunikację bezstanową. Jeśli jednak zaczniemy stosować jakieś techniki przechowywania stron w pamięci podręcznej w celu poprawienia wydajności aplikacji, to utracimy część z tej izolacji, gdyż wszystkie żądania będą w efekcie korzystały ze wspólnego obiektu przechowywanego w tej pamięci. Poza tym wszelkie informacje przechowywane w polu statycznym (oraz we wszelkich obiektach dostępnych pośrednio lub bezpośrednio poprzez takie pole statyczne) mogą być współużytkowane. Dla większości aplikacji wielowątkowych istnieje całkiem spore prawdopodobieństwo, że przy­ najmniej część informacji będzie musiała być używana przez większą liczbę wątków, zatem uzyskanie całkowitej izolacji może być w ich przypadku niemożliwe. Jednak dążenie do uzy­ skania jak najwyższego poziomu izolacji jest doskonałym pomysłem - im więcej informacji będzie dostępnych lokalnie dla konkretnego wątku, tym mniej będziemy musieli przejmować się zagrożeniami związanymi ze współbieżnym korzystaniem z tych informacji .

Niezm ienność Jeśli współdzielenie danych jest naprawdę konieczne, to często można uniknąć wielu zagro­ żeń związanych z ich współbieżnym używaniem, udostępniając jedynie dane niezmienne, czyli takie, których nie można zmodyfikować. Na przykład wartości pól oznaczonych mody­ fikatorem readon l y nie można zmieniać po utworzeniu obiektu - wymusza to sam kompilator C# - a zatem jeśli zdecydujemy się z nich korzystać, nie będziemy musieli się przejmować, że inny wątek dokona ich zmiany. Trzeba jednak uważać, gdyż modyfikator readon l y ma zasto­ sowanie wyłącznie do samego pola, a nie do obiektu, do którego to pole się odnosi (o ile jest to pole typu referencyjnego) . (Nawet jeśli pole jest typu wartościowego, to w przypadku gdy sama wartość zawiera pola typu referencyjnego, obiekty, do których się one odwołują, też nie podlegają działaniu modyfikatora readon l y) . A zatem, podobnie jak w przypadku izolacji, korzy­ stanie z niezmienności wymaga pewnej dyscypliny.

Synchronizacja Jeśli piszemy kod wielowątkowy, to wcześniej czy później pojawi się konieczność udostęp­ nienia jakichś informacji większej liczbie wątków. Co więcej, najprawdopodobniej informacje te będą musiały być modyfikowane - czasami po prostu nie da się stosować całkowitej izolacji i niezmienności . W takich sytuacjach pojawi się także konieczność synchronizacji dostępu do współużytkowanych danych. Przykładowo za każdym razem, gdy taka informacja będzie modyfikowana, trzeba będzie zadbać o to, by żaden inny wątek nie próbował jej w tym samym czasie odczytywać lub zmieniać. To z kolei wymaga zachowania największej dyscypliny spo­ śród wszystkich opisanych wcześniej rozwiązań. Będzie też zapewne najbardziej skompliko­ wane, choć jednocześnie zapewnia największą elastyczność . .NET Framework udostępnia szeroki wachlarz narzędzi pomagających w synchronizacji wąt­ ków korzystających ze wspólnych informacji. To właśnie te narzędzia są tematem kolejnego podrozdziału.

Podstawowe narzędzia synchronizacj i Podczas tworzenia programów korzystających z wielu wątków może się pojawić potrzeba koordynacji ich działania na dwa ważne sposoby. Kiedy istnieją współużytkowane i modyfiko­ walne dane, to musi także istnieć możliwość zagwarantowania, że każdy z wątków kolejno jeden po drugim - będzie w stanie uzyskać do nich dostęp . Jednak równie często zdarza się, 638

I

Rozdział 16. Wątki i kod asynchroniczny

że wątki będą musiały zdobywać informacje o pewnych zdarzeniach - wątek może być na przy­ kład blokowany aż do momentu, gdy będzie miał coś pożytecznego do zrobienia . Niektóre podstawowe narzędzia synchronizacji udostępniają więc możliwość korzystania z powiadomień, a nie uzyskiwania wyłącznego dostępu. Inne zapewniają obie te możliwości jednocześnie.

Mon itor Najczęściej stosowanym podstawowym narzędziem synchronizacji jest na platformie .NET monitor. Monitory są obsługiwane bezpośrednio przez .NET Framework - można ich używać ze wszelkimi innymi obiektami - jak również przez język C#, który udostępnia specjalne słowo kluczowe służące do korzystania z nich. Monitory zapewniają zarówno możliwość implemen­ tacji wzajemnego wykluczenia, jak i korzystania z powiadomień. Najprostszym zastosowaniem monitora jest zapewnienie, że wątki będą uzyskiwać dostęp do wspólnego zasobu kolejno, jeden po drugim. Listing 16.9 przedstawia fragment kodu, który przed zastosowaniem go w programie korzystającym z kilku wątków musiałby zostać zabez­ pieczony właśnie w taki sposób, na jaki pozwalają monitory. Jego przeznaczeniem jest obsługa listy ostatnio używanych łańcuchów znaków - z podobnego kodu można skorzystać w celu wyświetlenia listy ostatnio używanych plików prezentowanej standardowo w menu Plik. Powyż­ szy kod nie próbuje w żaden sposób zabezpieczyć się na wypadek działania wielowątkowego.

Listing 16.9. Kod niedostosowany do działania wielowątkowego cl ass Mos tRecent l yUsed { pri vate Li s t i tems = new Li st () ; pri vate i nt max l tems ; publ i c Mos t Recen t l yUsed ( i nt max i mumi temCount) { max l tems = maxi mumi temCount ;

publ i c vo i d U s e l tem (s t r i ng i tem) { li Jeśli element już jest na liście i nie jest jej pierwszym li elementem, to usuwamy go z aktualnie zajmowanego miejsca, li gdyż zamierzamy umieścić go na pierwszym miejscu. i nt i temlndex = i tems . I ndexOf ( i tem) ; i f ( i temlndex > O) { i tems . RemoveAt ( i temi ndex) ;

li Jeśli element już jest pierwszy, nie musimy nic robić. i f ( i temlndex ! = O) { i tems . I nsert (O , i tem) ; li Upewniamy się, że liczba elementów listy nie przekroczy li dopuszczalnego limitu. i f ( i tems . Count > max l tems ) { i tems . RemoveAt ( i tems . Count l) ; -

Podstawowe narzędzia synchronizacji

I

639

publ i c I Enumerabl e Getl tems () { return i tems . ToArray ( ) ;

Listing 16.10 przedstawia przykładowy kod służący do przetestowania powyższej klasy.

Listing 16.10. Testowanie klasy MostRecentlyUsed const i nt I terat i ons = 10000 ; s t at i c vo i d Tes tMru (Mos t Recen t l yUsed mru) { li Inicjalizacja generatora liczb losowych przy użyciu identyfikatora li wqtku zapewnia, że każdy wqtek będzie używał innych danych li (choć z drugiej strony sprawia, że każdy test będzie miał li inny przebieg, co nie jest idealnym rozwiązaniem). Random r = new Random (Thread . CurrentThread . ManagedThread l d) ; s t r i ng [] i tems = { "j eden " , " dwa " , " trzy " , " cztery " , " p i ęć " , " s ześć " , " s i edem " , " os i em " } ; for ( i nt i = O ; i < I terat i ons ; ++i ) { mru . Us e l tem ( i tems [ r . Next ( i tems . Length) ] ) ;

Kod z listingu 16.10 jedynie pobiera w losowej kolejności elementy z grupy predefiniowa­ nych łańcuchów znaków. Wywołanie tej metody w jednym wątku da oczekiwane wyniki: na samym jej końcu obiekt typu MostRecent l yUsed zwróci ostatnio używane elementy umieszczone w nim podczas testu. Jednak podczas analogicznego testu przeprowadzonego z wykorzystaniem większej liczby wątków i przedstawionego na listingu 16.11 zdarzy się coś zupełnie innego.

Listing 1 6 . 1 1 . Test wykonywany z wykorzystaniem większej liczby wqtków Mos tRecentl yUsed mru = new Mos t Recen t l yUsed (4) ; const i nt TestThreadCount = 2 ; Li s t threads = (from i i n Enumerabl e . Range ( O , TestThreadCount) sel ect new Thread ( () => Tes tMru (mru) ) ) . To l i s t () ; threads . ForEach (t => t . Start () ) ; threads . ForEach (t => t . Jo i n () ) ; foreach (stri ng i tem i n mru . Ge t l tems () ) { Consol e . Wri tel i ne ( i tem) ;

Na komputerach z wielordzeniowymi procesorami ten kod ulega awarii - po jakimś czasie zostaje zgłoszony wyjątek Argumen tOut OfRangeExcep t i on . Awaria nie zawsze następuje w tym samym miejscu. Może ona wystąpić w jednym z dwóch wywołań metody RemoveAt klasy L i st. Przyczyną zgłaszania tego wyjątku jest wyścig. Przeanalizujmy dla przykładu poniższy wiersz kodu pochodzący z listingu 16.9. i tems . RemoveAt ( i tems . Count

-

l) ;

Wiersz ten odczytuje wartość właściwości Count, następnie pomniejsza ją o jeden, by uzyskać indeks ostatniego elementu listy, i w końcu pobiera ten ostatni element. Zjawisko wyścigu może wystąpić w tym kodzie w przypadku, gdy jakiś inny wątek zakończy usuwanie elementu z listy w czasie pomiędzy odczytaniem wartości właściwości Count i wywołaniem metody RemoveAt . 640

I

Rozdział 16. Wątki i kod asynchroniczny

Wówczas wywołanie tej metody spowoduje zgłoszenie wyjątku Argumen tOu tOfRangeExcep t i on, gdyż będziemy prosić o usunięcie elementu o indeksie określającym miejsce za ostatnim elementem listy. W rzeczywistości będziemy mieć szczęście, jeśli wyjątek ten w ogóle zostanie zgłoszony, gdyż klasa L i s t wcale tego nie gwarantuje w przypadku jednoczesnego korzystania z list w wielu wątkach. Oto, co napisano w jej dokumentacji w części poświęconej bezpieczeństwu działania w środowiskach wielowątkowych: Publiczne składowe statyczne tego typu są bezpieczne pod względem wielowątkowości. Więk­ szość typów wchodzących w skład biblioteki .NET Framework nie gwarantuje takiego bezpie­ czeństwa w przypadku składowych instancji.

Oznacza to, że to my mamy zadbać, by obiekt Li st nigdy nie był używany w więcej niż jednym wątku w danym czasie . Problemy, jakich może przysporzyć niewłaściwe korzystanie z obiektów L i st, mogą być znacznie bardziej subtelne niż awaria lub zgłoszenie wyjątku. Mogą na przykład doprowadzić do uszkodzenia danych. '

. .

"',

...._'__...z.r_ '

Klasa Li st nie jest pod tym względem żadnym wyjątkiem. Zdecydowana większość typów w .NET Framework nie daje gwarancji bezpiecznego korzystania ze składowych instancji w środowiskach wielowątkowych .

Bardzo podobną notatkę moglibyśmy umieścić w dokumentacji naszej klasy Mos t Recen t l yU sed, informując jej użytkowników o tym, że także i ona nie daje żadnych gwarancji . W rzeczywi­ stości mogłoby się okazać, że to najlepsze rozwiązanie, bowiem zagwarantowanie pojedyn­ czej klasie prawidłowego działania we wszystkich możliwych sytuacjach, jakie mogą się zdarzyć w programach wielowątkowych, jest niezwykle trudne . Jedynie aplikacja, która używa danej klasy, jest tak naprawdę w stanie określić, jakie powinno być jej prawidłowe działanie . Na przykład może się okazać, że obiekt klasy MostRecentl yUsed musi być synchronizowany z jakimś innym obiektem - w takim przypadku aplikacja musiałaby samodzielnie realizować wszystkie czynności związane z tą synchronizacją, a nasza klasa nie byłaby w stanie sama wykonywać żadnych użytecznych operacji . To tylko jeden z powodów, dla których brak gwarancji bez­ piecznego działania w środowiskach wielowątkowych jest tak powszechny w bibliotekach klas - nie ma żadnej dobrej, ogólnej definicji określającej, jak dla poszczególnych klas powinno wyglądać takie „bezpieczne działanie" . Gdybyśmy zdecydowali, że właśnie ten problem m a rozwiązywać nasza aplikacja, to jak powinna ona wyglądać? Nie mamy do dyspozycji rzeczywistej aplikacji, a jedynie prosty pro­ gram testowy, a zatem jego kod powinien w jakiś sposób synchronizować wywołania metod naszego obiektu. Listing 16.12 przedstawia odpowiednio zmodyfikowaną wersję metody testowej z listingu 16.10. (Należy zauważyć, że kod z listingu 16.11 używa tego samego obiektu, można zatem sądzić, że także w nim należałoby wprowadzić stosowne zmiany. Jednak ten kod, zanim zacznie używać obiektu, czeka, aż wszystkie wątki zostaną zakończone, a to oznacza, że wyko­ nywane w nim operacje odczytu nie będą pokrywać się w czasie z realizowanymi w wątkach operacjami zapisu, dzięki czemu blokowanie nie jest tu konieczne. Innymi słowy, implemen­ tacja przedstawiona na listingu 16.12 będzie dobrym i wystarczającym rozwiązaniem) .

Listing 16.12. Synchronizacja w kodzie wywolujqcym s t at i c vo i d Tes tMru (Mos t Recen t l yUsed mru) { Random r = new Random (Thread . CurrentThread . ManagedThread l d) ;

Podstawowe narzędzia synchronizacji

I

641

s t r i ng [] i tems = { "j eden " , " dwa " , " trzy " , " cztery " , " p i ęć " , " s ześc " , " s i edem " , " os i em " } ; for ( i nt i = O ; i < I terat i ons ; ++i ) { l ock (mru) { mru . Us e l tem ( i tems [ r . Next ( i tems . Length) ] ) ;

Jedyną modyfikacją wprowadzoną w powyższym przykładzie jest umieszczenie wywołania metody U s e I t ern obiektu klasy Mos t Recent l yUsed wewnątrz bloku l oc k . Zastosowanie słowa kluczowego l oc k sprawia, że C# wygeneruje dodatkowy fragment kodu korzystający z klasy Mon i tor i uzupełniony o odpowiednią obsługę wyjątków. Oto faktyczny odpowiednik kodu przedstawionego na poprzednim listingu: Mos tRecentl yUsed referenceTolock = mru) ; bool l oc kAcqu i red = fal s e ; try { Mon i tor . Enter ( referenceTo loc k , ref l oc kAcq u i red) ; mru . Us e l tem ( i tems [r . Next ( i tems . Length) ] ) ; fi nal l y { i f ( l oc kAcqu i red) { Mon i tor . Ex i t (referenceTo loc k) ;

(Tak będzie wyglądał kod wygenerowany przez C# 4.0 . Wcześniejsze wersje języka generują kod nieco prostszy, który jednak nie obsługuje prawidłowo ewentualnych błędów. Podsta­ wowa idea działania w obu przypadkach będzie jednak taka sama. Wygenerowany fragment kodu kopiuje referencję mru do osobnej zmiennej, by zapewnić prawidłowe działanie nawet w przypadku, gdyby kod umieszczony w bloku l oc k zmieniał tę zmienną) . Według dokumentacji metoda Mon i tor . Enter uzyskuje wyłączną blokadę obiektu przekazanego jako pierwszy argument jej wywołania. Ale co to dokładnie oznacza? Otóż w pierwszym wątku, który wywoła tę metodę, jej działanie zakończy się natychmiast, jednak jakikolwiek inny wątek, który spróbuje wykonać to samo wywołanie, używając tego samego obiektu, będzie musiał zaczekać - w tych wątkach metoda Mon i tor . En t er nie zakończy się aż do momentu, gdy wątek aktualnie posiadający blokadę zwolni ją, wywołując metodę Mon i tor . Ex i t . W danej chwili tylko jeden wątek może posiadać blokadę, a zatem jeśli większa ich liczba oczekuje w metodzie Mon i tor. Enter na uzyskanie blokady tego samego obiektu, to .NET Framework wybiera jeden z nich i przydziela mu blokadę, natomiast wszystkie pozostałe wątki dalej pozo­ stają zablokowane . ••



. •

._,..�;

.

L---...iJ"' '

642

I

Posiadanie blokady obiektu ma tylko jeden efekt: uniemożliwia jakimkolwiek innym wątkom uzyskanie blokady danego obiektu. I to wszystko. W szczególności uzyskanie blokady nie uniemożliwia innym wątkom korzystania z danego obiektu. Innymi słowy, błędem byłoby sądzić, że uzyskanie blokady obiektu oznacza jego zablokowanie. Być może wygląda to na dzielenie włosa na czworo, jednak w praktyce różnica ta oddziela kod działający od kodu, który nie będzie działał.

Rozdział 16. Wątki i kod asynchroniczny

Zastosowanie monitorów jest w całości kwestią konwencji - wyłącznie od nas zależy, którego obiektu będziemy używać do uzyskiwania blokady i ochrony danych. W przykładzie z lis­ tingu 16 .12 pobieramy blokadę tego samego obiektu, którego stan ma być chroniony, jednak bardzo często stosowanym - a nawet preferowanym - rozwiązaniem jest tworzenie zupełnie odrębnego obiektu, którego jedynym przeznaczeniem jest pełnienie roli obiektu używanego do blokowania. Takie rozwiązanie jest stosowane z kilku powodów. Przede wszystkim często zdarza się, że przy użyciu jednej blokady chcemy chronić kilka odrębnych danych. Na przykład aktualizacji naszego obiektu MostRecentl yUsed musi towarzyszyć wprowadzenie zmian w innym stanie programu, chociażby zmiana informacji przechowywanych w usłudze rejestrującej historię wykonywanych operacji. Jeśli w grę wchodzi modyfikacja kilku różnych obiektów, to arbitralny wybór jednego z nich i zastosowanie go do uzyskania blokady może utrudnić analizę kodu, gdyż dla innych osób może nie być oczywiste, dlaczego blokada danego obiektu jest używana do ochrony kilku różnych obiektów, a nie tylko tego jednego. Jeśli natomiast utworzymy spe­ cjalny obiekt, którego jedynym zadaniem będzie blokowanie, to dla każdego, kto będzie ana­ lizował kod, będzie jasne, że musi pomyśleć o stanie, który dana blokada ochrania. Innym powodem przemawiającym za unikaniem uzyskiwania blokady obiektu, do którego dostęp chcemy synchronizować, jest to, że nie zawsze będziemy wiedzieć, czy obiekt sam nie próbuje uzyskać swojej blokady. Niektórzy programiści, próbując tworzyć obiekty bezpieczne pod względem wielowątkowości (ang. thread-safe) (bez względu na to, jaką definicję tego bez­ pieczeństwa wybrali), umieszczają w metodach instrukcję l oc k ( t h i s ) . Uzyskiwanie blokady referencji t h i s jest oczywiście złą praktyką, gdyż nie można mieć pewności, czy ktoś inny używający obiektu nie spróbuje z jakichś własnych powodów uzyskać jego blokady. Takie blokowanie w ramach obiektu jest wewnętrzną sprawą implementacji, tymczasem referencja t h i s jest publiczna, a przeważnie nie będziemy chcieli, by szczegóły implementacyjne były dostępne publicznie . Krótko mówiąc, nie należy próbować uzyskiwać blokady, używając d o tego celu jakiegokol­ wiek obiektu, do którego dostęp chcemy synchronizować . Biorąc to wszystko pod uwagę, co powinniśmy zrobić, jeśli chcemy usprawnić naszą klasę Most Recentl yUsed i sprawić, by była ona bardziej niezawodna w przypadku użycia jej w progra­ mach wielowątkowych? Przede wszystkim powinniśmy określić, jakie scenariusze działania wielowątkowego chcemy obsługiwać. Samo stwierdzenie, że zależy nam na utworzeniu klasy bezpiecznej pod względem wielowątkowości, jest pozbawione znaczenia . Załóżmy zatem, że chcemy zapewnić możliwość jednoczesnego wywoływania metod U s e l tem oraz Get l tems w wielu wątkach bez obawy, że zaczną się pojawiać wyjątki. Zwróćmy uwagę na fakt, że to raczej niewielkie zabezpieczenie . Nic nie wspomniano tu o stanie, w jakim znajdzie się obiekt po wykonaniu wywołania - stwierdzono jedynie, że nie powinien on dopro­ wadzić do wystąpienia awarii. Oczywiście byłoby znacznie lepiej, gdybyśmy zagwarantowali, że wywołania będą obsługiwane w takiej kolejności, w jakiej je wykonywano. Niestety nie można tego zagwarantować w przypadku, gdy logika blokowania jest zaimplementowana wewnątrz klasy. Systemowy mechanizm szeregujący może zdecydować się wywłaszczyć wątek tuż po wywołaniu metody U s e l tem, zanim będzie miał on szansę wykonania umieszczonego w niej kodu synchronizującego. W ramach przykładu pomyślmy, co by się stało, gdyby wątek A wywołał metodę U s e l t em, a następnie, przed jej zakończeniem, zostałaby ona wywołana także przez wątek B, lecz zanim którekolwiek z tych wywołań zostałoby zakończone, wątek C wywołałby metodę Get l tem s .

Podstawowe narzędzia synchronizacji

I

643

Metoda ta mogłaby nie zwrócić żadnego z obiektów przekazanych w wątkach A i B. Mogłaby też zwrócić oba z nich, przy czym mogłaby to zrobić na dwa różne sposoby - Get I tems zwraca listę uporządkowaną, więc na początku listy mógłby się znaleźć obiekt przekazany w wątku A lub w wątku B. Mogłoby się także zdarzyć, że metoda zwróciłaby tylko jeden obiekt - ten przekazany przez wątek A lub przez wątek B . Jeśli konieczne jest zapewnienie koordynacji wywołań różnych metod, tak jak w tym przypadku, to nie można tego zrobić wewnątrz klasy Mos t Recen t l yUsed . To rozwiązanie zapewnia nam bowiem możliwość rozpoczęcia synchroni­ zacji w momencie, kiedy realizacja wywołania już trwa. To kolejny powód, dla którego kod odpowiedzialny za synchronizację jest zazwyczaj implementowany na poziomie aplikacji, a nie na poziomie pojedynczych obiektów. A zatem najlepszym efektem, jaki możemy uzyskać, umieszczając kod synchronizujący wewnątrz naszej klasy, jest zabezpieczenie jej przed zgła­ szaniem wyjątków w przypadkach, gdy będzie ona używana jednocześnie w wielu wątkach. To rozwiązanie zostało przedstawione na listingu 16.13.

Listing 16.13. Dodanie blokowania do klasy cl ass Mos tRecent l yUsed { pri vate Li s t i tems = new Li st () ; pri vate i nt max l tems ; pri vate obj ect l ockObj ect = new obj ect () ; publ i c Mos t Recen t l yUsed ( i nt max i mumi temCount) { max l tems maxi mumi temCount ; =

publ i c vo i d U s e l tem (s t r i ng i tem) { l oc k ( l ockObj ect) { li Jeśli element już jest na liście i nie jest jej pierwszym li elementem, to usuwamy go z aktualnie zajmowanego miejsca, li gdyż zamierzamy umieścić go na pierwszym miejscu. i nt i teml ndex = i tems . I ndexOf ( i tem) ; i f ( i teml ndex > O) { i tems . RemoveAt ( i temi ndex) ;

li Jeśli element już jest pierwszy, nie musimy nic robić. i f ( i teml ndex ! = O) { i tems . I nsert (O , i tem) ; li Upewniamy się, że liczba elementów listy nie przekroczy li dopuszczalnego limitu. i f ( i tems . Count > max l t ems) { i tems . RemoveAt ( i tems . Count l) ; -

publ i c I Enumerabl e Getl tems () { l oc k ( l ockObj ect)

644

I

Rozdział 16. Wątki i kod asynchroniczny

return i tems . ToArray () ;

Trzeba zwrócić uwagę, że do naszej klasy dodaliśmy nowe pole l oc kObj ect zawierające refe­ rencję do obiektu, którego będziemy używali wyłącznie do pobierania blokady. Sama blokada jest uzyskiwana wewnątrz metod wykonujących operacje na liście. Blokadę tę musimy posia­ dać przez cały czas realizacji metody Usel tem, gdyż kod blokuje stan wszystkich elementów listy bezpośrednio po jej uruchomieniu, a reszta wykonywanych przez nią operacji zależy od tego, co uda się jej znaleźć. Jej kod po prostu by nie działał, gdyby zawartość listy została zmieniona w trakcie wykonywanych operacji. Właśnie dlatego blokada jest pobierana na cały czas wyko­ nywania metody. W tym konkretnym przypadku pobranie blokady na cały okres wykonywania metody raczej nie powinno przysporzyć problemów, gdyż nie trwa ono zbyt długo. Jednak ogólna zasada zaleca, by blokady nie utrzymywać dłużej, niż to konieczne. Im dłużej posiadamy blokadę, tym większe jest prawdopodobieństwo, że jakiś inny wątek spróbuje ją także uzyskać i będzie zmuszony do oczekiwania. Szczególnie złym pomysłem jest wywoływanie podczas posiada­ nia blokady kodu, który wykonuje jakieś odwołanie sieciowe i musi czekać na otrzymanie odpowiedzi (przykładem może być uzyskanie blokady i przesłanie żądania do bazy danych) . Ze szczególną ostrożnością należy podchodzić do uzyskiwania wielu blokad. Próby uzyskania jednej blokady podczas posiadania innej są najlepszym sposobem na dopro­ wadzenie do zakleszczenia. Czasami jednak nie da się uniknąć tego typu rozwiązań. W takich przypadkach konieczne jest opracowanie odpowiednich strategii unikania zakleszczeń. Zagadnienia te wykraczają poza zakres tematyczny niniejszej książki, jednak jeśli Czytelnik znajdzie się w takiej sytuacji, to odpowiednim rozwiązaniem będzie skorzystanie z blokad hierarchicznych (ang. lock leveling) dużo informacji na ten temat można znaleźć w internecie, wyszukując hasło „lock leveling for mul­ tithreading". -

Zgodnie z informacjami, które podaliśmy kilka stron wcześniej, zastosowanie klasy Mon i tor nie ogranicza się wyłącznie do blokowania . Daje ona także pewne możliwości korzystania z powiadomień.

Powiadom ienia Załóżmy, że chcielibyśmy napisać pewien kod testujący naszą klasę Mos t Recen t l yUsed w roz­ wiązaniach wielowątkowych. Nawet stosunkowo prosty test stanowi spore wyzwanie. Na przykład co powinniśmy zrobić, by sprawdzić, czy po zakończeniu wywołania metody Usel tem w jednym wątku przekazany przez nią element będzie widoczny jako pierwszy element zwró­ cony przez metodę Get I tern wywołaną w innym wątku? W tym przypadku nie testujemy dzia­ łania równoległego - sprawdzamy operacje sekwencyjne, przy czym jedna z nich jest wyko­ nywana w jednym wątku, a kolejna w innym . W jaki sposób napisać kod, który mógłby skoordynować te operacje wykonywane w różnych wątkach? Musimy zmusić jeden z nich, do oczekiwania, aż inny coś zrobi. Moglibyśmy ponownie zastosować metodę J o i n klasy T h read i czekać, aż pierwszy wątek zostanie zakończony. Ale co zrobić, gdy wcale nie chcemy koń­ czyć działania pierwszego wątku? Może nam zależeć na przykład na wykonaniu sekwencji operacji, w której poszczególne wątki wykonują zwoje zadania kolejno, jeden po drugim.

Podstawowe narzędzia synchronizacji

I

645

Właśnie w takich przypadkach mogą nam pomóc monitory. Nie tylko pozwalają one chronić wspólny stan, lecz także zapewniają możliwość określania, kiedy ten stan uległ zmianie. Klasa Mon i tor udostępnia metodę W a i t, która współdziała z dwiema innymi metodami: P u l s e lub Pul s eAl l . Wątek oczekujący na jakieś zdarzenie może wywołać metodę Wai t, która spowoduje jego zablokowanie aż do momentu wywołania metody Pul s e lub Pul s eA 1 1 . Wywołując którą­ kolwiek z tych metod, musimy już posiadać blokadę przekazywanego do nich obiektu. Próba wywołania któreś z nich w przypadku, gdy nie dysponujemy tą blokadą, spowoduje zgłosze­ nie wyjątku. W przykładzie przedstawionym na listingu 16 .14 skorzystano z tych metod, by zapewnić jednemu wątkowi możliwość oczekiwania na wykonanie pewnej operacji w drugim wątku. Jedynym interesującym stanem naszej przykładowej klasy jest pole logiczne o nazwie canGo, które początkowo ma wartość fal s e, lecz przyjmuje wartość t rue, gdy drugi wątek zrobi to, na co czekamy, o czym nas poinformuje, wywołując metodę GoNow. Ponieważ to pole logiczne będzie używane w kilku wątkach, musi być synchronizowane. Dlatego też klasa Wai t Fori t defi­ niuje także pole l o c kObj ect zawierające referencję do obiektu, którego jedynym celem będzie umożliwienie nam pobrania blokady używanej do synchronizacji dostępu do pola canGo. Nigdy nie należy próbować bezpośrednio pobierać blokady, używając w tym celu pola lub zmiennej typu bool bądź jakiegokolwiek innego typu wartościowego. Do pobiera­ nia blokad można używać wyłącznie typów referencyjnych. Jeśli zatem w wywoła­ niu metody Moni tor . Enter spróbujemy przekazać wartość typu boa l , to kompilator C# zrobi to, co zawsze robi w przypadkach, gdy zamiast obiektu przekażemy daną typu wartościowego - umieści ją wewnątrz obiektu, tak jak opisaliśmy to w rozdziale 4. Będziemy zatem uzyskiwali blokadę tego obiektu, a nie samej wartości. Problem polega na tym, że za każdym razem tworzony będzie nowy obiekt, a nasze blokowa­ nie nie będzie do niczego przydatne. Słowo kluczowe l ock w języku C# zapobiega podejmowaniu prób uzyskania blokady wartości - jeśli spróbujemy coś takiego zrobić, kompilator zgłosi błąd. Gdy jednak spróbujemy wywoływać metody klasy Moni tor bezpośrednio, C# nie uchroni nas przed popełnieniem tego błędu. To kolejny argument przemawiający za wyrobieniem sobie nawyku tworzenia niezależnego obiektu służącego wyłącznie do ochrony stanu i uży­ wania go do uzyskiwania blokad.

Listing 16.14. Koordynacja wątków przy wykorzystaniu klasy Monitor cl ass Wa i t Fo r i t { pri vate bool canGo ; pri vate obj ect l ockObj ect = new obj ect () ; publ i c vo i d Wa i tUnt i l Ready () { l oc k ( l ockObj ect) { wh i l e ( ! canGo) { Mon i tor . Wa i t ( l oc kObj ect) ;

publ i c vo i d GoNow ()

646

I

Rozdział 16. Wątki i kod asynchroniczny

l oc k ( l ockObj ect) { canGo true ; li Obudź mnie, zanim pójdziesz Mon i to r . Pul seAl l ( l ockObj ect ) ; =

...

Obie metody powyższej klasy, zanim cokolwiek zrobią, uzyskują blokadę, gdyż obie sprawdzają pole canGo i oczekujemy, że będą wywoływane z innych wątków. Następnie metoda Wai tUnt i l '"+Ready zaczyna realizować pętlę, która kończy się w momencie, gdy pole canGo przyjmie wartość t rue. Podczas każdej iteracji pętli wywoływana jest metoda Mon i tor . Wa i t, co ma okre­ ślone konsekwencje . Po pierwsze, zostaje zwolniona blokada, co jest ważne, gdyż bez tego wątek, który wywołał metodę GoNow, nigdy nie byłby w stanie zmienić wartości pola canGo. Po drugie, dzięki temu wątek wywołujący metodę W a i t U n t i l Ready zostaje zablokowany aż do momentu, gdy jakiś inny wątek wywoła metodę Pul se lub Pul s eAl l , używając przy tym obiektu l oc kObj ect. I po trzecie, po zakończeniu wywołania metody Wa i t ponownie uzyskamy blokadę . ••



.

·

.._,..�;

.

L-------11.Ji'" '

A dlaczego w tym przykładzie zastosowaliśmy pętlę? Czy nie wystarczyłaby instrukcja warunkowa i f umieszczona przed pojedynczym wywołaniem metody Wai t? W tym przypadku faktycznie takie rozwiązanie by wystarczyło, jednak ogólnie zadziwiająco łatwo jest stworzyć kod, który będzie generował masę błędnych powiadomień. Załóżmy, że zmodyfikowaliśmy powyższy kod w taki sposób, że oprócz metody GoNow udo­ stępnia on także trzecią metodę o nazwie OhHangOnAMi nu te, która ponownie zmienia wartość pola can Go na fal se. Innymi słowy, nasza klasa stała się bramą, która może się otwierać i zamykać. W takim przypadku mogłoby się zdarzyć, że metoda Wai tUnti l Ready obudziłaby się po wywołaniu metody GoNow, a pole canGo z powrotem przyjęłoby war­ tość fal se, gdyż zostałaby wywołana metoda OhHangOnAMi nute. Choć w naszym prostym przykładzie taka sytuacja się nie zdarzy, to jednak ogólnie rzecz biorąc, warto wyrobić sobie nawyk sprawdzania, czy po zakończeniu oczekiwa­ nia interesujący nas warunek wciąż jest spełniony, a jeśli nie jest, to zapewnić moż­ liwość dalszego oczekiwania na jego spełnienie.

Metoda GoNow pobiera blokadę, by mieć pewność, że bezpiecznie można zmodyfikować pole canGo, któremu chce przypisać wartość true. Następnie wywołuje ona metodę Pul s eAl l , informując w ten sposób .NET Framework, że zaraz po zwolnieniu blokady należy obudzić wszystkie wątki, które aktualnie oczekują na obiekt l oc kObj ect. (Metoda Pul se spowodowałaby zwolnienie tylko jednego wątku, jednak ponieważ nasza kasa Wai t For i t ma tylko dwa możliwe stany - może być gotowa bądź nie - to gdy stanie się gotowa, musi obudzić wszystkie oczekujące wątki) . W dalszej kolejności metoda GoNow kończy działanie i zwalnia blokadę, gdy realizacja kodu wyj­ dzie poza blok l oc k, co oznacza, że wszelkie inne wątki oczekujące wewnątrz metody Wai tUnt i l '"+Ready nie będą już zablokowane. Jeśli jednak na sygnał oczekuje więcej niż jeden wątek, to nie wszystkie z nich zaczną działać jednocześnie, gdyż metoda Mon i tor . Wa i t przed zakończeniem działania pobiera blokadę, która jest przez nią zwalniana tylko przejściowo na czas oczekiwania . Metoda ta upiera się, by w momencie jej wywoływania proces wywołujący posiadał blokadę i w momencie wzna­ wiania jej realizacji tę blokadę także będziemy posiadać. W rezultacie, jeśli metoda P u l s eAl l zwolni większą liczbę wątków, ich metody Wai t będą kończone nie jednocześnie, lecz jedna po drugiej .

Podstawowe narzędzia synchronizacji

I

647

Kiedy realizacja metody Wai tUnt i l Ready zostanie wznowiona, pętla ponownie sprawdzi stan pola canGo tym razem będzie ono miało wartość t rue, a zatem pętla zostanie zakończona . Następnie wyjdziemy poza blok l oc k, zwalniając blokadę obiektu l oc kObj ect i pozwalając tym samym, by kolejny oczekujący wątek (jeśli taki jest) zrobił dokładnie to samo. W ten sposób wszystkie oczekujące wątki zostaną, jeden po drugim, odblokowane. -

'

. .

Ścisła integracja blokowania oraz powiadomień, jaką zapewniają monitory, może się wydawać nieco dziwna - w tym przypadku nawet nam trochę przeszkadza. Przedsta­ wiony przykład działałby bardzo dobrze, nawet gdyby wszystkie oczekujące wątki zostały zwolnione jednocześnie, a nie musiały czekać na kolejne pobieranie blokady. Niemniej jednak takie połączenie blokowania i powiadomień ma kluczowe znacze­ nie w zdecydowanej większości zastosowań metod Pul se i Wai t. Powiadomienia są związane ze zmianą wspólnego stanu obiektów, dlatego też jest niezwykle ważne, by zgłaszający je kod posiadał blokadę, jak również by posiadał ją także kod w momencie dowiadywania się o powiadomieniu, gdyż dzięki temu będzie on mógł bezzwłocz­ nie sprawdzić stan. Bez tego w krótkich odstępach czasu pomiędzy zgłoszeniem powiadomienia i uzyskaniem blokady lub zwolnieniem blokady i oczekiwaniem na powiadomienie mogłyby występować zjawiska różnego rodzaju wyścigów pomiędzy wątkami.

Na listingu 16 .15 przedstawiliśmy prosty program korzystający z przedstawionej wcześniej klasy Wa i t Fori t . Program ten tworzy wątek, który czeka chwilę, a następnie wywołuje metodę GoNow. Wątek główny czeka na to zdarzenie, wywołując metodę Wai tUnt i l Rea dy po uruchomie­ niu dodatkowego wątku.

Listing 16.15. Stosowanie klasy WaitForlt cl ass Program { s t at i c vo i d Ma i n (stri ng O args) { Wai t Fo r i t wai ter = new Wa i t Fori t () ; ThreadStart twork = del egate { Con sol e . Wri tel i ne ( "Wątek uruchomi ony . . . " ) ; Thread . S l eep ( l OOO) ; Con sol e . Wri tel i ne ( " Generuj ę powi adomi en i e . " ) ; wa i ter . GoNow () ; Consol e . Wri teli ne ( " Powi adomi ono . " ) ; Thread . S l eep ( l OOO) ; Consol e . Wri teli ne ( "Wątek kończy d z i a ł an i e . . . " ) ; }; Thread t = new Thread (twork) ; Consol e . Wr i teli ne ( " Uruchomi łem nowy wątek . " ) ; t . Start () ; Consol e . Wr i teli ne ( " Cze kam na wykonan i e wąt ku . " ) ; wai ter . Wai tUnt i l Ready () ; Conso l e . Wri te l i ne ( " Oczek i wan i e z a kończone . " ) ;

Wyniki pokazują, dlaczego taki sposób koordynacji działania wątków jest czasami niezbędny: Uruchomi ł em nowy wąte k . Cze kam na wykonan i e wąt ku . Wątek uruchomi ony . . . Generuj ę powi adomi en i e .

648

I

Rozdział 16. Wątki i kod asynchroniczny

Powi adomi ono . Oczeki wan i e z a kończone . Wątek kończy dz i ał an i e . . .

Zauważmy, że nowy wątek nie został uruchomiony natychmiast - wątek główny wyświetla komunikat „Czekam na wykonanie wątku" po wywołaniu metody St art wątku dodatkowego, lecz komunikat ten pojawił się przed komunikatem „Wątek uruchomiony . . . " generowanym w pierwszej kolejności w wątku dodatkowym. Innymi słowy, sam fakt zwrócenia sterowania z metody Start klasy Thread nie stanowi jeszcze żadnej gwarancji, że nowo utworzony wątek zdołał już coś zrobić. Określoną kolejność wykonywania działań w różnych wątkach można zatem uzyskać wyłącznie dzięki zastosowaniu takich metod jak Wai t oraz Pul se. Nigdy nie należy używać metody Thread . Sl eep w celu rozwiązania problemów z kolej­ nością realizacji wątków w kodzie produkcyjnym - technika ta nie jest wydajna, a na jej działaniu nie można polegać. W przykładzie przedstawionym na listingu 16.15 sko­ rzystano z niej wyłącznie po to, by wyraźniej pokazać problemy z koordynacją działania wątków. Choć można jej używać w przykładach do zwiększania problemów oraz ich odkrywania, to jednak nie daje ona żadnych gwarancji - uśpienie jednego wątku tylko po to, by dać innemu możliwość wznowienia działania, nie zapewnia, że faktycznie wznowi on działanie. W szczególności dotyczy to systemów, które są mocno obciążone.

Realizacja wątku głównego nie rozpocznie się bezzwłocznie po wywołaniu metody GoNow w innym wątku. (A jeśli nawet, to nie będzie on wykonywany na tyle długo, by udało mu się wyświetlić komunikat „Powiadomiono.") . Powyższy przykład za każdym razem może genero­ wać nieco inne wyniki i choć jesteśmy w stanie wymusić nieco porządku w tym, co się w nim dzieje, to jednak kolejności zachodzących tu zdarzeń nie da się do końca przewidzieć. Podczas tworzenia kodu wielowątkowego niezwykle ważne jest precyzyjne i jasne określenie, w jakim stopniu staramy się wymusić określoną kolejność, używając do tego celu blokad i powiadomień. W tym przykładzie gwarantujemy, że nasz wątek główny nie dotrze do wiersza wyświetlają­ cego komunikat „Oczekiwanie zakończone", dopóki drugi wątek nie dotrze do wywołania metody GoNow. Jednak jest to jedyny pewnik dotyczący jego działania - przebieg wykonania obu wątków wciąż może być przerywany na wiele różnych sposobów. Nigdy nie można zakła­ dać, że jakaś konkretna, obserwowana w praktyce kolejność zdarzeń będzie występować zawsze. Choć klasa Mon i tor oraz słowo kluczowe l oc k są najczęściej stosowanymi mechanizmami syn­ chronizacyjnymi, to jednak są także i inne rozwiązania.

I nne typy blokad Monitory są bardzo użyteczne i zazwyczaj są optymalnym pierwszym narzędziem do imple­ mentacji blokowania, istnieją jednak pewne sytuacje, w których zastosowanie bardziej wyspe­ cjalizowanych rozwiązań alternatywnych zapewni lepszą wydajność i większą elastyczność. Ponieważ rozwiązania te są stosowane raczej sporadycznie, nie będziemy opisywali ich szcze­ gółowo - przedstawimy tylko ogólnie, czym one są i kiedy mogą się nam przydać. Aby zrozumieć, dlaczego powstały alternatywy dla monitorów, warto dowiedzieć się czegoś wię­ cej na temat ich możliwości i ograniczeń. Monitory zostały zaprojektowane do użycia w ramach jednej dziedziny aplikacji - nie można z nich korzystać w celu synchronizowania operacji wykonywanych przez odrębne procesy ani pomiędzy domenami aplikacji współdzielącymi jeden proces . Koordynacja działań pomiędzy procesami w systemie Windows jest możliwa, Podstawowe narzędzia synchronizacji

I

649

jednak wymaga zastosowania zupełnie innych mechanizmów. Jedną z przyczyn tego stanu rzeczy jest to, że monitory starają się, jeśli to tylko możliwe, unikać ingerencji w działanie systemowego mechanizmu szeregującego. Jeśli nasz kod wykonuje instrukcję l oc k (bądź jawnie wywołuje metodę Mon i tor . Enter) , uży­ wając w niej obiektu, którego blokady nie posiada aktualnie żaden inny wątek, to platforma .NET jest w stanie wydajnie takie sytuacje obsłużyć. Nie musi w tym celu wykonywać żadnych odwołań do systemu operacyjnego. Monitory pozwalają na to, gdyż działają lokalnie w obrębie domeny aplikacji. Synchronizacja działań pomiędzy domenami aplikacji wymaga natomiast zazwyczaj pomocy ze strony systemu operacyjnego, a gdy pojawia się konieczność odwołania się do niego, to uzyskanie blokady staje się znacznie bardziej kosztowne. A zatem tam, gdzie nie występuje żadna rywalizacja, monitory działają doskonale. Kiedy jednak dojdzie do zablo­ kowania - bądź to ze względu na rywalizację, bądź na jawne wywołanie metody Wa i t - do akcji musi wkroczyć systemowy mechanizm szeregujący, gdyż tylko on jest w stanie zmienić stan wątku z wykonywalnego na zablokowany. Zazwyczaj nie ma w tym nic złego, gdyż ozna­ cza to, że wątek może efektywnie oczekiwać - kiedy wątek zostaje zablokowany, nie zużywa on cykli procesora, który może w tym czasie wykonywać inną przydatną pracę lub, jeśli nie będzie żadnych wątków do wykonywania, przejść w tryb oszczędzania energii, co jest szcze­ gólnie istotne w przypadku laptopów korzystających z zasilania bateryjnego. Jednak mogą się także pojawić sytuacje, w których koszt odwołania do systemu operacyjnego przewyższa korzyści, jakie odwołanie to jest nam w stanie zapewnić. I tak oto dotarliśmy do pierwszego rodzaju wyspecjalizowanych blokad.

Spin lock Struktura Spi n Loc k wprowadzona w .NET 4 zapewnia podobne możliwości funkcjonalne jak monitory, jednak w przypadku występowania zjawiska rywalizacji będzie ona, w pętli, nieustan­ nie sprawdzać, czy blokada nie została zwolniona. W efekcie tego oczekujący wątek będzie zużywał cykle procesora. Słowo „spin" 5 w nazwie typu oznacza ciągłe „kręcenie się w kółko" w pętli podczas oczekiwania na zwolnienie blokady. Można uznać, że w porównaniu z eleganckim, umożliwiającym oszczędzanie energii stanem zablokowania, w który można wprowadzić wątek w przypadku korzystania z monitorów, roz­ wiązanie to jest koszmarne. Czasami okazuje się, że pomysł ten faktycznie jest tak zły, na jaki wygląda. W ogromnej większości przypadków nie będziemy chcieli korzystać z blokad Spi n Loc k . Niemniej jednak zapewniają one jedną potencjalną zaletę: nigdy nie odwołują się do systemu operacyjnego, więc stanowią rozwiązanie znacznie „lżejsze" od monitorów. A zatem jeśli na konkretnej blokadzie naprawdę sporadycznie dochodzi do rywalizacji, to stosowanie ich może być mniej kosztowne . (Jeśli ma miejsce rywalizacja, lecz jej czas trwania jest bardzo krótki, to może się okazać, że w przypadku systemów wielordzeniowych koszt stosowania blokad Spi n lock będzie w rzeczywistości mniejszy niż koszt angażowania systemowego mechanizmu szeregują­ cego. Ogólnie rzecz biorąc, stosowanie tego typu blokad w systemach jednoprocesorowych jest jednak złym pomysłem, choć istniejąca implementacja typu Sp i n Lock nieco redukuje nega­ tywne efekty swego działania poprzez stosowanie techniki ustępowania6 w przypadkach, gdy nie udało się uzyskać blokady na komputerze jednoprocesorowym) . 5

Ang. spin: wirować, obracać się

6

Ustępowanie (ang. yielding) polega na informowaniu przez wątek systemowego mechanizmu szeregującego o tym, że chce on dać innemu wątkowi szansę na wykorzystanie procesora, zamiast biernie czekać, aż zostanie wywłaszczony. Jeśli w systemie nie ma żadnych innych wątków, które można by uruchomić, ustępowanie nie daje żadnego efektu i wątek dalej jest wykonywany.

650

I

-

przyp. tłum.

Rozdział 16. Wątki i kod asynchroniczny

'

. .

,

.___�.·

Spi n lock jest typem wartościowym, a konkretnie: strukturą. Właśnie to zapewnia jego „lekki" charakter, gdyż można go umieszczać w innych obiektach, a nie w osobnej przestrzeni na stosie. Oczywiście oznacza to także, że trzeba bardzo uważać na przypisywanie danych tego typu do zmiennych lokalnych, ponieważ oznaczałoby to utworzenie ich kopii, a blokowanie takiej kopii nie byłoby szczególnie przydatne.

Nigdy nie należy używać blokad Spi n Lock bez przeprowadzenia odpowiednich testów porów­ nawczych. W ich ramach należy zmierzyć wszystkie interesujące nas parametry wydajności w przypadku wykorzystania monitorów, a następnie w przypadku, gdy monitory zastąpimy blokadami Spi n Loc k. Zastosowanie tego drugiego rozwiązania można rozważyć wyłącznie wtedy, gdy testy wykazały bezdyskusyjnie płynące z niego korzyści . Jeśli nie mamy infrastruktury pozwalającej na sprawdzenie wszystkich wymagań wydajnościowych bądź jeśli nie dysponu­ jemy ilościowymi, precyzyjnie określonymi wymaganiami dotyczącymi efektywności działania aplikacji, oznacza to, że nasz projekt nie jest gotowy do zastosowania blokad Spi n Loc k . Z jakichś dziwnych powodów bardzo wielu programistów uwielbia bawić się w kana­ powych specjalistów od poprawiania wydajności aplikacji. Zadziwiająco wiele czasu i energii marnuje się na wszelkiego typu listach dyskusyjnych, forach internetowych i wewnętrznych spotkaniach firmowych, na których prowadzone są zacięte dyskusje dotyczące teoretycznej przewagi jednego rozwiązania nad drugim. Niestety w tych równaniach rzadko kiedy pojawiają się jakiekolwiek empiryczne testy. Jeśli ktoś, posługując się wyłącznie argumentami logicznymi, stara się dowieść, że jedno z tych rozwiązań jest szybsze, to należy go traktować bardzo podejrzliwie. Egzo­ tyczne i wysoko wyspecjalizowane narzędzia synchronizacyjne takie jak typ Spi n Lock wyzwalają w takich osobach wszystko, co najgorsze. (Właśnie dlatego w ogóle o tym wspominamy - wcześniej czy później każdy spotka się z programistą opętanym chęcią znalezienia jakiegoś zastosowania dla typu Spi nlock). Jednak jedyną pewną drogą do poprawy wydajności aplikacji są testy i pomiary.

Blokady odczytu i zapisu Wcześniej w tym rozdziale sugerowaliśmy, że sposobem na uniknięcie konieczności synchroni­ zacji dostępu do danych jest zapewnienie ich niezmienności - w środowisku .NET dowolna liczba jednocześnie wykonywanych wątków może bezpiecznie korzystać z tych samych danych, o ile tylko nie próbują ich modyfikować. Czasami jednak mogą się zdarzyć frustrujące sytu­ acje, w których używane dane są prawie przeznaczone tylko do odczytu. Na przykład witryna WWW może prezentować cytat dnia, który najprawdopodobniej będzie się zmieniał tylko raz dziennie, lecz jednocześnie będzie używany na każdej spośród setek stron udostępnianych przez nią w ciągu każdej sekundy. Wzajemnie wykluczające się blokady tworzone przy wykorzystaniu klasy Mon i tor, w których blokadę może w danej chwili uzyskać tylko jeden wątek, wydają się niezbyt dobrze pasować do tej sytuacji . Może się okazać, że takie wspólne dane stanowią wąskie gardło systemu wszystkie wątki muszą się ustawić w kolejce, by jeden po drugim uzyskiwać dostęp do zasobu, choć w rzeczywistości jakiekolwiek problemy mogą się pojawić tylko jeden raz w ciągu dnia. I właśnie w takich przypadkach można skorzystać z blokad odczytu i zapisu. Idea stojąca u ich podstaw jest taka, by podczas pobierania blokady określać, czy musimy zmodyfikować daną, czy też jedynie ją odczytać. Blokada pozwala, by dana była jednocześnie odczytywana przez dowolną liczbę wątków, jeśli jednak wątek zechce ją zmodyfikować, to Podstawowe narzędzia synchronizacji

I

651

najpierw będzie musiał poczekać na zakończenie operacji wykonywanych przez wątki posia­ dające blokadę do odczytu, a następnie uzyskać blokadę do zapisu. Kiedy to już nastąpi, to wszystkie inne wątki - niezależnie od tego, czy chodzi im tylko o odczyt danych, czy też o ich modyfikację - będą musiały poczekać na zwolnienie blokady. Innymi słowy, blokady tego typu umożliwiają nam wykonywanie dowolnej liczby jednoczesnych operacji odczytu, jed­ nak operacja zapisu wymaga wyłącznego dostępu. (Praktyczne szczegóły tych blokad, jak to zazwyczaj bywa, są nieco bardziej skomplikowane, gdyż w razie ich stosowania należy uni­ kać sytuacji, w których niekończący się strumień operacji odczytu uniemożliwia wykonanie operacji zapisu. Może się zdarzyć, że trzeba będzie wstrzymać nowe operacje odczytu, nawet jeśli jakieś inne są już w trakcie realizacji, by umożliwić wykonanie oczekujących zapisów) . Choć w teorii może się to wydawać dobrym rozwiązaniem, to jednak praktyczne korzyści czasami nie dorównują tym teoretycznym. Nie powinno się nawet myśleć o zastosowaniu tego typu blokad, jeśli nie występują zauważalne problemy z wydajnością aplikacji wykorzystują­ cej prostsze sposoby blokowania bazujące na monitorach. Opisywany tu sposób blokowania jest znacznie bardziej złożony, istnieje zatem spore prawdopodobieństwo, że zastosowanie go doprowadzi do spowolnienia rozwiązania, zwłaszcza w przypadkach, gdy zjawisko rywali­ zacji nie występuje zbyt często. (Jest całkiem prawdopodobne, że przedstawiony wcześniej przykład z witryną WWW oraz cytatem dnia należałoby zaliczyć do tej kategorii . Jeśli cytat jest zwyczajnym łańcuchem znaków, to jak długo trwa pobranie odwołania do tego łańcucha? Nawet w przypadku obsługi setek żądań na sekundę szanse na wystąpienie rywalizacji w trakcie tej operacji są bardzo małe) . Sytuacji bynajmniej nie poprawia fakt, że pierwsza implementacja blokad odczytu i zapisu zastosowana w .NET Framework - klasa ReaderWri t erlo c k - była, szczerze mówiąc, niezbyt dobra. Rozwiązanie bazujące na monitorach musiałoby być naprawdę bardzo marnej jakości, by zastosowanie klasy ReaderWri terlock mogło się wydać bardziej interesujące. Niestety niektórych problemów związanych z tą klasą nie da się naprawić inaczej niż w sposób, który doprowa­ dziłby do kłopotów z istniejącym kodem, dlatego w .NET 3.5 wprowadzono jej znacznie lepszy zamiennik - klasę ReaderWri t erlockSl i m . Jeśli naprawdę potrzebujemy mechanizmu blokady odczytu i zapisu, to zawsze należy korzystać z tej nowszej klasy, chyba że absolutnie nie­ zbędna jest możliwość uruchamiania aplikacji w starszych wersjach platformy .NET. Trzeba pamiętać, że w odróżnieniu od klasy ReaderWri t erlo c k jej nowsza alternatywa implementuje interfejs I Di sposab l e, dlatego też należy zadbać o to, by obiekty tej klasy zwalniać w odpowied­ nim momencie. Oznacza to, że jeśli będziemy ich używali jako szczegółów implementacji naszej klasy, to zapewne także i ona będzie musiała implementować interfejs I Di sposabl e.

Muteksy Klasa Mutex udostępnia mechanizm blokowania przypominający monitory. Jego nazwa pocho­ dzi od angielskich słów mutually exclusive - wzajemnie wykluczający - które oznaczają, że w danej chwili tylko jeden wątek może posiadać blokadę . Uzyskanie muteksu jest znacznie bardziej kosztowne niż uzyskanie monitora, gdyż zawsze wiąże się z zaangażowaniem sys­ temowego mechanizmu szeregującego. Dzieje się tak, gdyż działanie muteksów nie ogranicza się do jednego procesu. Można utworzyć obiekt Mutex o pewnej nazwie i jeśli inny proces dzia­ łający w ramach tej samej sesji użytkownika Windows utworzy inny obiekt Mutex o tej samej nazwie, to oba te obiekty będą się w rzeczywistości odwoływać do tego samego obiektu syn­ chronizacyjnego systemu Windows. A zatem by uzyskać muteks, nie wystarczy być jedynym wątkiem w aplikacji, który posiada blokadę - trzeba być jedynym wątkiem w całej sesji

652

I

Rozdział 16. Wątki i kod asynchroniczny

użytkownika Windows, który będzie ją posiadał. (Istnieje nawet możliwość utworzenia global­ nego obiektu Mutex obejmującego wszystkie sesje użytkowników, co oznacza, że by uzyskać muteks, dany wątek będzie musiał być jedynym w całym systemie, który posiada blokadę) . Jeśli utworzymy muteks bez nazwy, to będzie on miał zasięg lokalny, obejmujący bieżący pro­ ces, jednak pomimo to będzie on odwoływał się do systemu operacyjnego, gdyż klasa Mutex jest w rzeczywistości opakowaniem dla obiektów muteksów udostępnianych przez jądro sys­ temu operacyjnego Windows.

I nne mechanizmy synchronizacj i Oczywiście monitory nie służą wyłącznie do blokowania - dzięki oczekiwaniu i przesyłaniu sygnałów zapewniają one także możliwość koordynacji działania. Jednak .NET Framework udostępnia także inne, bardziej wyspecjalizowane narzędzia służące do tego celu.

Zdarzen ia Zdarzenia zapewniają usługi przypominające klasę Wai t Fori t, którą przedstawiliśmy na lis­ tingu 16.14 - zdarzenie jest wartością logiczną, na którą można czekać. Lecz w przeciwień­ stwie do prostego jednokrotnego mechanizmu, który zaimplementowaliśmy w przedstawionym wcześniej przykładzie, zdarzenie może wielokrotnie przechodzić pomiędzy dwoma dostępnymi stanami. Platforma .NET udostępnia dwie klasy: Manual Reset Event oraz Au toReset E v en t . Druga z nich automatycznie przechodzi do stanu domyślnego po uruchomieniu oczekujących wątków, natomiast pierwsza pozostaje w tak zwanym stanie sygnalizującym aż do chwili, gdy jawnie go zmienimy. Stosowanie klasy AutoResetEvent może być kłopotliwe, bowiem liczba wysłanych sygna­ łów niekoniecznie będzie odpowiadać temu, ile razy wątki faktycznie zostały zwol­ nione. Jeśli taki sygnał przekażemy dwukrotnie, raz za razem, w chwili gdy żadne wątki nie będą oczekiwać, to nie zostanie on uwzględniony - stan po przesłaniu drugiego sygnału będzie taki sam jak po przesłaniu pierwszego. Może to prowadzić do błędów związanych z przypadkowym traceniem sygnałów, co może skutkować zawie­ szaniem się kodu. Tej klasy należy zatem używać z wielką ostrożnością.

Te typy są opakowaniami dla podstawowych mechanizmów synchronizacyjnych systemu Win­ dows, zatem, podobnie jak typu Mut ex, można ich używać do koordynacji działania odręb­ nych procesów. Oczywiście oznacza to także, że stosując je, narażamy się na koszty związane z ingerencją w działanie systemowego mechanizmu szeregującego. W platformie .NET 4 wprowadzono rozwiązanie alternatywne o nazwie Man ual Res etEventSl i m . Klasa ta korzysta z technik „aktywnego oczekiwania" przypominających nieco sposób dzia­ łania blokady Spi n lock w krótkich okresach czasu. A zatem podobnie jak klasa Mon i tor będzie ona używać systemowego mechanizmu szeregującego wyłącznie w sytuacjach, gdy oczekiwanie będzie konieczne. A więc jeśli tylko pełne możliwości klasy Manua l Res et Event nie są nam abso­ lutnie niezbędne (czyli na przykład musimy koordynować działanie odrębnych procesów) i jeśli korzystamy z platformy .NET w wersji 4. lub nowszej, to lepszym rozwiązaniem będzie użycie klasy Man ual Res etEventS l i m .

Podstawowe narzędzia synchronizacji

I

653

Od l iczan ie W bibliotece klas .NET 4 pojawiła się nowa klasa Countdown Event, która zapewnia wygodne rozwiązanie często występującego problemu: uzyskania informacji o tym, kiedy praca została wykonana. Czytelnik zapewne pamięta, że już wcześniej spotkaliśmy się z nim podczas korzy­ stania z puli wątków. Umieściliśmy w kolejce kilka zadań do wykonania, lecz nie mieliśmy prostej możliwości dowiedzenia się, kiedy ich realizacja została zakończona. Jednym z rozwią­ zań tego problemu byłoby skorzystanie z możliwości biblioteki Task Parallel Library (którą zajmiemy się już niebawem), jednak moglibyśmy także użyć klasy Countdown Even t . Jest to bardzo prosta klasa. Dla każdego uruchamianego zadania należy wywołać metodę AddCount . (Jeśli z góry wiemy, ile zadań mamy wykonać, to możemy też przekazać odpowiednią liczbę w wywołaniu konstruktora) . Po zakończeniu każdego zadania wywoływana jest metoda S i gnal . Natomiast w przypadku, gdy musimy zaczekać na wykonanie pozostałych zadań (na przykład przed zakończeniem działania programu), wystarczy wywołać metodę Wai t .

BlockingCollection W przestrzeni nazw Sys t em . Col l ect i on s . Con c urrent dostępnych jest wiele różnych klas zapro­ jektowanych specjalnie do wykorzystania w środowiskach wielowątkowych. Wyglądają one nieco odmiennie od normalnych kolekcji, gdyż zostały pomyślane w taki sposób, by można z nich było korzystać bez konieczności blokowania. Oznacza to, że nie są one w stanie udostęp­ nić nam żadnych możliwości, które bazują na zachowaniu jakiejkolwiek spójności pomiędzy dwoma momentami w czasie. Na przykład nie zapewniają one możliwości stosowania indek­ sów liczbowych, gdyż liczba elementów w kolekcji może się zmieniać, co zaobserwowaliśmy, próbując używać kolekcji L i st w programie wielowątkowym przedstawionym na listingu 16.1 1 . A zatem nie są to jedynie przystosowane do działania w środowiskach wielowątko­ wych zwyczajne wersje klas kolekcji - są to klasy, których API zostało zaprojektowane pod kątem wykorzystania w aplikacjach wielowątkowych bez konieczności stosowania blokowania. Klasa Bl ocki ngCo 1 1 ect i on nie jest jedynie kolekcją do zastosowań wielowątkowych - udostęp­ nia ona także dodatkową możliwość koordynacji. Pozwala, by wątki oczekiwały na pojawie­ nie się elementów w kolekcji . Użycie metody Take spowoduje zablokowanie wątku w przy­ padku, gdy kolekcja jest pusta . Kiedy w kolekcji pojawią się dane, metoda ta zwróci jeden element. W dowolnej chwili dowolna liczba wątków może oczekiwać na zakończenie wywoła­ nia metody Take, a ponadto w każdym momencie inne wątki mogą wywołać metodę Add . Jeśli metoda Add zostanie wywołana dostatecznie dużo razy, by wszystkie wątki oczekujące na zakoń­ czenie metody Take otrzymały po elemencie danych, i będzie wywoływana dalej, to dopiero wtedy dane będą faktycznie gromadzone w kolekcji. Jeśli natomiast w momencie wywołania metody Take kolekcja nie będzie pusta, to element zostanie zwrócony natychmiast. Dzięki temu można stworzyć rozwiązanie, w którym grupa kilku wątków będzie przetwa­ rzać elementy robocze wygenerowane przez inną grupę wątków. Kolekcja B l o c k i ngCol l ect i on działa jako bufor - jeśli dane będą szybciej generowane niż przetwarzane, to będą groma­ dzone w kolekcji, natomiast w przeciwnym przypadku kolekcja będzie efektywnie blokować wątki do momentu pojawienia się nowych danych. Z kolekcji B l o c k i ngCol l ect i on można korzystać w aplikacjach WPF, które muszą wykonywać długotrwałe operacje w tle. Wątek obsługi interfejsu użytkownika może umieszczać w kolekcji zadania do wykonania, które następnie będą pobierane i przetwarzane przez wątki robocze .

654

I

Rozdział 16. Wątki i kod asynchroniczny

Takie rozwiązanie nie różni się szczególnie od wykorzystania puli wątków, jednak zapewnia możliwość ograniczenia liczby używanych wątków roboczych - jeśli do przetwarzania zadań w tle wykorzystywany jest tylko jeden wątek, to kod służący do synchronizacji będzie mógł być znacznie prostszy. Dowiedzieliśmy się już, jak można tworzyć wątki jawnie, i poznaliśmy narzędzia niezbędne do zapewnienia prawidłowego działania programów w środowiskach wielowątkowych. Kolej­ nym zagadnieniem, którym się zajmiemy, będzie model programowania asynchronicznego, w którym dodatkowe wątki nie są tworzone w sposób jawny. Także w tym przypadku będą nam potrzebne poznane wcześniej techniki blokowania i synchronizacji, gdyż wciąż będziemy obracać się w świecie programowania współbieżnego. Zmienimy jedynie używany styl pro­ gramowania .

Programowan ie asynchron iczne Niektóre rzeczy z natury są wolne. Odczyt całej zawartości muzycznej płyty CD, pobranie dużego pliku z serwera na drugim końcu świata przy wykorzystaniu połączenia o niewielkiej przepustowości bądź też odtwarzanie dźwięków - każdy z tych procesów podlega ograni­ czeniom powodującym, że ich wykonanie zajmie dużo czasu: sekundy, minuty, a może nawet godziny. Jak wszystkie te operacje będą wyglądały z punktu widzenia programisty? Otóż wcale nie muszą one wyglądać inaczej niż pozostałe operacje, które można wykonać szybciej . Nasz kod składa się z sekwencji instrukcji zapisanych jedna po drugiej, a wykonanie niektórych z nich zajmuje więcej czasu niż wykonanie innych. To całkiem zrozumiałe . Na przykład jeśli nasz kod wywołuje metodę Down l oadStri ng klasy WebCl i e n t , to program nie przejdzie do kolejnej instrukcji aż do momentu zakończenia wywołania. Dzięki temu nie tylko wiemy, co nasz kod robi, lecz także znamy kolejność, w jakiej są wykonywane poszczególne operacje. Interfejsy API tego typu są określane jako synchroniczne. Oznacza to, że czas zakończenia wywołania jest uzależniony od momentu zakończenia wykonywania operacji; proces realizacji kodu jest ściśle zsynchronizowany z wykonywaniem kolejnych zadań. API tego typu są także czasami określane jako blokujące, gdyż blokują wywołujący wątek aż do czasu zakończenia wykonywanych czynności . Blokujące interfejsy API mogą przysparzać problemów podczas obsługi interfejsu użytkow­ nika, gdyż zablokowany wątek nie może nic zrobić aż do momentu zakończenia aktualnie wykonywanej operacji . Powinowactwo do wątków sprawia, że kod obsługujący poczynania użytkownika musi być wykonywany w odpowiednim wątku, jeśli zatem wątek ten będzie zajęty, interfejs użytkownika aplikacji przestanie reagować. Korzystanie z programów, które nagle przestają reagować na wykonywane czynności, jest denerwujące - takie aplikacje wydają się zamierać za każdym razem, gdy wykonanie operacji zajmuje zbyt dużo czasu, przez co korzy­ stanie z nich może być bardzo frustrujące. Brak reakcji na czynność wykonaną przez użytkow­ nika w czasie krótszym od 100 milisekund w zupełności wystarczy, by doprowadzić do utraty przez niego koncentracji . (Sytuacja staje się jeszcze gorsza, gdy w samym interfejsie używane są aplikacje - w takich przypadkach przypadkowe opóźnienie sięgające 15 milisekund może przekształcić płynną animację w prawdziwy przerywany koszmar) .

Programowanie asynchroniczne

I

655

Jednym z rozwiązań problemów tego typu są wątki. Jeśli wszystkie potencjalnie długotrwałe zadania będą wykonywane w osobnych wątkach w żaden sposób niezwiązanych z obsługą interfejsu użytkownika, to aplikacja będzie szybko i sprawnie reagować na jego poczynania. Niemniej jednak takie rozwiązanie czasami może sprawiać wrażenie zbyt skomplikowanego w wielu przypadkach długotrwałe operacje nie są w rzeczywistości wykonywane synchronicz­ nie. W ramach przykładu przeanalizujmy podstawowe operacje takie jak zapis i odczyt danych z karty sieciowej lub z dysku sieciowego. Działające w ramach jądra systemu sterowniki obsłu­ gujące dyskowe i sieciowe operacje wejścia-wyjścia otrzymują od systemu operacyjnego pole­ cenie rozpoczęcia wykonywania odpowiednich czynności . System oczekuje, że odpowiednio skonfigurują one urządzenia sprzętowe, po czym niemal natychmiast przekażą sterowanie ponownie do niego - Windows jest bowiem oparty na założeniu, że większość długotrwałych zadań będzie wykonywana asynchronicznie i że realizacja kodu nie musi ściśle odpowiadać postępom wykonywanych prac. Model asynchroniczny nie musi się wcale ograniczać tylko do wewnętrznego działania systemu operacyjnego Windows - istnieją także publicznie dostępne asynchroniczne interfejsy API. Ich wywołania zazwyczaj kończą się bardzo szybko, na długo przed faktycznym zakończeniem wykonywanych operacji, natomiast sam fakt zakończenia prac jest określany dzięki wyko­ rzystaniu mechanizmu powiadomień lub okresowego odpytywania (ang. polling) . Szczegóły rozwiązań są różne i zależą od konkretnego API, niemniej jednak ogólna zasada działania jest identyczna. Wiele synchronicznych interfejsów API to w rzeczywistości kod, który rozpoczyna działanie operacji asynchronicznych i usypia wątek aż do momentu ich wykonania. Asynchroniczne interfejsy API wydają się właśnie tym, czego trzeba do tworzenia sprawnie działających, interaktywnych aplikacji7 . Można zatem uznać, że stosowanie kilku wątków w celu użycia synchronicznych interfejsów API, które w rzeczywistości są jedynie opakowaniem dla operacji wykonywanych asynchronicznie, jest dosyć niedorzeczne . Zamiast tworzyć nowe wątki wszędzie tam, gdzie to możliwe, możemy zacząć stosować asynchroniczne interfejsy API bezpośrednio, usuwając w ten sposób niepotrzebnego pośrednika . .NET Framework definiuje dwa podstawowe wzorce operacji asynchronicznych. Istnieje wzo­ rzec niskopoziomowy zapewniający bardzo duże możliwości i odpowiadający faktycznemu sposobowi działania systemu Windows . Oprócz tego istnieje także wzorzec nieco wyższego poziomu, który jest nieco mniej elastyczny, lecz jednocześnie znacznie łatwiejszy do stosowania w kodzie związanym z obsługą interfejsu użytkownika .

Model programowania asynchronicznego Model programowania asynchronicznego (ang. Asynchronous Programming Model, w skrócie APM) jest wzorcem wykorzystywanym przez wiele asynchronicznych API dostępnych na plat­ formie .NET. Definiuje on często występujące mechanizmy umożliwiające określanie, kiedy zadanie zostało wykonane, gromadzenie wyników zakończonych prac oraz raportowanie błę­ dów, które wystąpiły podczas wykonywania operacji asynchronicznych .

7 W aplikacjach internetowych działających na serwerach asynchroniczne interfejsy API są zazwyczaj wyko­ rzystywane w nieco inny sposób. W tych przypadkach są one najbardziej użyteczne, gdy w ramach obsługi jednego żądania aplikacja musi skorzystać z kilku różnych zewnętrznych usług.

656

I

Rozdział 16. Wątki i kod asynchroniczny

Interfejsy API korzystające z tego modelu programowania udostępniają pary metod, których nazwy zaczynają się odpowiednio od słów Beg i n oraz End 8 . Na przykład klasa Soc ket dostępna w przestrzeni nazw Sys t em . Net . Soc ket zawiera wiele przykładów wykorzystania tego wzorca: B eg i nAccept oraz EndAccept, Beg i nSend oraz EndSend, Beg i n Connect oraz EndConnect i tak dalej . Konkretna sygnatura metody Begi n zależy od jej przeznaczenia. Na przykład metoda Beg i nConnect gniazda wymaga przekazania adresu, z jakim należy nawiązać połączenie, natomiast metoda Begi n Recei ve musi wiedzieć, gdzie należy umieszczać odczytywane dane oraz jak dużo tych danych jesteśmy gotowi odebrać. Niemniej jednak APM wymaga, by dwa ostatnie parametry każdej z tych metod zawsze miały taką samą postać; ma to być delegacja typu Asyn cCa l l back oraz obj ect . Dodatkowo może się także okazać konieczne, by metoda zwracała obiekt imple­ mentujący interfejs I Asyn c Res u l t . Poniżej przedstawiliśmy przykład pochodzący z klasy Dns należącej do przestrzeni nazw Sys t ern . Net. publ i c s t at i c IAsyncRes u l t Beg i nGetHos t Entry ( s t r i ng hostNameOrAddres s , AsyncCal l back reques tCal l ba c k , obj ect s tateObj ect

Kod wywołujący może przekazać n u l l jako wartość parametru typu AsyncCa l l back. Jeśli jed­ nak zostanie przekazana inna referencja, to typ implementujący APM jest zobowiązany do wywołania metody zwrotnej po zakończeniu operacji . Sygnatura delegacji A syncCa l l back wy­ maga, by metoda akceptowała argument typu I Asyn cRes ul t - implementacja APM przekaże ten sam obiekt I Asyn c Res u l t w wywołaniu metody zwrotnej oraz zwróci go jako wynik wywoła­ nia metody Beg i n . Obiekt ten reprezentuje wykonywaną asynchroniczną operację. Wiele klas pozwala na jednoczesne wykonywanie wielu operacji, a obiekty I Asyn cResul t pozwalają je rozróżniać. Przykład przedstawiony na listingu 16.16 pokazuje jeden ze sposobów korzystania z tego wzorca. Zamieszczony tam kod wywołuje asynchroniczną metodę Beg i nGetHost Entry udostęp­ nianą przez klasę Dn s . Poszukuje ona adresu IP komputera, zatem wymaga przekazania łańcucha znaków zawierającego nazwę tego komputera . Oprócz tego pobiera ona dwa standardowe argumenty APM: delegację i obiekt. Ten ostatni argument może być całkowicie dowolny wywoływana metoda w ogóle go nie używa, jedynie później przekazuje go z powrotem do naszego kodu. Moglibyśmy przekazać nul l jako wartość ostatniego argumentu metody Beg i nGet '"+Hos t En t ry, lecz w przykładzie przekazujemy liczbę, by zademonstrować, gdzie zostanie ona później zwrócona. APM udostępnia ten argument dlatego, że w przypadkach, gdy jednocze­ śnie wykonywanych jest więcej operacji asynchronicznych, zapewnia on wygodną możliwość skojarzenia z każdą z nich jakichś informacji. (Możliwość ta miała znacznie większe znaczenie we wcześniejszych wersjach języka C#, w których nie były dostępne ani metody anonimowe, ani wyrażenia lambda. W tamtych czasach argument ten stanowił najprostszy sposób przeka­ zania jakichś informacji do metody zwrotnej) .

Listing 16.16. Stosowanie APM

-

modelu programowania asynchronicznego

cl ass Program { s t at i c vo i d Ma i n (stri ng O args) {

8 Odpowiednio: rozpocznij i zakończ

-

przyp. tłum.

Programowanie asynchroniczne

I

657

Dns . Beg i nGetHos t Entry ( " hel i on . pl " , OnGetHostEntryComp l ete , 42) ; Consol e . ReadKey () ; s t at i c vo i d OnGetHo s t EntryCompl ete ( IAsyncRes u l t i ar) { I PHost Entry res u l t = Dns . EndGetHo s t Entry ( i ar) ; Consol e . Wr i teli ne (res u l t . Addres s li s t [O] ) ; Consol e . Wr i teli ne ( i ar . AsyncState) ;

Metoda Mai n czeka z zakończeniem programu aż do momentu naciśnięcia jakiegoś klawisza. Podobnie jak było w przypadku prac wykonywanych przy użyciu puli wątków, także i tu uruchomienie asynchronicznego żądania nie uchroniłoby programu przed zamknięciem, zanim zdążyłoby ono zostać obsłużone - stąd zastosowanie metody Read Key. (Znacznie solidniejszym rozwiązaniem odpowiednim do wykorzystania w rzeczywistym programie, który musiałby oczekiwać na zwrócenie wyników asynchronicznej operacji, byłoby zastosowanie opisanej wcze­ śniej klasy Countdown Event ) . Klasa Dns wywoła naszą metodę OnGetHostEntryCompl ete po odszukaniu podanego adresu kom­ putera. Należy zwrócić uwagę, że pierwszą czynnością wykonywaną w tej metodzie jest wywo­ łanie metody EndGetHostEntry - drugiej połówki APM. Metoda End zawsze wymaga przeka­ zania obiektu I Asyn c Resul t związanego z realizowanym wywołaniem. Pamiętamy zapewne, że identyfikuje on wykonywaną operację asynchroniczną, dzięki czemu metoda EndGet Hos t En try jest w stanie określić, które wyniki nas interesują. '

. '

Model programowania asynchronicznego nie określa, w jakim wątku ma zostać wywo­ łana metoda zwrotna. W praktyce często, choć nie zawsze, będzie to jeden z wątków należących do puli wątków. Niektóre konkretne implementacje APM mogą precy­ zyjnie określać i gwarantować to, w jakim wątku metoda ta zostanie wywołana, jednak w większości przypadków żadnych takich gwarancji nie ma. A ponieważ zazwyczaj nie będziemy wiedzieć, który to będzie wątek, konieczne będzie podejmowanie tych samych środków zapobiegawczych, których musimy używać w przypadku pisania wielowątkowego kodu, gdzie wątki sami jawnie tworzymy. Na przykład w aplikacjach WPF lub Windows Forms, chcąc zaktualizować interfejs użytkownika po wykonaniu operacji asynchronicznej, trzeba będzie skorzystać z klasy Synchron i zati onContext lub z innego podobnego mechanizmu.

Metoda End stosowana w APM zwraca dowolne dane uzyskane podczas wykonywania ope­ racji. W naszym przypadku operacja zwraca jedną daną typu I PHostEntry, może się jednak zdarzyć, że inne implementacje będą zwracać ich więcej, korzystając przy tym z parametrów wyjściowych (out ) lub referencyjnych (ref) . Nasz przykład z listingu 16.16 wyświetla zwrócony wynik, a następnie wartość właściwości Asyn cState obiektu I Asyn c Res ul t, którą będzie liczba 42 - to właśnie w tym miejscu pojawia się wartość przekazana jako ostatni argument wywo­ łania metody Beg i nGetHostEntry . Nie jest to jedyny sposób korzystania z modelu programowania asynchronicznego. Można także zamiast delegacji przekazać n ul l . Dysponujemy też trzema innymi możliwościami, a wszyst­ kie z nich są w jakiś sposób związane z obiektem I Asyn c Res ul t zwracanym przez wywołanie metody Beg i n . Można sprawdzać wartość właściwości I sComp l eted tego obiektu, by dowiedzieć się, czy operacja została zakończona . W dowolnej chwili można też wywołać odpowiednią

658

I

Rozdział 16. Wątki i kod asynchroniczny

metodę End - jeśli operacja jeszcze się nie zakończyła, to wywołanie to spowoduje zablokowa­ 9 nie kodu wywołującego aż do momentu jej zakończenia . Można także skorzystać z właściwości A syn cWa i t H a n d l e . Zwraca ona obiekt stanowiący opakowanie uchwytu synchronizacyjnego Win32, do którego zostanie przesłany sygnał po zakończeniu wykonywania operacji asyn­ chronicznej . (Ta ostatnia możliwość jest rzadko stosowana i przysparza pewnych problemów związanych z prawami własności oraz okresem istnienia uchwytu - bardziej szczegółowe informacje na ten temat można znaleźć w dokumentacji MSDN. Wspomnieliśmy o niej w tej książce wyłącznie ze względu na pedantyczne poczucie obowiązku wyczerpania opisywanych zagadnień) . '

.'

!.---I.I"'.

,

Niezależnie od wybranego sposobu oczekiwania na zakończenie operacji wywołanie metody End jest wymagane. Nie jest istotne, czy interesuje nas wynik operacji, czy nie metodę tę i tak trzeba wywołać. Jeśli tego nie zrobimy, mogą się pojawić wycieki zasobów.

Operacje asynchroniczne mogą prowadzić do występowania wyjątków. Jeśli przyczyną tych wyjątków będą nieprawidłowe dane wejściowe, takie jak przekazanie pustej referencji, to wyją­ tek zostanie zgłoszony przez metodę B eg i n . Istnieje jednak możliwość, że coś się nie uda pod­ czas realizacji operacji, na przykład w trakcie jej wykonywania zostanie zerwane połączenie sieciowe . W takim przypadku wyjątek zgłosi metoda End. Model programowania asynchronicznego jest często wykorzystywany w bibliotece klas .NET Framework i choć stanowi on efektywny i elastyczny sposób obsługi operacji asynchronicznych, to jednak stosowanie go w ramach obsługi interfejsu użytkownika jest dosyć niewygodne . Metody zwrotne obsługujące zakończenie operacji są zazwyczaj wykonywane w dowolnych wątkach, co sprawia, że nie można w nich aktualizować interfejsu użytkownika . Z kolei wsparcie dla wielu jednocześnie realizowanych operacji asynchronicznych, możliwe dzięki iden­ tyfikacji każdej z nich przy użyciu obiektu I Async Res u l t, może być użyteczne w środowiskach serwerowych, jednak w przypadku tworzenia zwyczajnych aplikacji klienckich prowadzi do niepotrzebnego skomplikowania kodu. Dlatego też istnieje alternatywny wzorzec realizacji operacji asynchronicznych znacznie wygodniejszy w przypadku tworzenia kodu związanego z obsługą interfejsu użytkownika .

Programowanie asynchroniczne bazujące na zdarzeniach Niektóre klasy udostępniają alternatywny wzorzec programowania asynchronicznego. Opera­ cję rozpoczyna się w nim zazwyczaj od wywołania metody, której nazwa kończy się słowem Async; przykładem może być metoda Down l oadDataAsync klasy WebCl i en t . W odróżnieniu od przedstawionego wcześniej APM, w tym przypadku w wywołaniu metody nie jest przeka­ zywana żadna delegacja. O zakończeniu operacji jesteśmy informowani przy użyciu odpowied­ niego zdarzenia takiego jak Down l oadDataComp l eted. W celu zapewnienia, że zdarzenie zostanie zgłoszone w tym samym wątku, w którym rozpoczęto realizację operacji, klasy implementu­ jące ten wzorzec muszą korzystać z klasy Syn chron i zat i on Context (bądź powiązanej z nią klasy AsyncOperat i onManager) . A zatem w przypadku obsługi interfejsu aplikacji oznacza to, że zdarzenie informujące o zakończeniu operacji zostanie zgłoszone w wątku obsługi interfejsu użytkownika.

9 Ta możliwość nie zawsze jest dostępna. Na przykład jeśli spróbujemy wykonać takie przedwczesne wywołanie metody End w wątku obsługi interfejsu użytkownika aplikacji Silverlight, to zostanie zgłoszony wyjątek.

Programowanie asynchroniczne

I

659

W rzeczywistości jest to model jednowątkowy. Zapewnia on poprawę szybkości i wrażliwości interfejsu użytkownika związaną z wykorzystaniem działania asynchronicznego, a jednocze­ śnie jest mniej kłopotliwy w użyciu niż kod wielowątkowy. W niektórych scenariuszach, gdy wykorzystanie tego wzorca jest możliwe, stanowi on najlepszy wybór, gdyż jest zdecydo­ wanie prostszy od wszelkich innych rozwiązań alternatywnych . Jednak nie zawsze można z niego skorzystać, ponieważ niektóre klasy udostępniają tylko rozwiązania wykorzystujące APM. (Jeszcze inne w ogóle nie zapewniają możliwości działania asynchronicznego. W takim przypadku uzyskanie sprawnie reagującego interfejsu użytkownika nie wymaga stosowania jakiejkolwiek z technik opisanych w tym rozdziale) . Oczywiście jednowątkowy kod asynchroniczny jest nieco bardziej złożony do kodu sekwencyjnego, zatem tworzenie go nie jest tak zupełnie bezproblemowe. Na przykład trzeba uważać, by nie wykonywać jednocześnie kilku operacji asynchronicznych, pomiędzy którymi mogą wystąpić konflikty. Poza tym komponenty korzystające z tego wzorca będą wywoływały nasze metody zwrotne we właściwym wątku tylko i wyłącz­ nie w przypadku, kiedy to my w odpowiednim wątku ich użyjemy. Trzeba mieć świadomość, że w razie wykorzystania tego wzorca wraz z innymi mechanizmami wielowątkowymi operacja zainicjowana w wątku roboczym nie zostanie zakończona w wątku obsługi interfejsu użytkownika.

Istnieją dwie opcjonalne możliwości, które daje wzorzec programowania asynchronicznego bazujący na zdarzeniach. Niektóre klasy udostępniają także powiadomienia przekazujące infor­ macje o postępach realizowanej operacji; na przykład klasa WebCl i ent udostępnia zdarzenie Down l oad ProgressC hanged . (Te zdarzenia także są zgłaszane w oryginalnym wątku) . Oprócz tego klasa może zapewniać możliwość anulowania realizowanej operacji asynchronicznej - przy­ kładem jest klasa WebCl i ent, która udostępnia metodę Can cel Asyn c .

Doraźne operacje asynchron iczne Nie istnieje żadna zasadnicza konieczność, by nasz kod korzystał bądź to z APM, bądź z modelu programowania asynchronicznego bazującego na zdarzeniach. Są to jedynie pewne konwen­ cje. Czasami można się spotkać z kodem, który wykorzystuje swoje własne niezwykłe kon­ wencje wykonywania operacji asynchronicznych. Zazwyczaj zdarza się to, gdy postać kodu jest uzależniona od pewnych czynników zewnętrznych. Na przykład w przestrzeni System . Thread i ng została zdefiniowana klasa Overl apped udostępniająca zarządzaną reprezentację asynchro­ nicznego mechanizmu systemowego. System Windows nie dysponuje żadnym bezpośrednim odpowiednikiem któregokolwiek ze wzorców asynchronicznych stosowanych na platformie .NET, a jako metody zwrotne są w nim zazwyczaj używane wskaźniki do funkcji. Klasa Over 4 l apped naśladuje ten sposób działania, zezwalając na przekazywanie delegacji jako argu­ mentu wywołania swoich metod. Jeśli chodzi o ideę działania, nie różni się to szczególnie od APM - rozwiązanie to nie jest jedynie w pełni zgodne z tym wzorcem. Standardowe wzorce asynchroniczne są użyteczne, jednak operują na dosyć niskim poziomie. Jeśli konieczna jest koordynacja działania większej liczby operacji, to ilość pracy, jaką trzeba wykonać, może być bardzo duża, zwłaszcza jeśli w grę wchodzi solidna obsługa błędów oraz zapewnienie możliwości przerwania operacji asynchronicznych. Biblioteka Task Parallel Library (w skrócie TPL) zapewnia znacznie bardziej wszechstronny sposób pracy z wieloma jednocze­ śnie wykonywanymi operacjami asynchronicznymi.

660

I

Rozdział 16. Wątki i kod asynchroniczny

Task Paral lel Library W wersji 4. platformy .NET pojawiła się biblioteka Task Parallel Library - zbiór klas zdefi­ niowanych w przestrzeni nazw System . Threadi ng . Tas ks, które ułatwiają koordynację prac wyko­ nywanych współbieżnie. Pod niektórymi względami TPL przypomina z zewnątrz pulę wątków, gdyż pozwala na przesyłanie niewielkich elementów roboczych (zadań) do wykonania oraz jest w stanie określić, ile wątków należy uruchomić w celu wykonania wszystkich niezbędnych ope­ racji. Jednak TPL udostępnia także różne usługi, z których nie możemy korzystać w przypadku jawnego stosowania puli wątków. Dotyczy to zwłaszcza takich zagadnień jak obsługa błędów, anulowanie wykonywanych operacji asynchronicznych oraz zarządzanie zależnościami pomię­ dzy poszczególnymi zadaniami. TPL pozwala kojarzyć ze sobą zadania . Istnieje na przykład możliwość zdefiniowania relacji rodzic-dziecko, dzięki której wszystkie zadania o niższym priorytecie będą oczekiwały na zakończenie zadania o priorytecie wyższym. Można także zażądać, by zakończenie jednego zadania spowodowało uruchomienie innego. W przypadku tworzenia kodu asynchronicznego i współbieżnego implementacja obsługi błędów jest trudnym i złożonym zadaniem. Co zrobić, gdy jakaś operacja jest realizowana przez 20 współbieżnie działających wątków i w jednym z nich wystąpią problemy, podczas gdy pozo­ stałe będą prawidłowo realizowane, zostały już zakończone bądź jeszcze w ogóle ich nie uru­ chomiono? TPL udostępnia system pozwalający w uporządkowany sposób zatrzymać pracę i zgromadzić wszystkie błędy, jakie wystąpiły, w jednym miejscu. Mechanizmy konieczne do zatrzymania pracy w przypadku wystąpienia błędów są przydatne, gdy chcemy mieć możliwość zatrzymania realizowanych operacji z jakiegoś powodu takiego jak naciśnięcie przycisku Anuluj przez użytkownika . Zaczniemy od przedstawienia najważniejszego pojęcia stosowanego w TPL, którym jest zadanie, co zresztą nie stanowi szczególnego zaskoczenia.

Zadan ia Zadanie jest pewną czynnością, jaką program musi wykonać. Jest ona reprezentowana przez instancję klasy Tas k zdefiniowanej w przestrzeni nazw Sys t em . Threadi ng . Tas k s . Nie określa ona dokładnie, w jaki sposób dana czynność zostanie wykonana. Może być ona chocby wywołaniem metody, lecz równie dobrze może to być operacja asynchroniczna wykonywana bez koniecz­ ności powiązania jej z jakimś wątkiem - TPL zapewnia na przykład możliwość tworzenia obiektów zadań wykonywanych przy użyciu implementacji APM. Przykład przedstawiony na listingu 16.17 pokazuje, w jaki sposób można utworzyć zadanie wykonujące fragment kodu.

Listing 16.17. Zadanie wykonujące fragment kodu us i ng Sys tem ; us i ng Sys tem . Thread i ng . Tas ks ; namespace Tpl Examp l es { cl ass Program { stat i c voi d Mai n (stri ng O arg s )

Task Parallel Library

I

661

Tas k . Factory . StartNew (Go , " Jeden " ) ; Tas k . Factory . StartNew (Go , " Dwa " ) ; Consol e . ReadKey () ; stat i c voi d Go (obj ect name) { for ( i nt i = O ; i < 100 ; ++ i ) { Consol e . Wri teli ne ( " { O } : { 1 } " , name , i ) ;

Klasa Tas k udostępnia statyczną właściwość Factory. Zwraca ona obiekt Tas k Fa ctory, którego można użyć do utworzenia nowego zadania. TPL definiuje ten dodatkowy poziom abstrakcji, który stanowi klasa Tas k Factory, aby można było określać i stosować różne strategie tworzenia zadań. Domyślny obiekt wytwórczy zwracany przez właściwość Tas k . Fact ory tworzy nowe zadania wykonujące kod przy użyciu puli wątków, istnieje jednak możliwość tworzenia takich obiektów wytwórczych, które będą działały inaczej . Można na przykład przygotować obiekt wytwórczy tworzący zadania, które będą wykonywane w wątku obsługi interfejsu użytkownika. Metoda StartNew obiektu T a s k Factory tworzy nowe zadanie code-based (ang. code-based task) . Można do niego przekazać delegację - przyjmie ono metodę bezargumentową bądź z jednym argumentem typu Obj ect . W przypadkach gdy chcemy przekazać więcej argumentów, można użyć tej samej sztuczki z zastosowaniem wyrażenia lambda, którą przedstawiliśmy w przy­ kładzie z listingu 16.4. Listing 16 .18 wykorzystuje to rozwiązanie, by przekazać do metody Go dwa argumenty, używając przy tym bezargumentowej przeciążonej wersji metody StartNew. (Pusta para nawiasów () informuje kompilator C#, że należy utworzyć bezargumentowe wyra­ żenie lambda, które stanie się metodą wywoływaną przez metodę StartNew) .

Listing 16.18. Przekazywanie większej liczby argumentów dzięki zastosowaniu wyrażenia lambda s t at i c vo i d Mai n (s tri ng O arg s ) { Tas k . Factory . St artNew ( () => Go ( " Jeden " , 100) ) ; Tas k . Factory . St artNew ( () => Go ( " Dwa " , 500) ) ; Consol e . ReadKey () ; s t at i c vo i d Go ( s tr i ng name , i nt i terat i on s ) { for ( i nt i = O ; i < i terat i ons ; ++i ) { Consol e . Wr i teli ne ( " { O } : { 1 } " , n ame , i ) ;

Dwa ostatnie przykłady wyglądają bardzo podobnie do przedstawionych wcześniej przykładów wykorzystujących pulę wątków. Występuje w nich także ten sam problem: nie zapewniają możliwości uzyskania informacji, kiedy zadanie zostało wykonane; właśnie z tego powodu użyliśmy kiepskiego rozwiązania, w którym czekamy z zamknięciem programu na naciśnięcie klawisza, dzięki czemu nie zakończy się on przed wykonaniem asynchronicznych operacji .

662

I

Rozdział 16. Wątki i kod asynchroniczny

Na szczęście zadania udostępniają nam znacznie lepsze rozwiązanie: pozwalają nam poczekać, aż zostaną wykonane. Klasa Tas k definiuje metodę Wai t, która blokuje działanie wątku aż do momentu zakończenia zadania. Jest to metoda instancji, zatem można ją wywołać dla każdego zadania jeden raz. Dostępna jest także statyczna metoda Wai tAl l , w której wywołaniu przeka­ zywana jest tablica obiektów Tas k . Metoda ta powoduje zablokowanie wątku aż do momentu zakończenia wszystkich przekazanych zadań. Jej zastosowanie zostało przedstawione w przy­ kładzie 16.19. (Jedyny argument tej metody został opatrzony modyfikatorem params, dzięki czemu poszczególne zadania można przekazywać w jej wywołaniu, jakby były niezależnymi argumentami. W przykładzie z listingu 16.19 kompilator C# pobierze oba zadania przekazane w wywołaniu metody Wai tAl l i za nas umieści je w tablicy) .

Listing 16.19. Metoda Task.WaitAll s t at i c vo i d Mai n (s tri ng O arg s ) { Tas k t 1 = Tas k . Factory . StartNew ( () => Go ( "Jeden " , 100) ) ; Tas k t2 = Tas k . Factory . StartNew ( () => Go ( " Dwa " , 500) ) ; Tas k . Wa i tAl l (t l , t2) ;

Alternatywnym rozwiązaniem byłoby utworzenie jednego zadania i zdefiniowanie dwóch, które chcemy wykonać, jako jego zadań podrzędnych.

Relacja rodzic-dziecko Jeśli utworzymy zadanie code-based, które w ramach swojego działania tworzy kolejne zada­ nia, to te nowe zadania będą jego dziećmi. W przykładzie przedstawionym na listingu 16.20 utworzono dwa zadania, podobnie jak robiliśmy to poprzednio, jednak tym razem są one tworzone wewnątrz innego zadania, a jednocześnie w wywołaniu metody StartNew przekazy­ wana jest wartość Attach edToParent typu wyliczeniowego Tas kCreat i onOpt i a n s, dzięki czemu definiowana jest relacja rodzic-dziecko.

Listing 16.20. Zadania powiązane relacją rodzic-dziecko s t at i c vo i d Mai n (s tri ng O arg s ) { Tas k t = Tas k . Factory . StartNew ( () => { Tas k . Factory . StartNew ( () => Go ( "Jeden " , 100) , Tas kCreat i onOpt i ons . AttachedToParent) ; Tas k . Factory . StartNew ( () => Go ( " Dwa " , 500) , Tas kCreat i onOpt i ons . AttachedToParent) ; }) ; t . Wa i t () ;

Warto zwrócić uwagę, że w tym przykładzie metoda Wa i t jest wywoływana wyłącznie w wątku nadrzędnym. Aby wątek został uznany za zakończony, nie tylko on musi zostać wykonany konieczne jest także wykonanie jego wątków podrzędnych. (A jeśli także te wątki będą posia­ dać swoje wątki podrzędne, to i one będą musiały zostać wykonane) . Dlatego jeśli istnieje jeden wątek nadrzędny, a wszystkie pozostałe są jego dziećmi, to nie ma potrzeby tworzenia wszyst­ kich tych wątków i przekazywania ich w wywołaniu metody Wai tAl l .

Task Parallel Library

I

663

Współbieżność precyzyjna Choć zadania code-based są na pierwszy rzut oka bardzo podobne do realizowanych przy uży­ ciu puli wątków elementów roboczych, to jednak TPL zaprojektowano z myślą o zapewnieniu możliwości wykonywania znacznie mniejszych zadań niż te, które można efektywnie wyko­ nywać, korzystając z puli wątków. TPL zachęca do stosowania współbieżności precyzyjnej (ang. fine-grained concurrency) . Jej założeniem jest przekazywanie do wykonania dużej liczby niewiel­ kich czynności, dzięki czemu biblioteka uzyskuje dużą swobodę i możliwość określenia, jak należy rozplanować wykonywanie poszczególnych zadań na dostępnych procesorach logicz­ nych. Czasami takie postępowanie jest określane jako nadekspresja (ang. overexpression) współ­ bieżności. Idea tego rozwiązania polega na tym, że gdy zaczną się pojawiać komputery wypo­ sażone w większą liczbę procesorów logicznych, to kod próbujący współbieżnie wykonywać większą liczbę zadań będzie w stanie lepiej wykorzystywać ich większe możliwości . Wewnętrzne działanie TPL opiera się na wykorzystaniu puli wątków CLR, dlatego może się wydawać dziwne, że biblioteka ta jest w stanie bardziej efektywnie obsługiwać dużą liczbę elementów roboczych. TPL zapewnia jednak dostęp do niektórych nowych możliwości doda­ nych do puli wątków w .NET 4, z których nie można korzystać za pośrednictwem klasy Thread "+Pool . Klasa ta zazwyczaj uruchamia poszczególne czynności w takim porządku, w jakim były dodawane do puli - innymi słowy, działa w oparciu o kolejkę FIFO (pierwszy na wejściu, pierwszy na wyjściu) . (Dokumentacja ani słowem nie wspomina o jakichkolwiek gwarancjach dotyczących takiego sposobu działania, jednak fakt, że klasa ThreadPool funkcjonuje tak od lat, oznacza zapewne, że jakakolwiek zmiana doprowadziłaby do wielu problemów w istniejącym kodzie) . Jednak w razie przygotowania czynności do wykonania w formie obiektu Tas k pula wąt­ ków zaczyna działać inaczej . Każdy procesor logiczny otrzymuje osobną kolejkę, a umieszczane w niej zadania są zazwyczaj przetwarzane w kolejności LIFO (ostatni na wejściu, pierwszy na wyjściu) . Okazuje się, że w wielu sytuacjach taki sposób działania jest znacznie bardziej efek­ tywny. Dotyczy to zwłaszcza przypadków, gdy wykonywane elementy robocze są niewielkie. Swoją drogą, ta kolejność realizacji nie jest rygorystycznie przestrzegana - procesory pozo­ stające w stanie bezczynności mogą kraść innym procesorom zadania, pobierając je z końca ich kolejek. (Jeśli Czytelnik zastanawia się nad zasadnością użycia tej kolejności realizowania zadań, uzasadnienie można znaleźć w ramce zamieszczonej poniżej) . We wszystkich przedstawionych do tej pory przykładach zamierzone czynności były wykony­ wane, lecz nie zwracały żadnych rezultatów. Jednak zadania mogą zwracać wyniki.

Zadania zwracające wyn iki Klasa Tas k dziedziczy po klasie Task, dodając do niej właściwość Resul t, która po zakoń­ czeniu zadani zawiera wygenerowany przez nie wynik. Klasa ta jest zgodna z koncepcją czasami określaną w literaturze dotyczącej programowania współbieżnego mianem cech (ang. feature) reprezentuje ona czynności, które w jakimś momencie zwrócą wyniki . Przedstawiona już wcześniej metoda Tas k Factory . St artNew może tworzyć zadania dowolnego rodzaju - udostępnia ona swoje przeciążone wersje akceptujące także metody zwracające wynik. (A zatem można w jej wywołaniu przekazać delegacje Fun c lub Fun c zamiast Act i on i Act i on, których używaliśmy w poprzednich przykładach) . Te przeciążone wersje metody zwracają obiekt typu Tas k. (Alternatywnie można także wywołać metodę St art New, używając w tym celu statycznej właściwości Tas k . Factory) .

664

I

Rozdział 16. Wątki i kod asynchroniczny

Kolejki LIFO i kradzież zadań Trzy cechy puli wątków - odrębne kolejki zadań dla poszczególnych procesorów logicznych, wykonywanie zadań w kolejności LIFO oraz kradzież zadań - mają jeden wspólny cel: zapew­ nienie wydajnego wykorzystania pamięci podręcznej procesorów. Jeśli to tylko możliwe, to będziemy chcieli wykonywać zadania na tym samym procesorze logicz­ nym, na którym zostały one wygenerowane, gdyż można sądzić, że w pamięci podręcznej tego procesora znajduje się już wiele informacji dotyczących tych zadań. Przekazanie zadania innemu procesorowi logicznemu oznaczałoby konieczność przeniesienia danych z początkowego procesora do nowego - tego, który będzie wykonywał zadanie. To właśnie dlatego każdy procesor logiczny dysponuje swoją własną kolejką, a nowe zadania są dodawane do kolejki tego procesora logicznego, który je utworzył. Argumentem przemawiającym za wykorzystaniem kolejności LIFO jest to, że w przypadku nowo utworzonych zadań jest większe prawdopodobieństwo, iż związane z nimi dane będą się jeszcze znajdowały w pamięci podręcznej procesora. W związku z tym średnia przepustowość będzie lepsza, jeśli to właśnie one zostaną wykonane w pierwszej kolejności. Natomiast w przypadku kradzieży zadań innym procesorom logicznym jednym z argumentów tłumaczących pobieranie ich z końca kolejki jest to, że chodzi mam o pobranie tych zadań, dla których prawdopodobieństwo przechowywania ich danych w pamięci podręcznej procesora logicznego jest najmniejsze, dzięki czemu można zminimalizować liczbę danych, które trzeba będzie przenieść. A zatem w tym przypadku najlepszym kandydatem do ukradzenia będzie najwcześniejsze zada­ nie w kolejce. Inną zaletą tego rozwiązania jest to, że pozwala ono zredukować współzawodnic­ two - daną kolejkę można utworzyć w taki sposób, by różne procesory mogły równocześnie ope­ rować na jej dwóch końcach.

Można uruchomić zadanie Tas k, a następnie wywołać metodę Wai t, by poczekać na zwrócenie wyniku. Można także odczytać wartość właściwości Res u l t, co spowoduje wywo­ łanie metody Wai t, jeśli wynik jeszcze nie będzie dostępny. Niemniej jednak zablokowanie działania aż do momentu zakończenia zadania nie byłoby rozwiązaniem szczególnie użytecz­ nym - stanowiłoby ono jedynie bardzo okrężny sposób realizacji kodu w sposób synchro­ niczny . W rzeczywistości czasami właśnie o to może nam chodzić. Możemy utworzyć wiele wątków podrzędnych, a następnie poczekać, aż wszystkie one zostaną wykonane, zachowując przy tym możliwość skorzystania ze wszystkich udogodnień w zakresie obsługi wyjątków zapewnianych przez TPL . Niemniej jednak nieraz przydatna będzie możliwość uniknięcia blokowania i przekazania jakiejś metody zwrotnej, która zostanie wywołana po wykonaniu zadania. Tę możliwość zapewniają nam kontynuacje.

Kontynuacje Kontynuacje są zadaniami wywoływanymi po zakończeniu innych zadań10 • Klasa Tas k defi­ niuje metodę Cont i n ueWi t h umożliwiającą określenie kodu, który będzie realizowany w ramach kontynuacji zadania. Wymaga ona przekazania delegacji pobierającej jeden argument, który reprezentuje zakończone zadanie. Metoda Cant i n ueW i t h posiada kilka przeciążonych wersji 10

Jeśli Czytelnik zetknął się z tym terminem w znaczeniu stosowanym w językach takich jak Scheme, zapew­ niających możliwość wywołania z użyciem bieżącej kontynuacji, to powinien pamiętać, że w tym przypadku chodzi o coś innego. Pomiędzy obydwoma znaczeniami tego słowa występuje co prawda pewne podobieństwo, gdyż w obu przypadkach chodzi o możliwość kontynuowania pewnej pracy nieco później, jednak różnice pomiędzy nimi są naprawdę znaczące.

Task Parallel Library

I

665

pozwalających użyć delegacji zwracającej wartość (w tym przypadku zadanie kontynuacji także będzie obiektem Tas k) bądź delegacji, która wartości nie zwraca (wówczas zadanie kontynuacji będzie obiektem Tas k ) . Metoda ta zwraca obiekt Tas k reprezentujący kontynuację . A zatem istnieje możliwość tworzenia kontynuacji w formie łańcucha wywołań: s t at i c vo i d Mai n (s tri ng O arg s ) { Tas k t = Tas k . Factory . StartNew ( () => Go ( " Jeden " , 100) ) . Cont i nueWi th ( t l => Go ( " Dwa " , 500) ) . Cont i nueWi th (t2 => Go ( " Trzy " , 200) ) ; t . Wa i t () ;

Powyższy kod spowoduje wykonanie trzech zadań jednego po drugim. Trzeba zwrócić uwagę, że użyta w przykładzie zmienna t odwołuje się do trzeciego zadania, czyli ostatniej kontynu­ acji, dlatego też wywołanie t . Wai t sprawi, że program zaczeka na wykonanie wszystkich zadań. Oczekiwanie na pierwsze dwa zadania nie jest konieczne, gdyż trzecie z nich nawet nie może się rozpocząć, dopóki nie zostaną one wykonane; oczekiwanie na ostatnie zadanie niejawnie oznacza oczekiwanie na wszystkie trzy. Kontynuacje są nieco bardziej interesujące w przypadku, gdy początkowe zadanie zwraca jakiś wynik. Oznacza to bowiem, że mogą one coś z tym wynikiem zrobić. Na przykład możemy używać zadania, które pobiera jakieś dane z serwera, oraz kontynuacji, która prezentuje je w interfejsie użytkownika aplikacji. Oczywiście aktualizacja interfejsu użytkownika wymaga tego, by kontynuacja była realizowana w odpowiednim wątku, jednak tu może nam pomóc TPL.

Mechani zmy szeregujące Klasa Tas kSchedul er ma za zadanie określać, kiedy oraz jak należy wykonywać zadania. Jeśli nie określimy mechanizmu szeregującego (ang. scheduler) jawnie, to zostanie zastosowany mechanizm domyślny korzystający z puli wątków. Jednak podczas tworzenia zadania można zastosować inne mechanizmy szeregujące - zarówno metoda StartNew, jak i Cant i n ueWi t h udostępniają wersje przeciążone pozwalające na przekazanie obiektu Tas kSchedul er. TPL udo­ stępnia z kolei mechanizm szeregujący korzystający z obiektu Syn c h ron i z a t i onContext, który pozwala na wykonywanie zadań w wątku obsługi interfejsu użytkownika. Przykład 16.21 poka­ zuje, w jaki sposób można go użyć podczas obsługi zdarzeń w aplikacji WPF.

Listing 16.2 1 . Kontynuacja w wątku obsługi interfejsu użytkownika vo i d OnButtonCl i ck (obj ect sende r , RoutedEventArgs e) { Tas kSchedu l er u i Schedul er = Tas kSchedu l er . FromCurrentSynchron i z a t i onContext () ; Tas k . Factory . StartNew (GetData) . Cont i nueWi th ( (tas k) => UpdateU i (tas k . Res u l t) , u i Schedul er) ; s t r i ng GetData () { WebCl i ent w = new WebCl i ent () ; return w . Downl oadStri ng ( " http : //hel i on . pl / " ) ;

vo i d Update U i (stri ng i nfo)

666

I

Rozdział 16. Wątki i kod asynchroniczny

myTextBox . Text = i nfo ;

W tym przykładzie tworzymy zadanie, które zwraca łańcuch znaków, używając przy tym domyślnego mechanizmu szeregującego. Zadanie to wywoła funkcję GetData w wątku pobranym z puli wątków. Jednocześnie jednak tworzymy także kontynuację, używając obiektu Tas kSche "+dul er pobranego za pomocą wywołania metody FromCurren tSyn chron i zat i onContext . Metoda ta pobiera wartość właściwości Current obiektu Synchron i z a t i onContext, po czym zwraca obiekt mechanizmu szeregującego, który będzie używał pobranego kontekstu podczas wykonywa­ nia wszystkich zadań. Ponieważ kontynuacja jawnie żąda użycia tego mechanizmu, metoda UpdateU i zostanie wywołana w wątku obsługi interfejsu użytkownika . W efekcie metoda GetData jest wywoływana w jakimś wątku pobranym z puli wątków, a następ­ nie zwrócona przez nią wartość jest przekazywana do metody UpdateUi wykonywanej w wątku obsługi interfejsu użytkownika . Podobnej sztuczki można używać podczas korzystania z implementacji APM, gdyż obiekty fabrykujące używane do tworzenia zadań udostępniają metody tworzące zadania, które dzia­ łają w oparciu o APM.

Zadania i model programowan ia asynchronicznego Klasy Tas k Fa ctory oraz Tas k Fa ctory udostępniają wiele przeciążonych wersji metody FromAsyn c . Można do niej przekazywać metody Beg i n oraz End stanowiące implementację wzorca APM, jak również argumenty, które chcielibyśmy do nich przekazać. Metoda ta zwraca obiekt Task lub Tas k wykonujący operację asynchroniczną, a nie obiekt wykonujący delegację. Kod przedstawiony na listingu 16.22 korzysta z tego, by wykonać w zadaniu asynchroniczną metodę klasy Dns, której używaliśmy we wcześniejszych przykładach.

Listing 1 6 .22 . Tworzenie zadania realizującego metodę APM Tas kSchedul er u i Schedul er = Tas kSchedu l er . FromCurrentSynchron i zat i onContext () ; Tas k< I PHostEntry> . Factory . FromAsync ( Dns . Beg i nGetHos tEntry , Dns . EndGetHos t En t ry , " hel i on . pl " , nul l ) . Con t i nueWi th ( (tas k) => UpdateU i (tas k . Res u l t . Addres s Li s t [OJ . ToStri ng () ) , u i Schedul er) ;

FromAsync udostępnia kilka wersji przeciążonych dla różnych metod APM, zarówno tych bezar­ gumentowych, jak i pobierających od jednego do trzech argumentów, co obejmuje przeważającą część implementacji APM. Oprócz przekazywania do zadań metod Beg i n i End, przekazywane są także ich argumenty oraz dodatkowy argument typu Obj ect, który można przekazywać we wszystkich metodach Beg i n . (W przypadku nielicznych metod stanowiących pozostałą części implementacji APM, które mają więcej argumentów bądź posiadają argumenty wyjściowe lub referencyjne, można skorzystać z przeciążonej wersji metody FromAsyn c akceptującej argu­ ment typu I Asyn cResul t . Skorzystanie z tej wersji metody wymaga użycia nieco bardziej rozbu­ dowanego kodu, jednak pozwala nam to wykonać w ramach zadania dowolną metodę imple­ mentacji APM) .

Poznaliśmy zatem podstawowe sposoby tworzenia zadań oraz ustalania powiązań pomiędzy nimi bądź to w formie relacji rodzic-dziecko, bądź też w formie kontynuacji. Co jednak można zrobić w przypadku, gdy będziemy chcieli zatrzymać już rozpoczętą operację asynchroniczną? Takiej możliwości nie daje nam ani pula wątków, ani APM, zapewnia ją jednak biblioteka TPL. Task Parallel Library

I

667

Obsługa anu l owan ia Anulowanie rozpoczętej operacji asynchronicznej jest problemem wyjątkowo trudnym. Wystę­ puje przy tym wiele dziwnych wyścigów, z którymi trzeba sobie poradzić. Może się zdarzyć, że w chwili, gdy spróbujemy przerwać operację, jej realizacja będzie już zakończona. Ewen­ tualnie, jeśli do tego czasu nie zostanie zakończona, to jej realizacja może dotrzeć do punktu, po którego przekroczeniu nie można już jej przerwać - w takim przypadku próba jej anulo­ wania jest skazana na niepowodzenie . Jeśli nawet przerwanie operacji jest możliwe, to może to trochę potrwać. Obsłużenie i przetestowanie każdej możliwej kombinacji jest wystarczająco kłopotliwe, nawet jeśli chodzi o anulowanie tylko jednej operacji, jeśli natomiast używamy wielu powiązanych ze sobą zadań, staje się ono jeszcze trudniejsze. Na szczęście w .NET 4 wprowadzono nowy model obsługi anulowania operacji asynchronicz­ nych udostępniający doskonale przemyślany i przetestowany sposób rozwiązywania pro­ blemów, które najczęściej się przy tym pojawiają. Możliwości zastosowania tego modelu nie ograniczają się jedynie do TPL - można go stosować niezależnie, dzięki czemu pojawia się on także w innych miejscach platformy .NET. (Korzystają z niego na przykład klasy równoległego przetwarzania danych, które zostały przedstawione w dalszej części rozdziału) . Jeśli chcemy mieć możliwość przerwania operacji asynchronicznej, trzeba do niej przekazać obiekt Cancel at i onToken . Pozwala on realizowanej operacji zauważyć fakt jej anulowania - obiekt posiada właściwość I sCancel l ati on Requested. Dodatkowo istnieje możliwość przekazania w meto­ dzie Reg i s t er delegacji, która zostanie wywołana w momencie anulowania. Obiekt Can ce 1 1 at i on Token jedynie ułatwia zauważenie faktu zażądania anulowania operacji. Nie zapewnia on natomiast możliwości inicjalizacji anulowania. Do tego celu jest używana osobna klasa - Can cel l at i onTokenSource. Powodem rozdzielenia wykrywania oraz kontroli anulowa­ nia na dwie odrębne klasy jest to, że w przeciwnym przypadku nie byłoby możliwe przeka­ zanie do operacji powiadomienia o żądaniu anulowania bez jednoczesnego zapewnienia jej możliwości inicjacji tego anulowania. Klasa Can cel l at i onTokenSource tworzy tak zwane tokeny anulowania (ang . cancellation tokens) - najpierw prosimy o przydzielenie takiego tokenu, a następnie przekazujemy go do operacji, którą być może będziemy chcieli przerwać. Przykład przedstawiony na listingu 16.23 jest podobny do tego z listingu 16.21, lecz dodatkowo przeka­ zuje do metody StartNew token anulowania, a następnie korzysta z obiektu Can cel l at i onTokenSource w celu przerwania operacji w przypadku kliknięcia przez użytkownika przycisku Anuluj.

Listing 1 6.23. Nieefektywny sposób anulowania operacji pri vate Can cel l ati onTokenSource cancel Source ;

vo i d OnButtonCl i ck (obj ect sende r , RoutedEventArgs e) { cancel Source = new Cancel l ati onTokenSource () ;

Tas kSchedu l er u i Schedul er = Tas kSchedu l er . FromCurrentSynchron i za t i onContext () ; Task . Factory . StartNew (GetData , can cel Source . Token)

. Cont i nueWi th ( (tas k) => UpdateU i (tas k . Res u l t) , u i Schedul er) ;

voi d On Cancel Cl i ck (obj ect sender , RoutedEventArgs e) {

668

I

Rozdział 16. Wątki i kod asynchroniczny

i f (cancel Source ! = nul l ) { cancel Source . Can cel () ; } }

s t r i ng GetData () { WebCl i ent w = new WebCl i ent () ; return w . Downl oadStri ng ( " http : //hel i on . pl / " ) ; vo i d Update U i (stri ng i nfo) { cancel Source = n ul l ;

myTextBox . Text = i nfo ;

Okazuje się, że sposób anulowania operacji zastosowany w tym przykładzie nie jest efektywny, gdyż w ramach zadania wykonywane jest jedno wywołanie blokującej metody. W praktyce anulowanie operacji w takim przypadku nic nie da - realny efekt można by uzyskać wyłącz­ nie w sytuacji, gdyby użytkownikowi udało się kliknąć przycisk Anuluj przed rozpoczęciem realizacji zadania. Uwidacznia to pewien bardzo ważny aspekt anulowania: nigdy nie jest ono wymuszane, lecz zawsze wymaga współdziałania, gdyż w przeciwnym razie jedynym roz­ wiązaniem byłoby usunięcie całego wątku realizującego zadanie . I choć byłoby to możliwe, to jednak siłowe zamykanie wątków może sprawić, że procesor znajdzie się w niepewnym stanie, gdyż zazwyczaj nie można mieć pewności, że usunięty wątek nie był w trakcie mody­ fikowania jakiegoś wspólnego stanu. Ponieważ rozwiązanie to stawia pod znakiem zapytania integralność naszego programu, jedyną rzeczą, jaką w konsekwencji można później bezpiecznie wykonać, jest jego zamknięcie, a to jest nieco drastyczne . A zatem ten model anulowania wymaga współpracy ze strony przerywanego zadania . W naszym przykładzie anulowanie dałoby jakikolwiek efekt wyłącznie w przypadku, gdyby użytkownikowi udało się kliknąć przycisk Anuluj, zanim realizacja zadania zostałaby rozpoczęta . Anulowanie może być znacznie bardziej przydatne, jeśli całość wykonywanej pracy zostanie podzielona na relatywnie małe fragmenty. Gdy anulujemy zadania, które dodano do kolejki, lecz których jeszcze nie zaczęto realizować, to nigdy nie zostaną one wykonane. Realizacja aktu­ alnie wykonywanych zadań będzie kontynuowana, jeśli jednak wszystkie nasze operacje nie będą zbyt duże, to ich zakończenie nastąpi stosunkowo szybko. Natomiast w przypadku, gdy wyko­ nanie naszego zadania będzie wymagało dłuższego czasu, będziemy musieli w nim sprawdzać, czy nie zażądano jego przerwania, a jeśli tak - odpowiednio to żądanie obsłużyć. Oznacza to, że kod wykonywany w ramach zadania będzie musiał mieć dostęp to tokenu anulowania i okresowo sprawdzać wartość jego właściwości I sCancel l a t i on Requested. Anulowanie nie jest jedyną przyczyną, która może doprowadzić do zatrzymania zadania lub grupy zadań przed ich zakończeniem. Innym powodem może być wystąpienie wyjątku.

Obsługa błędów Zadanie może się zakończyć na jeden z trzech sposobów: może zostać w całości wykonane, może zostać anulowane bądź też może w nim wystąpić blqd. Właściwość TaskState obiektu Task odzwierciedla te trzy stany, korzystając z trzech możliwych wartości: RanTu Camp l et i on, Can cel ed oraz Faul ted. W sytuacji gdy właściwość ta przyjmuje wartość Faul t ed, dodatkowo właściwości

Task Parallel Library

I

669

I s F a u l ted jest przypisywana wartość true. Zadanie code-based znajdzie się w stanie F a u l t ed w przypadku, gdy zostanie w nim zgłoszony wyjątek. Informacje o tym wyjątku można pobrać, korzystając z właściwości Except i on obiektu zadania. Właściwość ta zwraca obiekt typu Aggregate "+Excepti on, którego właściwość I nnerExcept i on s zawiera listę wszystkich zgłoszonych wyjątków. Jest to lista, gdyż sposób wykorzystania niektórych zadań sprawia, że w trakcie ich realizacji może zostać zgłoszonych wiele wyjątków - dane zadanie może mieć na przykład wiele zadań podrzędnych, których nie udało się prawidłowo wykonać.

Jeśli nie sprawdzimy właściwości I s Faul ted, a zamiast tego spróbujemy kontynuować działanie programu, bądź to wywołując metodę Wai t, bądź próbując pobrać wartość właściwości Res u l t obiektu Tas k, to w naszym kodzie zostanie zgłoszony wyjątek AggregateExcept i on. Istnieje możliwość napisania kodu, który nigdy nie będzie sprawdzał występowania wyjątków. Przykład przedstawiony na listingu 16.17 uruchamia dwa wątki, a ponieważ całkowicie igno­ ruje on obiekty Tas k zwracane przez wywołania metody StartNew, zatem w oczywisty sposób przestaje się nimi interesować. Gdyby były to wątki podrzędne innego wątku, nie miałoby to większego znaczenia - zignorowanie wyjątku w wątku podrzędnym powoduje, że wątek nadrzędny przyjmuje stan Faul ted. Jednak w naszym przypadku nie są to wątki podrzędne, zatem jeśli w trakcie ich realizacji pojawią się wyjątki, to nasz program ich nie zauważy. Nie­ mniej jednak TPL stara się, by takie wyjątki nie zostały zignorowane - korzysta z pewnej moż­ liwości mechanizmu oczyszczania pamięci, określanej mianem finalizacji, w celu wykrycia, czy w usuwanym z pamięci obiekcie Tas k wystąpił błąd, którego program w ogóle nie zauważył . W razie wykrycia takiej sytuacji zostanie zgłoszony wyjątek AggregateExcept i on, który spowo­ duje przerwanie pracy programu, chyba że jego proces został odpowiednio skonfigurowany i potrafi radzić sobie z nieobsłużonymi wyjątkami. (Platforma .NET wykonuje wszystkie fina­ lizatory w specjalnym przeznaczonym do tego wątku i to właśnie w nim TPL zgłasza wszystkie wyjątki) . Sposób postępowania z nieobsługiwanymi wyjątkami pozwala dostosować do własnych potrzeb klasa Tas kSchedul er udostępniająca zdarzenie Unob servedExcep t i on. Wszystko to oznacza, że należy implementować obsługę błędów we wszystkich wątkach nad­ rzędnych, w których mogą być zgłaszane wyjątki. Jednym z rozwiązań jest utworzenie kon­ tynuacji, która odpowiadałaby właśnie za obsługę błędów. Metoda Cont i n ueW i t h umożliwia przekazanie opcjonalnego argumentu typu wyliczeniowego Tas kCont i n uat i onOp t i ons, którego jedną z wartości jest Onl yOn Faul ted. Można jej użyć, by utworzyć kontynuację, która zostanie wykonana wyłącznie w razie wystąpienia nieprzewidzianego wyjątku. (Oczywiście takie nie­ oczekiwane wyjątki nigdy nie są czymś pozytywnym, gdyż z definicji nie przewidywaliśmy ich wystąpienia i w związku z tym nie wiemy, w jakim stanie znajdzie się program, gdy zostaną zgłoszone. Dlatego też w takiej sytuacji najprawdopodobniej trzeba będzie przerwać działanie programu, co i tak by nastąpiło, gdybyśmy w ogóle nie napisali żadnego kodu związanego z obsługą błędów. Należy wówczas zapisywać występujące błędy w dzienniku, a może także podjąć próbę zapisania gdzieś zmodyfikowanych danych, mając nadzieję na ich odtworzenie po ponownym uruchomieniu aplikacji) . Preferowanym sposobem obsługi błędów jest jednak stosowanie w kodzie zwyczajnych instrukcji t ry - cat c h, dzięki czemu zgłaszane wyjątki w ogóle nie trafią do kodu biblioteki TPL.

670

I

Rozdział 16. Wątki i kod asynchroniczny

Równoległość danych Ostatnim zagadnieniem związanym ze współbieżnością, jakim się zajmiemy, będzie równo­ ległość danych. W tym przypadku współbieżność jest efektem korzystania z wielu elementów danych, a nie skutkiem jawnego tworzenia wielu zadań lub wątków. Zagadnienie to może stanowić proste podejście do równoległości, gdyż nie musimy w żaden sposób informować plat­ formy .NET o tym, w jaki sposób chcemy podzielić całość prac, które musimy wykonać. W przypadku korzystania z zadań podczas tworzenia pierwszego z nich .NET Framework nie ma najmniejszego pojęcia o tym, ile zadań planujemy utworzyć. Natomiast w sytuacji równole­ głości danych platforma ma możliwość uzyskania nieco szerszego obrazu rozwiązywanego problemu, zanim będzie musiała określić, w jaki sposób rozdzieli jego wykonywanie pomiędzy poszczególne procesory logiczne . Dzięki temu czasami będzie ona w stanie bardziej efektywnie wykorzystać dostępne zasoby.

Metody Paral lel . For oraz Paral lel. ForEach Klasa Para 1 1 el udostępnia dwie metody służące do przetwarzania równoległego sterowanego danymi. Pod względem sposobu działania jej metody For oraz ForEach przypominają instrukcje for oraz foreach, jednak zamiast przetwarzać kolekcję element po elemencie, w systemach dys­ ponujących wieloma procesorami logicznymi będą one przetwarzały większą liczbę elementów jednocześnie. W przykładzie przedstawionym na listingu 16.24 została zastosowana metoda Paral l el . For. Zaprezentowany kod wylicza kolory poszczególnych pikseli fraktala nazywanego zbiorem Mandelbrota. Stanowi on popularną demonstrację równoległości, gdyż wartość każdego z pik­ seli można wyliczyć całkowicie niezależnie od pozostałych, a zatem zakres przetwarzania rów­ noległego jest potencjalnie nieograniczony (chyba że komputer dysponowałby większą liczbą procesorów logicznych, niż jest pikseli w generowanym fraktalu) . Ponieważ wykonywane obliczenia są stosunkowo kosztowne, łatwo można zauważyć efekty, jakie daje przetwarzanie równoległe. Zazwyczaj kod tego typu składałby się z dwóch zagnieżdżonych pętli for prze­ twarzających kolejno wiersze pikseli oraz wszystkie piksele w danym wierszu, jednak w tym przypadku zewnętrzna pętla została zastąpiona metodą Paral l el . For. (Zatem ta konkretna implementacja nie będzie mogła wykorzystywać więcej procesorów logicznych, niż jest prze­ twarzanych wierszy. Oznacza to, że zakres przetwarzania równoległego nie zejdzie aż do pozio­ mu poszczególnych pikseli, jednak jako że generowany obrazek zazwyczaj będzie miał kilkaset pikseli wysokości, to zakres równoległości i tak będzie stosunkowo duży) .

Listing 16.24. Metoda Parallel.For s t at i c i nt [ , ] Cal cul ateMandel brotVal ues ( i nt p i xel Wi dth , i nt p i xel He i ght , doubl e l eft , doubl e top , doubl e wi dth , doubl e hei ght , i nt max l terat i on s ) i nt [ , ] res u l ts

=

new i nt [p i xe l W i dth , p i xel Hei ght] ;

li Nierównoległa wersja poniższego wiersza wyglqdałaby następujqco: lifor(int pixelY O; pixelY < pixelHeight; ++pixelY) =

Paral l el . For (O , pi xel Hei ght , pi xel Y =>

{

doubl e y = top + (pi xel Y * he i ght) / (doubl e) p i xel He i ght ; for ( i nt p i xel X = O ; p i xel X < p i xel Wi dth ; ++p i xel X)

Równoległość danych

I

671

doubl e x = l eft + (pi xel X * wi dth) / (doubl e) p i xel Wi dth ;

li Uwaga: ten typ jest dostępny w przestrzeni nazw System.Numerics li dostępnej w podzespole System.Numerics. Compl ex c = new Compl ex (x , y) ; Compl ex z = new Compl ex () ; i nt i te r ; for ( i ter = l ; z . Magn i tude < 2 & & i ter < max l terat i ons ; ++i ter) { z = z * z + c; i f ( i ter == max l terat i on s ) { i ter = O ; res u l t s [p i xel X , p i xel Y] = i te r ; }) ; return res u l ts ;

Zastosowane w powyższym kodzie wywołanie o postaci: Paral l el . For (O , pi xel He i ght , p i ksel Y => { }) ;

przetwarza ten sam zakres danych co instrukcja for ( i nt p i xel Y = O ; p i sel Y < p i xel He i gh t ; p i xel Y++) {

Składnia używana w obu tych przykładach nie jest identyczna, gdyż Para 1 1 e l . For jest jedynie metodą, a nie instrukcją języka C#. Pierwsze dwa argumenty jej wywołania określają zakres danych - wartość początkowa jest podawana włącznie (czyli to od niej zacznie się przetwa­ rzanie), natomiast druga wyłącznie (czyli przetwarzanie zostanie zakończone bezpośrednio przed nią) . Ostatnim argumentem wywołania metody Paral l el . For jest delegacja, której argu­ mentem jest zmienna iteracyjna . W przykładzie z listingu 16 .24 zastosowaliśmy wyrażenie lambda, którego minimalistyczna składnia sprawia, że całość kodu będzie możliwie najbar­ dziej zbliżona do zwyczajnej pętli for. Metoda Paral l el . For będzie próbować wykonywać delegację na wielu procesorach logicznych jednocześnie, starając się wykorzystać je wszystkie w możliwie optymalny i pełny sposób oraz używając przy tym puli wątków. Jednak pewnym zaskoczeniem może być to, jak poszcze­ gólne iteracje są rozdzielane pomiędzy dostępne procesory logiczne . Pierwszy wiersz pikseli nie zostanie przydzielony pierwszemu procesorowi, drugi drugiemu i tak dalej . Okazuje się, że dostępne wiersze zostaną podzielone na grupy, dzięki czemu drugi procesor logiczny roz­ pocznie przetwarzanie nie od drugiego, lecz od któregoś z dalszych wierszy. Co więcej, począt­ kowe grupy mogą zostać później dodatkowo podzielone zależnie od postępów w realizacji kodu. Dlatego też ważne jest, by nie zakładać, że poszczególne iteracje będą wykonywane w jakiejś konkretnej kolejności. Grupowanie przetwarzanych elementów jest stosowane po to, by unik­ nąć podziału całości prac na elementy zbyt małe, by można je było wykonywać efektywnie. W optymalnym przypadku każdy procesor logiczny powinien otrzymać na tyle duży fragment zadania, by zminimalizowane zostały narzuty związane z przełączaniem kontekstu i syn­ chronizacją, a jednocześnie na tyle mały, by każdy z procesorów był zajęty, dopóki będzie coś

672

I

Rozdział 16. Wątki i kod asynchroniczny

do zrobienia. Takie grupowanie jest jednym z powodów, dla których przetwarzanie równo­ ległe sterowane danymi może być bardziej wydajne od jawnego stosowania wątków - stopień równoległości może być tak szczegółowy, jak to konieczne, lecz nie bardziej, dzięki czemu można zminimalizować wszelkie narzuty. Prawdopodobnie nazywanie rozwiązania z listingu 16.24 równoległością danych jest nieco nacią­ gane - w tym przypadku te dane są jedynie liczbami przekazywanymi do obliczeń. Metoda Paral l el . For nie jest w większym stopniu „zorientowana na dane" niż zwyczajna pętla for sterowana zmienną typu i nt - po prostu przetwarza ona pewien zakres liczb całkowitych. Jednak używając dokładnie tego samego sposobu, można by także przetwarzać zakres dowol­ nych danych zamiast zakresu liczb całkowitych. Dodatkowo dostępna jest także metoda Para "+l l el . ForEach, która jest bardzo podobna do Paral l el . For, lecz zgodnie z tym, czego można się spodziewać, pozwala przetwarzać zawartość dowolnej enumeracji I Enumerab l e (analogicz­ nie do instrukcji foreach języka C#), a nie jedynie pewien zakres całkowitych wartości. Metoda ta odczytuje większą liczbę elementów z enumeracji, by zapewnić grupowanie. (A jeśli użyjemy w niej danej typu I L i st, to wykorzysta indeksator, by określić jak najbardziej wydajną stra­ tegię podziału zbioru danych na grupy) . Istnieje jednak jeszcze jeden sposób równoległego przetwarzania kolekcji danych: PLINQ .

PLINQ

równoległe LI NQ

-

Równoległe LINQ (PLINQ) jest dostawcą LINQ, który pozwala przetwarzać dowolne dane I En umera b l e przy wykorzystaniu zwyczajnej składni LINQ, jednak w sposób równoległy. Z pozoru PLINQ jest zwodniczo łatwe . Zapytanie: var pq

=

from x i n some l i s t where x . SomeProperty > 42 sel ect x . Frob (x . Bar) ;

wykorzysta dostawcę LINQ to Objects, zakładając oczywiście, że somel i st implementuje I Enume "+rabl e. Poniższa wersja zapytania wykorzysta PLINQ. var pq

=

from x i n some li s t . As Paral l el ( ) where x . SomeProperty > 42 sel ect x . Frob (x . Bar) ;

Jedyną różnicą pomiędzy tym a poprzednim fragmentem kodu jest dodanie wywołania As Para "+ l l el - metody rozszerzenia udostępnianej przez klasę Paral l el E n umerabl e we wszystkich typach implementujących interfejs I En umerabl e. Jest ona dostępna we wszystkich plikach, w których korzystając z odpowiedniej deklaracji u s i ng, udostępniono przestrzeń nazw Sys t ern . "+Li nq. Metoda As Paral l el zwraca daną typu Paral l el Query, co oznacza, że nie będzie jej można przetwarzać przy użyciu zwyczajnych operatorów LINQ udostępnianych przez implementa­ cję dostawcy LINQ to Objects. Można korzystać ze wszystkich dostępnych wcześniej operato­ rów, lecz tym razem są one implementowane przez klasę Para 1 1 el Enumerab l e, która pozwala niektóre z nich wykonywać równolegle . '

. '

L--�.

,

Nie wszystkie zapytania będą wykonywane w sposób równoległy. Niektóre opera­ tory LINQ w zasadzie wymuszają wykonywanie operacji w określonej kolejności, dlatego też PLINQ przeanalizuje strukturę zapytania, by określić, które jego fragmenty - jeśli w ogóle takie są - będzie można z korzyścią wykonać równolegle.

Równoległość danych

I

673

Przetwarzanie wyników przy użyciu pętli foreach ogranicza nieco możliwości równoległego przetwarzania danych, co wynika z faktu, że instrukcja ta pobiera jeden element danych po drugim - początkowe elementy zapytania wciąż można realizować równolegle, natomiast ostateczny wynik jest sekwencyjny. Jeśli chcemy wykonać pewien kod dla wszystkich elementów enumeracji, zapewniając przy tym możliwość jego równoległej realizacji nawet w ramach tego ostatniego etapu przetwarzania, to możemy skorzystać z udostępnianego przez PLINQ opera­ tora ForA 1 1 : pq . ForAl l (x => x . DoSometh i ng () ) ;

Zastosowanie tego kodu spowoduje wykonanie delegacji dla każdego elementu zapytania, przy czym będą one mogły być wykonywane współbieżnie - zarówno do przetwarzania zapytania, jak i do późniejszego wykonania delegacji zostanie wykorzystanych tak dużo pro­ cesorów logicznych, jak to tylko możliwe. Oznacza to, że wszystkie standardowe ostrzeżenia związane z programowaniem wielowątko­ wym obowiązują także w przypadku stosowania operatora ForAl l . W rzeczywistości używa­ nie PLINQ może być nieco niebezpieczne, gdyż wcale nie jest oczywiste, że nasz kod będzie wykonywany przy użyciu wielu wątków. PLINQ sprawia, że równoległy kod wygląda zbyt normalnie. Nie zawsze będzie to problemem - PLINQ stara się zachęcać nas do stosowania w jego zapytaniach funkcyjnego stylu programowania, co oznacza, że większość używanych w nich danych będzie dostępna w trybie tylko do odczytu, dzięki czemu korzystanie z wielu wątków będzie znacznie łatwiejsze . Jednak kod wykonywany przez operator ForAl l będzie użyteczny wyłącznie w przypadku, gdy nie będzie powodował żadnych efektów ubocznych, dlatego też trzeba na niego bardzo uważać.

Podsu mowan ie Aby wykorzystać potencjał procesorów wielordzeniowych, konieczne jest wykonywanie kodu w wielu wątkach. Wątki są także przydatne do zapewniania sprawnego funkcjonowania inter­ fejsu użytkownika podczas realizacji długotrwałych operacji, choć w tym przypadku wyko­ rzystanie technik programowania asynchronicznego może być lepszym rozwiązaniem niż two­ rzenie wątków w jawny sposób. Choć nic nie stoi na przeszkodzie, by samemu jawnie tworzyć wątki, to jednak wykorzystanie puli wątków - zarówno jawne, jak i za pośrednictwem biblio­ teki TPL - jest zazwyczaj preferowane, gdyż sprawia, że łatwiej można dostosować kod do dostępnych na danym komputerze zasobów procesora. W przypadku tworzenia kodu, który musi przetwarzać ogromne kolekcje danych lub wykonywać jednakowe obliczenia na dużych zakresach liczb, można skorzystać z równoległości danych, która pozwala wykonywać operacje w sposób równoległy bez zbytniego komplikowania programu. Niezależnie od tego, z jakiego mechanizmu działania wielowątkowego chcemy skorzystać, naj­ prawdopodobniej pojawi się konieczność zastosowania podstawowych narzędzi do synchro­ nizacji i blokowania, dzięki którym będziemy mogli uniknąć zagrożeń przetwarzania równo­ ległego takich jak wyścigi. Zastosowanie mechanizmu monitorów wbudowanego w każdy obiekt na platformie .NET i udostępnianego za pośrednictwem klasy Mon i tor oraz użycie słowa kluczo­ wego l ock jest zazwyczaj najlepszym z możliwych rozwiązań. Dostępne są jednak także bardziej wyspecjalizowane narzędzia, które mogą lepiej się sprawdzać, jeśli akurat znajdziemy się w jednej z sytuacji, do których rozwiązywania zostały one stworzone.

674

I

Rozdział 16. Wątki i kod asynchroniczny

ROZDZIAŁ 17.

Atrybuty i odzwierciedlanie

Oprócz danych i kodu program .NET może zawierać metadane (ang. metadata) . Są to informa­ cje na temat danych - a więc informacje dotyczące typów, kodu, pól i tak dalej - które są przechowywane wraz z samymi danymi tworzącymi program. W niniejszym rozdziale zosta­ nie zaprezentowany sposób, w jaki powstają niektóre z tych metadanych i w jaki są używane. Duża część metadanych to informacje, których platforma .NET potrzebuje, aby wiedzieć, jak należy używać kodu. Metadane definiują na przykład, czy określona metoda ma charakter publiczny, czy prywatny. Można też jednak dodawać własne metadane. Robi się to za pomocą atrybutów (ang. attributes) .

Odzwierciedlanie (ang. reflection) jest z kolei procesem, dzięki któremu program jest w stanie odczytywać swoje własne metadane lub metadane związane z innym programem. O programie mówi się, że odzwierciedla sam siebie lub jakiś inny program, gdy wydobywa on metadane z odzwierciedlanego podzespołu i wykorzystuje je, aby poinformować o czymś użytkownika lub zmienić swój sposób działania.

Atrybuty Atrybut (ang. attribute) to obiekt reprezentujący dane, które chcemy powiązać z jakimś elemen­ tem swojego programu. Element, do którego dołączany jest atrybut, określany jest mianem celu (ang. target) tego atrybutu. Na przykład w rozdziale 12. korzystaliśmy z atrybutu Xml I gnore, który został zastosowany do właściwości: [Xml Ignore] publ i c stri ng LastName { get ; set ; }

Powyższy zapis informuje system serializacji XML, że akurat ta właściwość ma zostać zignoro­ wana podczas konwersji zachodzącej pomiędzy kodem XML i obiektami tego rodzaju. Przykład ten ilustruje pewną ważną cechę atrybutów: same z siebie nie wykonują one żadnych opera­ cji. Atrybut Xml I gnore nie zawiera żadnego kodu ani nie powoduje przeprowadzenia żadnych działań w momencie odczytywania lub modyfikowania właściwości, z którą jest związany. Jego wpływ ujawnia się wyłącznie wtedy, gdy korzystamy z mechanizmu serializacji XML, a jedyną przyczyną, dla której w ogóle coś w tym czasie robi, jest to, że system serializacji XML sprawdza obecność tego atrybutu. Atrybuty mają zatem charakter pasywny. Zasadniczo są po prostu szczególnego rodzaju adno­ tacjami. Aby mogły się do czegoś przydać, coś musi ich gdzieś szukać. 675

Typy atrybutów Niektóre atrybuty są zapewniane jako część składowa CLR, niektóre przez biblioteki klas platformy .NET, a niektóre przez jeszcze inne biblioteki. Poza tym możemy również definiować własne atrybuty i używać ich zgodnie ze swoimi potrzebami. Większość programistów korzysta jedynie z atrybutów dostarczanych przez istniejące biblioteki, jednak możliwość samodzielnego tworzenia własnych atrybutów w połączeniu z odzwiercie­ dlaniem może się okazać bardzo potężnym narzędziem, które zostało opisane w dalszej części tego rozdziału.

Cele atrybutów W bibliotece klas platformy .NET można znaleźć mnóstwo różnych atrybutów. Niektóre z nich mogą zostać zastosowane do podzespołów, inne do klas lub interfejsów, a jeszcze inne - takie jak Xml I gnore - do pól i właściwości . Większość atrybutów ma sens wyłącznie wtedy, gdy są związane z określonymi rzeczami - atrybutu Xml I gnore nie można użytecznie zastosować na przykład do metody, ponieważ metod nie da się serializować do postaci kodu XML. Co za tym idzie, każdy typ atrybutu deklaruje cele atrybutu (ang. attribute targets) za pomocą enumeracji Attri buteTarget s . Znaczenie większości pozycji tego wyliczenia jest dość łatwe do zrozumienia, jednak nie musi być w każdym przypadku oczywiste, dlatego pełna lista wraz z krótkimi opi­ sami została przedstawiona w tabeli 1 7.1 .

Tabela 17.1 . Możliwe cele atrybutów Nazwa składowej

Al l

Element, do którego atrybut może zostać zastosowany Każdy z następujących elementów: podzespół, klasa, konstruktor, delegacja, enumeracja, zdarzenie, pole, interfejs, metoda, moduł, parametr, właściwość, wartość zwracana lub struktura

Assemb l y

Podzespół

Cl ass

Klasa

Constructor

Konstruktor

Del egate

Delegacja

En urn

Enumeracja

Event

Zdarzenie

F i el d

Pole

Generi cParameter

Parametr typu dla metody lub klasy ogólnej

I nterface

I nterfejs

Met hod

M etoda

Modul e

Moduł

Parameter

Parametr metody

Property

Właściwość (zarówno

ReturnVal ue

Wartość zwracana

Struct

Struktura

676

I

get, jak i set, jeśli zostały zaimple mentowane)

Rozdział 17. Atrybuty i odzwierciedlanie

Stosowanie atrybutów Większość atrybutów stosuje się do ich celów, umieszczając je w nawiasach kwadratowych bez­ pośrednio przed elementami będącymi ich celem. Kilka typów celów nie stanowi jednak żadnej pojedynczej składowej kodu, dlatego w ich przypadku sprawy mają się nieco inaczej. Podzespół ma na przykład postać pojedynczego skompilowanego pliku wykonywalnego lub biblioteki .NET - w jednym projekcie znajduje się tu dosłownie wszystko, nie ma więc jakiegoś okre­ ślonego elementu w kodzie źródłowym, do którego dałoby się zastosować atrybut. Z tego powodu atrybuty podzespołów można umieszczać na początku dowolnego pliku. Podobnie jest w przypadku atrybutów, których celami są moduły 1 . .·

Atrybuty podzespołów i modułów należy umieszczać po wszystkich dyrektywach usi ng, lecz przed jakimkolwiek kodem.

Można korzystać z wielu atrybutów naraz. W takim przypadku wymienia się je po prostu po kolei: [as semb l y : Assembl yDel ayS i gn (fal se) ] [as semb l y : Assembl yKeyFi l e ( " . \ \ keyFi l e . sn k " ) ]

Zamiast tego można również umieścić wszystkie atrybuty wewnątrz wspólnego nawiasu kwa­ dratowego, oddzielając poszczególne pozycje przecinkami: [as semb l y : Assembl yDel ayS i gn (fal se) , as semb l y : Assembl yKeyFi l e ( " . \ \ keyFi l e . sn k " ) ]

Przestrzeń nazw Sys tern . Refl ect i o n oferuje wiele różnych rodzajów atrybutów, w tym atrybuty przeznaczone dla podzespołów (takie jak Assemb l yKey F i l eAt tri bute), konfiguracji oraz wersji. Niektóre z nich są rozpoznawane przez kompilator - na przykład atrybut pliku klucza jest wyko­ rzystywany w sytuacji, gdy kompilator generuje podpis cyfrowy tworzonego komponentu.

Własne atrybuty Możemy tworzyć własne atrybuty i korzystać z nich w czasie wykonania programu, jeśli jest nam to do czegoś potrzebne . Załóżmy, że organizacja, dla której pracujemy, chce śledzić poprawki błędów. Dysponujemy już bazą danych wszystkich wykrytych problemów, lecz chcielibyśmy powiązać swoje raporty błędów z określonymi poprawkami w kodzie. Obok odpowiednich wierszy kodu moglibyśmy dodawać komentarze takie jak ten: li Blqd 323. poprawiony przez Romana Hermana 1 .01 .2011

Znacznie ułatwi to odnalezienie takiego wiersza w kodzie źródłowym, lecz z uwagi na to, że komentarze są pomijane podczas kompilacji, informacje te nie przedostaną się do skompilowa­ nego kodu. Jeśli zależy nam na zmianie tego stanu rzeczy, możemy skorzystać z własnego atry­ butu. W tym celu należy zastąpić powyższy komentarz zapisem podobnym do następującego: [BugFi xAttri bute (323 , " Roman Herman " , " 1 - 1 -20 1 1 " , Comment = " Bł'ąd przesun i ęci a " ) ] 1 Moduły to poszczególne pliki, które tworzą podzespoły. Ogromna większość podzespołów składa się tylko

z jednego pliku, dlatego bardzo rzadko zdarzają się sytuacje, gdy trzeba zajmować się pojedynczymi modułami zamiast całymi podzespołami. Moduły zostały tu jednak wspomniane, aby podane informacje były pełne.

Atrybuty

I

677

Następnie moglibyśmy napisać program odczytujący odpowiednie metadane, aby odszukiwać swoje adnotacje na temat poprawek błędów i ewentualnie aktualizować bazę danych prze­ chowującą informacje o zgłoszonych problemach. Atrybut będzie tu spełniał rolę komentarza, umożliwi też jednak automatyczne pobieranie istotnych informacji za pomocą narzędzi progra­ mistycznych, które opracujemy. '

. .

"'; ....,'.___�_

Przykład ten może się wydawać trochę sztuczny, ponieważ w rzeczywistości raczej nie będziemy chcieli, aby tego typu informacje zostały wkompilowane w ostateczną wersję kodu, która trafi do odbiorców .

Defin iowanie własnych atrybutów Atrybuty, podobnie jak większość elementów w języku C#, są realizowane za pomocą klas. Aby utworzyć własny atrybut, powinniśmy utworzyć klasę pochodną wobec klasy System . Attri bute: publ i c cl ass BugFi xAttri bute : System . Attri bute

Musimy też poinformować kompilator, z jakimi rodzajami elementów atrybut ten może być używany (czyli określić cel atrybutu) . Robi się to za pomocą (jakżeby inaczej) atrybutu: [Attri buteU s age (Att r i buteTargets . Cl ass I Attri buteTargets . Cons t ructor I Attri buteTarget s . Fi el d I Attri buteTargets . Method I Attri buteTargets . Property , Al l owMul t i pl e = true) ]

At tri buteUsage to atrybut stosowany do klasy atrybutu. Zapewnia on dane na temat metadanych, które można określić mianem metaatrybutu (ang. meta-attribute) .

Konstruktorowi atrybutu Attri buteUsage zostały tu przekazane dwa argumenty. Pierwszym jest zbiór flag wskazujących cel - w przedstawionym powyżej przykładzie stanowią go: sama klasa i jej konstruktor, pola, metody i właściwości . Drugim argumentem jest flaga określająca, czy dany element może przyjmować więcej niż jeden atrybut tego rodzaju. W naszym przykła­ dzie właściwość A 1 1 owMu l t i p l e została ustawiona na wartość t rue, co wskazuje, że do składowych klasy może być przypisana większa liczba atrybutów B ug F i xAttri bute.

Nazywan ie atrybutów Nowy atrybut własny przedstawiony powyżej otrzymał nazwę Bug Fi xAttri bute. Zgodnie z kon­ wencją do wybranej nazwy atrybutu należy dołączyć słowo Attri bute. Kompilator obsługuje tę konwencję, umożliwiając korzystanie w momencie stosowania atrybutu z krótszej wersji nazwy. Dzięki temu można użyć następującego zapisu: [BugFi x ( 123 , " Roman Herman " , " 1 - 1 -2008 " , Comment = " Bł ąd przesun i ęc i a " ) ]

Napotkawszy taki zapis, kompilator spróbuje najpierw odnaleźć klasę atrybutu o nazwie B ug F i x, a gdy mu się to nie uda, poszuka klasy B ug F i xAtt r i bu te.

Konstruowanie atrybutów Mimo że atrybuty mają konstruktory, składnia, której używa się przy stosowaniu atrybutów, nie przypomina zbytnio składni wykorzystywanej w przypadku normalnych konstruktorów. Można tu przekazywać dwa rodzaje argumentów: pozycyjne (ang. positional) i nazwane (ang.

678

I

Rozdział 17. Atrybuty i odzwierciedlanie

named) . W przykładzie z atrybutem B ug F i x nazwisko programisty, identyfikator błędu oraz data są argumentami pozycyjnymi, zaś Comment jest argumentem nazwanym. Argumenty pozy­ cyjne są przekazywane przez konstruktor i muszą być podane w kolejności, w której zostały w nim zadeklarowane odpowiednie parametry: publ i c BugFi xAttri bute ( i nt bug I D , s t r i ng programmer , s t r i ng date) { t h i s . Bug I D = bug I D ; t h i s . Programmer = programmer ; t h i s . Date = date ;

Argumenty nazwane są implementowane jako pola lub właściwości: publ i c stri ng Comment { get ; set ; }

'

. .

Być może Czytelnik zastanawia się, dlaczego w przypadku argumentów nazwanych atrybutów stosuje się inną składnię, niż ma to miejsce w zwykłych wywołaniach kon­ struktorów i metod, w których argumenty nazwane przyjmują formę typu Comment : " Błąd przesun i ęci a " , a więc z dwukropkiem zamiast znaku równości. Przyczyny tej niespójności zapisu mają charakter historyczny. Atrybuty zawsze obsługiwały argu­ menty pozycyjne i nazwane, podczas gdy w wywołaniach metod i zwykłych konstrukto­ rów można je stosować w języku C# dopiero od wersji 4.0. Działanie tych mechani­ zmów różni się dość znacznie: składnia nazwanych argumentów w C# 4.0 służy głównie do obsługi argumentów opcjonalnych i ma zastosowanie tylko do prawdzi­ wych argumentów metod, podczas gdy argumenty nazwane związane z atrybutami w gruncie rzeczy w ogóle nie są argumentami - tak naprawdę są one właściwościami w przebraniu". „

Często stosowanym rozwiązaniem jest tworzenie dla argumentów pozycyjnych właściwości tylko do odczytu: publ i c i nt Bug I D { get ; pri vate set ; }

Używan ie atrybutów Po zdefiniowaniu atrybutu można zacząć z niego korzystać, umieszczając go bezpośrednio przed odpowiednim celem. Aby przetestować przedstawiony wcześniej przykładowy atrybut Bug F i x "+At tri bute, w zamieszczonym poniżej programie utworzono prostą klasę o nazwie MyMath posia­ dającą dwie funkcje składowe. Przypisanie atrybutu Bug Fi xAt tri bute do tej klasy w celu zapew­ nienia mechanizmu śledzenia historii konserwacji kodu powinno mieć następującą postać: [BugFi xAttri bute ( 12 1 , " Roman Herman " , " 0 1 -03-08 " ) ] [BugFi xAttri bute ( 1 07 , " Roman Herman " , " 0 1 -04-08 " , Comment= " Poprawi ony b ł ąd przesun i ęc i a " ) ] publ i c cl ass MyMath

Atrybuty te są przechowywane w metadanych. Listing 17.1 zawiera pełny kod programu.

Listing 1 7. 1 . Korzystanie z własnych atrybutów us i ng Sys tem ; namespace Cus tomAt t r i butes { li Tworzenie własnego atrybutu, który ma być przypisywany składowym klasy [Attri buteUsage (Attri buteTargets . Cl ass I Attri buteTargets . Constructor I

Atrybuty

I

679

Attri buteTargets . Fi el d I Attri buteTargets . Method I Attri buteTargets . Property , Al l owMu l t i pl e = true) ] publ i c cl ass Bug F i xAttri bute : Sys tem . Attri bute { li Konstruktor atrybutu dla parametrów pozycyjnych publ i c Bug Fi xAttri bute ( i nt bug I D , s t r i ng programmer , s t r i ng date t h i s . Bug I D = bug I D ; t h i s . Programmer = programmer ; t h i s . Date = date ;

li Akcesory publ i c i nt Bug I D { get ; pri vate set ; } publ i c s t r i ng Date { get ; pri vate set ; publ i c s t r i ng Programmer { get ; pri vate set ; li Wlaściwość dla parametru nazwanego publ i c s t r i ng Comment { get ; set ; } li ********* Przypisywanie atrybutów do klasy ******** [Bug F i xAttri bute ( 12 1 , " Roman Herman " , " 0 1 -03-08 " ) ] [Bug Fi xAttri bute ( 107 , " Roman Herman " , " 0 1 -04-08 " , Comment= " Poprawi ony b ł ąd przesun i ęc i a " ) ] publ i c cl ass MyMath { publ i c doubl e DoFuncl (doubl e param l ) { return paraml + Do Func2 (param l ) ; publ i c doubl e Do Func2 (doubl e param l ) { return paraml / 3 ;

publ i c cl ass Tes ter { stat i c voi d Mai n (stri ng O arg s ) { MyMath mm = new MyMath () ; Consol e . Wri teli ne ( "Wywo ł an i e DoFunc ( 7 ) . Wyn i k : { O } " , mm . DoFunc 1 (7 ) ) ;

Wynik działania programu: Wywołan i e DoFunc ( 7 ) . Wyn i k : 9 , 3333333333333333

Jak widać, atrybuty nie mają absolutnie żadnego wpływu na wynik działania programu. Nie jest to wielką niespodzianką, ponieważ - jak zostało to napisane wcześniej - mają one charak­ ter pasywny, co oznacza, że oddziałują jedynie na te elementy, które ich szukają, a jak na razie

680

I

Rozdział 17. Atrybuty i odzwierciedlanie

w kodzie nie znalazło się nic, co mogłoby to robić. W gruncie rzeczy na tę chwilę Czytelnik musi uwierzyć nam na słowo, że atrybuty te w ogóle istnieją. Sposób wydobycia tych metada­ nych i wykorzystania ich w programie zostanie przedstawiony w następnym podrozdziale.

Odzwiercied lan ie Aby atrybuty zapisane w metadanych mogły się do czegoś przydać, niezbędny jest sposób uzy­ skania do nich dostępu w czasie wykonania programu. Możliwości sprawdzania tych metada­ nych i wpływania na nie zapewniają klasy należące do przestrzeni nazw Refl ect i on wraz z klasą Sys t em . Type. Odzwierciedlanie wykorzystuje się głównie w przypadku następujących czterech operacji: Badanie metadanych

Rozwiązanie to może być wykorzystywane przez narzędzia i funkcje, których zadaniem jest wyświetlanie metadanych, lub też przez składowe bibliotek klas, które modyfikują swoje działanie w oparciu o metadane. Odkrywanie typów

W kodzie programu można sprawdzać typy obecne w podzespole i oddziaływać z nimi lub tworzyć ich obiekty. Aplikacja obsługująca wtyczki może korzystać z tego rozwiązania, aby sprawdzać, jakie możliwości oferuje plik DLL wtyczki. Późne wiązanie z metodami i właściwościami

Rozwiązanie to umożliwia programiście wywoływanie właściwości i metod na rzecz dyna­ micznie tworzonych obiektów w oparciu o wynik odkrywania typów. Technika ta jest rów­ nież znana pod nazwą dynamicznego wywoływania (ang. dynamie invocation) . (Jak Czytelnik wkrótce się przekona, czytając rozdział 18., w języku C# 4.0 wprowadzono prostszy sposób osiągania tego celu niż korzystanie z odzwierciedlania) . Tworzenie typów w czasie wykonania

W czasie wykonania programu da się generować nowe typy. Można skorzystać z tej możli­ wości w sytuacji, gdy własna klasa zawierająca kod generowany w czasie wykonania i prze­ znaczona do przeprowadzenia określonej operacji będzie działać znacznie szybciej niż rozwiązanie o bardziej ogólnym zastosowaniu. Jest to jednak zaawansowana technika, która wykracza poza zakres tematyczny niniejszej książki .

Badanie metadanych W tym punkcie wykorzystamy obsługę mechanizmu odzwierciedlania zapewnianą przez język C#, aby odczytać metadane związane z klasą MyMat h . System odzwierciedlania definiuje wiele klas, z których każda ma dostarczać informacji na temat określonego rodzaju metadanych. Na przykład klasa Con structor l n fo zapewnia dostęp do wszystkich metadanych dotyczących konstruktora, zaś zadaniem klasy Property I n fo jest dostar­ czenie metadanych związanych z właściwością. Nasz atrybut własny przedstawiony na lis­ tingu 17.1 może być stosowany do szerokiego spektrum celów, dlatego spotkamy się z kilkoma różnymi typami metadanych . Jednak wszystkie obsługiwane przez ten atrybut cele mają pewną wspólną cechę, a mianowicie taką, że mogą być składowymi klas . (Jest to oczywistą prawdą w przypadku właściwości, metod, pól i konstruktorów. Nasz atrybut może być również Odzwierciedlanie

I

681

stosowany do klas, które wydają się tu wyjątkiem, ponieważ nie stanowią składowych żad­ nych innych typów, chodzi jednak o to, że potencjalnie mogą nimi być) . Z tego zaś wynika, że typy metadanych związanych ze wszystkimi obsługiwanymi w naszym przypadku typami celów dziedziczą po jednej wspólnej klasie bazowej, którą jest Member l n fo . Klasa Member l n fo została zdefiniowana w przestrzeni nazw Sys t em . Refl ect i on . Możemy z niej skorzystać, aby poznać atrybuty składowej i zapewnić dostęp do odpowiednich metadanych. Zaczniemy od przechwycenia metadanych związanych z określonym typem: Sys tem . Refl ect i on . Memberlnfo i nf = typeof (MyMath) ;

W stosunku do typu MyMath używamy tu operatora typeof, który zwraca obiekt typu Type dzie­ dziczącego po klasie Member l n fo . ••



.

·

.._,..�;

,

L-------11.J"' '

Klasa Type stanowi serce zbioru klas odpowiedzialnych za odzwierciedlanie. Zawiera ona reprezentację typu obiektu. Klasa ta zapewnia podstawowy sposób dostępu do metadanych - można jej używać do przechwytywania informacji na temat innych składowych klasy (takich jak metody, właściwości, pola, zdarzenia itd.) .

Kolejnym krokiem jest wywołanie metody GetCus tomAt t ri b u t e s na rzecz tego obiektu klasy Member l n fo, przy czym w postaci argumentu należy tu podać typ atrybutu, który ma zostać odnaleziony. Metoda ta zwraca tablicę obiektów, z których każdy jest typu B ug F i xAttri bu te: obj ect [] attri butes ; attri butes = i nf . GetCustomAttri butes (typeof (Bug Fi xAttri bute) , fal se) ;

Dysponując tą tablicą, możemy przejść przez wszystkie jej elementy, wyświetlając na ekranie właściwości obiektu B ug F i xAttri bute. Na listingu 17.2 przedstawiona została zmodyfikowana wersja kodu metody Mai n należąca do klasy Tester z listingu 17. 1 .

Listing 17.2 . Korzystanie z odzwierciedlania publ i c s t at i c vo i d Ma i n (stri ng O arg s ) { MyMath mm = new MyMath () ; Consol e . Wri teli ne ("Wywołan i e DoFunc (7) . Wyn i k : {O } " , mm . Do Func 1 (7 ) ) ;

li Pobranie informacji na temat skladowej i użycie jej do wydobycia wlasnych atrybutów Sys tem . Refl ect i on . Memberl nfo i nf = typeof (MyMath) ; obj ect [] attri butes ; attri butes = i nf . GetCus tomAttri butes (typeof (BugFi xAttri bute) , fal se) ; li Przejście przez atrybuty z wydobywaniem wlaściwości foreach (Obj ect attri bute i n attri butes) { Bug Fi xAttri bute bfa = (BugFi xAttri bute) attr i bute ; Consol e . Wr i teli ne ( 11 \n l dentyfi kator b ł ędu : { O } 11 , bfa . Bug I D) ; Consol e . Wr i teli ne ( 11 Programi s t a : { 0 } 11 , bfa . Programmer) ; Consol e . Wr i teli ne ( 11 Data : { O } 11 , bfa . Date) ; Consol e . Wr i teli ne ( 11 Komentarz : { O} 11 , bfa . Comment) ;

Wynik wykonania programu jest następujący: Wywołan i e DoFunc ( 7 ) . Wyn i k : 9 , 33333333333333 I dentyfi kator b ł ędu : 1 2 1 Programi sta : Roman Herman

682

I

Rozdział 17. Atrybuty i odzwierciedlanie

Dat a : 0 1 -03 - 08 Komentarz : I dentyfi kator b ł ędu : 107 Programi sta : Roman Herman Dat a : 0 1 -04-08 Komentarz : Poprawi ony b ł ąd przesun i ęc i a

Gdy zastąpimy tę metodę na listingu 17.1 zmodyfikowanym kodem przedstawionym powy­ żej, a następnie uruchomimy program, na ekranie pojawią się odpowiednie metadane, tak jak można się było spodziewać.

Odkrywanie typów Odzwierciedlania można używać do badania i sprawdzania zawartości podzespołów. Można także odszukiwać za jego pomocą typy, które się w nich znajdują. Da się również odkrywać metody, pola, właściwości i zdarzenia związane z typem oraz sygnatury wszystkich metod tego typu. Ponadto można odkrywać interfejsy obsługiwane przez dany typ oraz jego klasę bazową. Gdybyśmy korzystali z tego rozwiązania do obsługi systemu wtyczek rozszerzających naszą aplikację, musielibyśmy mieć możliwość ładowania w czasie wykonania podzespołów, których nie znalibyśmy na etapie pisania jej kodu. Podzespół można dynamicznie załadować za pomocą statycznej metody A s s emb l y . Load. Na potrzeby odzwierciedlania klasa Assemb l y zawiera sam właściwy podzespół. Jedna z sygna­ tur metody Load ma następującą postać: publ i c s t at i c As semb l y Load (stri ng assembl yName)

Na przykład biblioteka Mscorlib.dll zawiera podstawowe klasy platformy .NET, dzięki czemu możemy przekazać ją w roli argumentu metodzie Load: Assembl y a = Assembl y. Load ( "Mscorl i b " ) ;

(W rzeczywistości biblioteka Mscorlib.dll będzie już załadowana, ale jest to bez znaczenia, ponie­ waż metoda ta zwraca podzespół, o który ją poprosimy, ładując go wcześniej, jeśli to konieczne) . Istnieje również metoda Load From przyjmująca w roli argumentu ścieżkę dostępu do pliku. Po załadowaniu odpowiedniego podzespołu możemy wywołać metodę GetTypes, która zwróci tablicę obiektów klasy Type. Obiekt taki reprezentuje deklarację określonego typu takiego jak klasa, interfejs, tablica, struktura, delegacja lub enumeracja: Type [] types = a . GetTypes () ;

Podzespół zwraca tablicę typów, które można wyświetlić na ekranie, korzystając z pętli foreach, tak jak zostało to pokazane na listingu 17.3. Z racji tego, że w przykładzie zastosowana została klasa Type, konieczne jest dodanie dyrektywy u s i ng odwołującej się do przestrzeni nazw System . "+Refl ect i on.

Listing 17.3. Zastosowanie odzwierciedlania w stosunku do podzespołu us i ng Sys tem ; us i ng Sys tem . Refl ect i on ; namespace Refl ecti ngAnAs sembl y { publ i c cl ass Tes ter {

Odzwierciedlanie

I

683

publ i c stat i c vo i d Mai n () { li Sprawdzanie, co znajduje się w podzespole Assemb l y a = Assemb l y . Load ( "Ms corl i b " ) ; Type [] types = a . GetTypes () ; foreach (Type t i n types ) { Consol e . Wri te l i ne ( " Typem j es t { O } " , t) ; Consol e . Wri teli ne ( " Znal ez i ono { O } typów " , types . Length) ;

Wynikiem wykonania tego programu dałoby się zapełnić wiele stron. Oto krótki fragment: Typem Typem Typem Typem Typem Typem Typem Typem

j es t j es t j es t j es t j es t j es t j es t j es t

Sys tem . Obj ect T h i sAs s emb l y Assembl yRef Sys tem . I Cl oneabl e Sys tem . Col l ect i ons . I Enumerabl e Sys tem . Col l ect i ons . I Col l ect i on Sys tem . Col l ect i ons . I li st Sys tem . Array

W przykładzie tym otrzymaliśmy tablicę wypełnioną informacjami na temat typów należących do podstawowej biblioteki, a następnie wydrukowaliśmy je jedną po drugiej . Tablica zawiera 2779 elementów, gdy program jest uruchamiany przy użyciu platformy .NET w wersji 4.0 .

Odzwierciedlan ie na rzecz określonego typu Zamiast przechodzić przez wszystkie typy, można też poprosić system odzwierciedlania o infor­ macje na temat jednego wybranego typu. Może się to wydawać nieco dziwne - skoro już wiemy, o jaki typ nam chodzi, po co mielibyśmy korzystać z odzwierciedlania do sprawdzania informacji na jego temat w czasie wykonania? W rzeczywistości może się to przydać z kilku różnych powodów. Niektóre aplikacje umożliwiają użytkownikom umieszczanie nazwy wyma­ ganego typu w pliku konfiguracyjnym, tak że program poznaje ją dopiero w czasie wykona­ nia i musi wyszukać tylko ten jeden konkretny typ . Aby skorzystać z tej możliwości, należy wydobyć typ z podzespołu za pomocą metody GetType, tak jak zostało to przedstawione na listingu 17.4.

Listing 1 7.4. Odzwierciedlanie na rzecz typu us i ng Sys tem ; us i ng Sys tem . Refl ect i on ; namespace Refl ecti ngOnAType { publ i c cl ass Tes ter { publ i c stat i c vo i d Mai n () { li Sprawdzanie pojedynczego typu Assemb l y a = Assemb l y . Load ( "Ms corl i b " ) ; Type theType = a . GetType ( " System . Refl ect i on . Assemb l y " ) ; Consol e . Wri teli ne ( " \n Poj edynczym typem j e s t { O } \n " , theType) ;

684

I

Rozdział 17. Atrybuty i odzwierciedlanie

Wynik: Poj edynczym typem j es t Sys tem . Refl ect i on . As s emb l y

Czasami przydatne może się okazać przechwycenie obiektu Type dla określonego typu, który jest znany na etapie kompilacji . Może się to wydawać dziwne z powodów wspomnianych wcześniej, ale najważniejszą przyczyną wykonywania tej operacji nie jest bynajmniej to, że da się w ten sposób dowiedzieć czegoś na temat typu. Rozwiązanie to może się okazać pomocne, gdy zachodzi potrzeba porównania jednego obiektu typu z innym. Gdybyśmy na przykład chcieli odnaleźć wszystkie typy należące do biblioteki Mscorlib, które dziedziczą po klasie Memberl n fo, powinniśmy przechwycić obiekt Type związany z tą klasą. Sposób poradzenia sobie z tym zadaniem został przedstawiony na listingu 17.5.

Listing 1 7.5. Korzystanie z obiektu określonego typu w celach porównawczych us i ng Sys tem ; us i ng Sys tem . L i n ą ; us i ng Sys tem . Refl ect i on ; namespace U s i ngASpe c i fi cType { publ i c cl ass Tes ter { publ i c stat i c vo i d Mai n () { li Sprawdzanie pojedynczego typu Assemb l y a Assemb l y . Load ( "Ms corl i b " ) ; =

var mat c h i ngTypes

=

from t i n a . GetTypes () where typeof (Memberlnfo) . I sAs s i gnabl e From (t)

sel ect t ; foreach (Type t i n match i ngTypes) { Consol e . Wri te li ne (t) ;

W przykładzie tym do odnalezienia odpowiednich typów wykorzystane zostało zapytanie LINQ. Ilustruje on jedną z rzeczy, które można zrobić z obiektem Type - zastosowanie metody I sAss i gnab l eFrom do określenia, czy da się przypisać instancję jednego typu do pola lub zmien­ nej innego. W kodzie tym sprawdzamy zatem każdy typ pod kątem tego, czy spełnia on waru­ nek możliwości przypisania go do zmiennej typu Memberl n fo. (Dzięki temu sieć przeszukiwana jest nieco szerzej, niż miałoby to miejsce, gdybyśmy brali pod uwagę jedynie klasę bazową zapytanie to odszuka wszystkie typy, które bezpośrednio lub pośrednio dziedziczą po klasie Memberl n fo) . Z uwagi na to, że dokładnie wiemy, jaki docelowy typ nas interesuje, możemy sko­ rzystać z operatora typeof w celu otrzymania obiektu klasy Type dla tego konkretnego typu.

Odszukiwan ie wszystkich składowych typu Wszystkie składowe obiektu klasy Typ e możemy sprawdzić za pomocą należącej do tej klasy metody GetMembers . Metoda ta zwraca listę wszystkich metod, właściwości i pól należących do danego typu, tak jak zostało to pokazane na listingu 17.6.

Odzwierciedlanie

I

685

Listing 17.6. Odzwierciedlanie na rzecz składowych typu us i ng Sys tem ; us i ng Sys tem . Refl ect i on ; namespace Refl ect i ngOnMembersOfAType { publ i c cl ass Tes ter { publ i c stat i c vo i d Mai n () { li Sprawdzanie pojedynczego typu Assemb l y a = Assemb l y . Load ( "Ms corl i b " ) ; Type theType = a . GetType ( " System . Refl ect i on . Assemb l y " ) ; Consol e . Wri teli ne ( " \n Poj edynczym typem j e s t { O } \n " , theType) ;

li Pobranie wszystkich składowych Memberl nfo [] mbrinfoArray theType . GetMembers () ; foreach (Memberlnfo mbrl n fo i n mbrlnfoArray) { Consol e . Wri te li ne ( " { O } to { 1 } " , mbrlnfo , mbri nfo . MemberType) ; =

Również w tym przypadku tekst będący efektem działania programu ma dość znaczną długość, jednak zobaczymy w nim pola, metody, konstruktory i właściwości, tak jak zostało to pokazane w poniższym fragmencie. Sys tem . Type GetType (Sys tem . Stri ng , Bool ean , Bool ean) to Method Sys tem . Type [] GetExportedTypes () to Method Sys tem . Stri ng CodeBase to Property Sys tem . Refl ect i on . Modul eResol veEventHandl er Modu l eResol ve to Event

Odszukiwan ie metod typu Niewykluczone, że będziemy chcieli skupić się wyłącznie na metodach, pomijając pola, wła­ ściwości i całą resztę składowych. W tym celu należy odszukać w przedstawionym powyżej kodzie wywołanie metody GetMembers: Memberlnfo [] mbrinfoArray

=

theType . GetMembers () ;

i zastąpić je wywołaniem metody GetMet hods: Memberlnfo [] mbrlnfoArray

=

theType . GetMethods () ;

Tekst stanowiący wynik wykonania programu zawiera teraz wyłącznie metody, jak widać to na przykładzie tego krótkiego fragmentu: Bool ean Equal s (Sys tem . Obj ect) to Method Sys tem . Stri ng ToSt r i ng () to Method Sys tem . Stri ng CreateQual i fi edName (System . Stri ng , System . Stri ng) to Method Bool ean get_Gl obal Assembl yCache () to Method

Późne wiązan ie Po znalezieniu metody można j ą wywołać, korzystając z mechanizmu odzwierciedlania. Można by na przykład wywołać należącą do klasy Sys t em . Ma t h metodę Cos, która zwraca cosinus podanego kąta .

686

I

Rozdział 17. Atrybuty i odzwierciedlanie

' ..

Metodę Cos możemy oczywiście wywołać w swoim kodzie w standardowy sposób, jednak odzwierciedlanie umożliwia wiązanie z tą metodą w czasie wykonania pro­ gramu. Technika ta, określana mianem późnego wiązania (ang. late binding), oferuje elastyczność przeprowadzanego podczas wykonania wyboru obiektu, z którym ma nastąpić wiązanie, a także programowe przeprowadzenie odpowiedniego wywołania. Możliwość taką zapewnia wprowadzone w języku C# 4.0 słowo kluczowe dynami c, które zostało dokładniej opisane w rozdziale 18., jednak czasami możemy chcieć prze­ jąć większą kontrolę nad mechanizmem odpowiadającym za późne wiązanie. Może się to okazać przydatne w sytuacjach, gdy tworzymy własny skrypt, który ma być uruchamiany przez użytkownika, lub korzystamy z obiektów, które nie są dostępne w czasie kompilacji.

Aby wywołać metodę Cos, należy najpierw pobrać informacje gromadzone przez klasę Type, które są związane z klasą Sys t em . Ma t h : Type theMathType

=

typeof (Sys tem . Math) ;

Dysponując informacjami na temat typu, moglibyśmy dynamicznie utworzyć jego instancję przy użyciu statycznej metody klasy Acti vator. W tym przypadku nie ma jednak takiej potrzeby, ponieważ metoda Cos ma charakter statyczny. Tak naprawdę wszystkie składowe klasy System . "+Math są statyczne i nawet gdybyśmy chcieli utworzyć odpowiednią instancję, nie mogliby­ śmy tego zrobić, ponieważ klasa System . Math po prostu nie ma publicznego konstruktora. Jednak z racji tego, że Czytelnik będzie miał do czynienia z typami, których obiekty trzeba będzie tworzyć, aby móc korzystać z ich składowych niestatycznych, powinien wiedzieć, jak powoływać do życia nowe obiekty za pomocą odzwierciedlania. Klasa Act i vator zawiera trzy statyczne metody, za pomocą których można tworzyć obiekty. Oto one: •

CreateCom i n s tance From - umożliwia tworzenie instancji obiektów COM;



Create i n stan ceFrom - umożliwia tworzenie odwołań do obiektów na podstawie nazwy podzespołu i typu;



Createl nstance - umożliwia tworzenie instancji określonego typu na podstawie obiektu klasy Type, na przykład: Obj ect theObj

=

Act i vato r . Create ins tance (someType) ;

Wróćmy do przykładu dotyczącego użycia metody Cos . Utworzona przez nas zmienna t h eMath 4Type odwołuje się do obiektu klasy Type, który otrzymaliśmy, wywołując metodę GetType. Zanim będziemy mogli wywołać jakąś metodę na rzecz obiektu tego typu, musimy ją pobrać z obiektu klasy Type. W tym celu należy wywołać metodę GetMet hod, przekazując jej w roli argu­ mentu nazwę odpowiedniej metody: Method l n fo cos i nelnfo

"'

„ "

.„

.„.„ . '

=

theMathType . GetMethod ( " Cos " ) ;

Oczywiście pojawia się tu pewien problem, gdy trzeba posługiwać się metodami przeładowanymi. Nie mamy z tym do czynienia w przykładzie przedstawionym powyżej, ponieważ w używanej klasie istnieje tylko jedna metoda o nazwie Cos. Jeśli jednak musimy korzystać z wielu metod o tej samej nazwie, możemy użyć innej wersji przeładowanej metody GetMethod, która przyjmuje dwa argumenty. W jej przy­ padku po nazwie metody można przesłać tablicę typów argumentów, dzięki którym będzie się dało jednoznacznie zidentyfikować wymagane przeładowanie. Gdybyśmy

Odzwierciedlanie

I

687

chcieli, moglibyśmy to zrobić również w tym przykładzie, choć nie jest to konieczne. Moglibyśmy zatem utworzyć tablicę Type [] zawierającą tylko jeden element: typeof 4 (doubl e) . W ten sposób poinformowalibyśmy metodę GetMethod, że zależy nam na wywołaniu konkretnie tej metody o nazwie Cos, która przyjmuje pojedynczy argu­ ment typu doubl e.

Na tym etapie dysponujemy obiektem typu Met h od l n fo, który udostępnia metodę I n voke umożliwiającą wywołanie metody reprezentowanej przez ten obiekt. W normalnym przypadku pierwszym argumentem metody I nvoke byłby obiekt, na rzecz którego chcemy wywołać odpo­ wiednią metodę, jednak z racji tego, że mamy do czynienia z metodą statyczną, obiekt taki nie istnieje i powinniśmy przekazać wartość n ul l . Następnie należy podać argumenty wywoły­ wanej funkcji. Metoda I n voke jest w stanie wywołać dowolną metodę niezależnie od liczby argumentów, które ona przyjmuje, dlatego argumenty te powinny być umieszczone w tablicy nawet wówczas, gdy ich liczba ogranicza się do jednego: Obj ect [] parameters = new Obj ect [ l] ; parameters [OJ = 45 * (Math . P I / 180) ; li 45 stopni w radianach Obj ect returnVal = cos i ne l nfo . I nvoke (nu l l , parameters ) ;

Listing 17.7 przedstawia wszystkie kroki wymagane do dynamicznego wywołania metody Cos .

Listing 1 7.7. Dynamiczne wywoływanie metody us i ng Sys tem ; us i ng Sys tem . Refl ect i on ; namespace Dynami cal l yinvoki ngAMethod { publ i c cl ass Tes ter { publ i c stat i c vo i d Mai n () { Type theMathType = Type . GetType ( " System . Math " ) ; li Z uwagi na to, że klasa System.Math nie ma publicznego konstruktora, poniższa instrukcja li spowodowalaby zgłoszenie wyjątku. li Object theObj Activator.Createlnstance(theMathType); =

li Tablica zawierająca jeden element Type [] paramTypes = new Type [l] ; paramTypes [O] Type . GetType ( " System . Doubl e " ) ; =

li Pobranie informacji na temat metody Cos Method l nfo Cos i ne l n fo = theMathType . GetMethod ( " Cos " , paramTypes) ; li Wypełnienie tablicy odpowiednimi wartościami argumentów Obj ect [] parameters = new Obj ect [ l] ; parameters [OJ = 45 * (Math . P I / 180) ; li 45 stopni w radianach Obj ect returnVal = Cos i ne i n fo . I nvoke (theMathType , parameters ) ; Consol e . Wri teli ne ( " Cos i nus kąta 45 s topn i wynos i { O } " , returnVal ) ;

Wynik wykonania powyższego programu będzie następujący: Cos i nus kąta 45 s topn i wynos i 0 , 707 1067 8 1 186548

688

I

Rozdział 17. Atrybuty i odzwierciedlanie

Wywołanie pojedynczej metody kosztowało nas sporo pracy. Prawdziwa potęga tego rozwią­ zania polega jednak na tym, że da się skorzystać z mechanizmu odzwierciedlania, aby zbadać podzespół znajdujący się na maszynie użytkownika w celu sprawdzenia, jakie metody on ofe­ ruje, a także aby dynamicznie wywołać wybraną z nich. Z rozdziału 18. dowiemy się, jak można używać słowa kluczowego dynam i c, aby zautomatyzować ten proces w określonych sytuacjach.

Podsu mowan ie Wszystkie komponenty .NET zawierają metadane. Niektóre z nich stanowią podstawowe infor­ macje o strukturze kodu - zawierają listy typów, ich nazw, składowych, które definiują, argu­ mentów przyjmowanych przez metody i tak dalej . Jednak system metadanych ma również charakter rozszerzalny - atrybuty mogą być osadzane wraz z podstawowymi metadanymi i da się je sprawdzać w czasie wykonania programu. Niektóre możliwości związane z metadanymi pozwalają ponadto korzystać z reprezentowanych przez nie elementów. Da się w ten sposób na przykład użyć zdobytej dynamicznie informacji na temat metody do jej wywołania .

Podsumowanie

I

689

690

I

Rozdział 17. Atrybuty i odzwierciedlanie

ROZDZIAŁ 18.

Typ dynamie

Starsze wersje języka C# miały pewne problemy z interakcją z określonymi rodzajami progra­ mów, zwłaszcza z aplikacjami należącymi do rodziny Microsoft Office . Dało się co prawda wykonać odpowiednie operacje, ale przed nastaniem ery C# 4.0 wymagało to dużego wysiłku, a uzyskane efekty były raczej żałosne . Problem wynikał ze zderzenia filozofii: pakiet Office prezentuje styl dynamiczny, podczas gdy język C# mocno skłaniał się ku stylowi statycznemu . Na szczęście standard C# 4.0 zapewnia znacznie lepszą obsługę stylu dynamicznego, ułatwiając w ten sposób programowanie aplikacji Microsoft Office i podobnych systemów za pomocą języka C#.

Styl statyczny kontra styl dynam iczny Na czym dokładnie polega różnica między stylem statycznym a dynamicznym? Używana terminologia może być nieco myląca, ponieważ w języku C# istnieje słowo kluczowe stat i c, które nie ma żadnego związku z opisywaną tu kwestią. Dlatego też na razie Czytelnik powinien odłożyć na bok swoją wiedzę związaną z tym słowem kluczowym. Gdy mowa jest o rozróżnie­ niu pomiędzy statycznością i dynamicznością, chodzi o to, że dynamiczne jest coś, o czym decy­ zja zostaje podjęta w czasie wykonania programu, podczas gdy rzecz statyczna określana jest na etapie jego kompilacji . Jeśli to wyjaśnienie brzmi dla Czytelnika nieco abstrakcyjnie, jest tak, ponieważ rozróżnienie to może mieć zastosowanie do wielu różnych rzeczy, na przykład do decyzji o tym, która metoda ma zostać wywołana, do typu zmiennej lub wyrażenia bądź też do znaczenia operatora. Przyjrzyjmy się więc jakiemuś konkretnemu przykładowi. Kompilator jest w stanie dowie­ dzieć się całkiem wielu rzeczy na temat kodu podczas jego kompilacji nawet w tak prostym przypadku jak ten, który został przedstawiony na listingu 18.1 .

Listing 18.1 . Prosty kod z różnymi elementami statycznymi var myStri ng = Cons o l e . Readli ne () ; var mod i fi edStri ng = myStri ng . Repl ace ( " col or" , " col our " ) ;

Skorzystaliśmy tu ze słowa kluczowego var, a więc nie poinformowaliśmy kompilatora, jaki typ mają mieć powyższe zmienne. Jest on jednak w stanie samodzielnie poradzić sobie z określe­ niem tego typu. Metoda Con sol e . Read l i n e zwraca wartość typu stri ng, co oznacza, że zmienna myStri ng ma mieć właśnie ten typ - w żadnym razie nie może on być inny, dlatego mówimy, że jest statyczny. (Rzecz jasna, tak samo byłoby w przypadku zmiennej, która zostałaby jawnie 691

zadeklarowana jako zmienna określonego typu, a więc zadeklarowanie mySt ri ng wprost jako zmiennej typu stri ng nic by tu nie zmieniło) . Podobnie będzie w przypadku zmiennej mod i fi ed '"+Stri ng - również tutaj kompilator poradzi sobie z określeniem, że chodzi o zmienną typu s t r i n g . Dowolna zmienna zadeklarowana przy użyciu słowa kluczowego var ma zatem typ statyczny. Oprócz typów zmiennych kompilator wyznacza w sposób statyczny również inne rzeczy w kodzie . Przykładem mogą być wywołania metod. Wywołanie metody Con sol e . Read l i ne jest bardzo proste . Con sol e to nazwa klasy, a więc w kodzie wyraźnie wskazane zostało miejsce, w którym należy szukać odpowiedniej metody. Z uwagi na fakt, że nie ma tu okazji do jakiej­ kolwiek niejednoznaczności w kwestii tego, o jaką metodę nam chodzi, jest to statyczne wywo­ łanie metody - w czasie kompilacji kodu dokładnie wiadomo, która metoda zostanie wywołana w czasie wykonania programu. Wywołanie metody mySt r i ng . Repl ace jest już nieco ciekawsze: nazwa myStri ng odnosi się do zmiennej, nie zaś klasy, dlatego aby wiedzieć, która metoda zostanie wywołana, musimy znać typ tej zmiennej . Jednak jak już wiemy, w naszym przykładzie typ zmiennej myStri ng jest wyzna­ czany statycznie jako stri ng. Jak to się nieraz zdarza, metoda Rep l ace ma dwie wersje przeła­ dowane, z których jedna przyjmuje dwa argumenty typu stri ng, zaś druga - dwa argu­ menty typu c h a r . W przedstawionym powyżej fragmencie kodu przekazujemy jej dwa literały typu stri ng, dlatego typy argumentów również są znane statycznie . Oznacza to, że kompilator może określić, które przeładowanie ma zostać wywołane, i „zaszywa" ten wybór w skompilowanym pliku - po zakończeniu procesu kompilacji jest już ustalona konkretna metoda, która ma zostać wywołana przez kod z listingu 18.1 . Wszystkie decyzje są tu podejmo­ wane na etapie kompilacji i nic nie może ich zmienić w czasie wykonania . Taka jest właśnie natura stylu statycznego. Dynamiczne elementy odraczają decyzje aż do czasu wykonania. Na przykład w językach umożliwiających dynamiczne wywoływanie metod proces wyznaczania, która dokładnie metoda ma działać, nie następuje do momentu, aż program osiągnie punkt, w którym spró­ buje ją wywołać. Oznacza to, że dynamiczny kod niekoniecznie musi wykonywać tę samą ope­ rację przy każdym uruchomieniu - określony fragment kodu może za każdym razem wywo­ ływać różne metody. Być może Czytelnik myśli teraz, że w poprzednich rozdziałach zostały już przedstawione możliwości języka C#, które pozwalają osiągnąć ten efekt. I ma rację: metody wirtualne, inter­ fejsy i delegacje zapewniają nam sposoby pisania kodu, w którym wybór właściwej metody odbywa się w czasie wykonania . Rozróżnienie między stylem dynamicznym a statycznym ma raczej charakter ciągły niż dyskretny. Metody wirtualne są bardziej dynamiczne niż metody niewirtualne, ponieważ umożliwiają wybór określonej funkcji w czasie wykonania . Interfejsy są bardziej dynamiczne niż metody wirtualne, ponieważ obiekt nie musi dziedziczyć po żadnej konkretnej klasie bazowej, aby implementować określony interfejs. Delegacje są bardziej dyna­ miczne niż interfejsy, ponieważ usuwają wymóg, aby cel był zgodny z pewnym konkretnym typem, a nawet aby w ogóle był obiektem - podczas gdy metody wirtualne i interfejsy wyma­ gają metod instancyjnych, delegacje obsługują również te, które zostały oznaczone słowem klu­ czowym stat i c . (Także w tym miejscu Czytelnik nie powinien dać się zwieść tej nieszczęśliwej dwuznaczności terminologii) . Korzystając z kolejnych wymienionych powyżej mechanizmów, można zauważyć, że z każdym przejściem na wyższy poziom dynamiczności kod wywołujący „wie" coraz mniej na temat kodu wywoływanego - coraz więcej może się zmienić w czasie wykonania programu. 692

I

Rozdział 18. Typ dynamie

Jednak w gruncie rzeczy wszystkie te mechanizmy oferują stosunkowo wąskie formy dyna­ mizmu. Wymienione powyżej możliwości wydają się dość mało znaczące w porównaniu z językiem, który wspiera styl dynamiczny całkowicie . Na przykład język JavaScript nie wymaga nawet, aby kod wywołujący znał dokładną liczbę argumentów, które spodziewa się otrzymać wywoływana metoda 1 . Z kolei w języku Ruby obiekt może dynamicznie „decydo­ wać", czy w ogóle ma zamiar implementować określoną metodę, co oznacza, że jest w stanie „postanowić" w czasie wykonania, że będzie implementował metody, o których włączeniu jego twórca nie pomyślał, pisząc oryginalny kod!

Styl dynamiczny i automatyzacja COM Pakiet Microsoft Office jest programowalny za pośrednictwem systemu określanego mianem automatyzacji COM (ang. COM automation), który jest w stanie dostosowywać się do zmiennej liczby argumentów. Office korzysta z tego mechanizmu z całkiem dobrym skutkiem. Oferuje on metody, które są bardzo elastyczne, ponieważ przyjmują zaskakującą liczbę argumentów, dzięki czemu można kontrolować każdy wyobrażalny aspekt operacji. API pakietu Office powstało z myślą o współdziałaniu z językiem Visual Basic for Applications (VBA), w którym zastosowano styl dynamiczny, dzięki czemu swobodnie można w nim pomijać argumenty, którymi nie jest się zainteresowanym. Mechanizm dynamicznego wywoływania metod zapew­ niany przez ten język jest w stanie uzupełnić brakujące argumenty sensownymi wartościami standardowymi. Powoduje to jednak pojawienie się pewnego problemu w przypadku używa­ nia bardziej statycznych języków. Język C# 3.0 wymaga, aby na etapie kompilacji znane były liczba i typy argumentów (jest tak nawet w przypadku wywołań delegacyjnych, czyli najbar­ dziej dynamicznej formy wywoływania metod dostępnej w tym języku) . Oznacza to, że nie można sobie pozwolić na opuszczenie elementów, o które się nie dba - jesteśmy zmuszeni do zapewnienia jakiejś wartości dla każdego z argumentów. Mimo to twórcy programu Microsoft Word postanowili umożliwić nam pisanie kodu przy­ pominającego ten, który został przedstawiony na listingu 18.2.

Listing 1 8.2 . Automatyzacja programu Word zgodna z intencjami firmy Microsoft var doc = wordApp . Documents . Open ( "Word Fi l e . docx " , ReadOn l y : t rue) ;

W języku C# 3.0 bylibyśmy zmuszeni do napisania zdecydowanie mniej atrakcyjnego kodu, który został zaprezentowany na listingu 18.3.

Listing 1 8 . 3 . Automatyzacja programu Word przed pojawieniem się języka C# 4.0 obj ect fi l eName = @ "Word Fi l e . docx " ; obj ect mi ss i ng = Sys tem . Refl ect i on . M i s s i ng . Val ue ; obj ect readOn l y = t rue ; var doc = wordApp . Documents . Open (ref fi l eName , ref mi s s i ng , ref readOn l y , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref m i s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref m i s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng) ; 1 Właśnie z tego powodu język C# obsługuje listy argumentów o zmiennej długości, lecz w gruncie rzeczy tylko

udaje, że to robi. Metody tego rodzaju mają w rzeczywistości z góry ustaloną liczbę argumentów, z których ostatni jest tablicą. Istnieje tylko jedna metoda o zmiennej długości listy argumentów, Consol e . Wri teli ne, a kom­ pilator jest w stanie statycznie określić, kiedy jest ona używana.

Styl statyczny kontra styl dynamiczny

I

693

Język C# 3.0 wymagał nie tylko tego, abyśmy zapewniali wartość odpowiadającą każdemu argumentowi (wykorzystując do tego specjalną wartość „ten argument został celowo pominięty" w celu zaznaczenia, że świadomie nie podaliśmy żadnej określonej wartości), lecz również tego, abyśmy precyzyjnie trzymali się reguł systemu typów. W przypadku programu Word wybrano najbardziej ogólną reprezentację z możliwych, aby zapewnić maksymalną elastycz­ ność, dlatego przed każdym argumentem widocznym w powyższym przykładzie znajduje się słowo kluczowe ref - nie zamyka to możliwości powrotnego przekazywania danych przez każdy z tych argumentów. Nie ma tu znaczenia, że skutkiem jest niespotykana złożoność sygnatur metod, ponieważ z założenia mamy korzystać z języka, którego mechanizm dyna­ micznego wywoływania metod automatycznie przeprowadzi wszystkie konwersje w czasie wykonania. Jeśli jednak używa się języka pozbawionego tego typu mechanizmu, takiego jak C# 3.0, robi się trochę nieciekawie. W gruncie rzeczy działanie automatyzacji COM polega na tym, że obiekt docelowy ostatecznie odpowiada za wartości standardowe, dostosowywanie i pozostałe kwestie. Prawdziwy pro­ blem leży w tym, że język C# 3.0 nie zapewnia żadnej składni, za pomocą której dałoby się z tego skorzystać - gdy chcemy użyć obiektu COM, musimy skorzystać z usług dynamicznego wywoływania metod oferowanych przez mechanizm odzwierciedlania, który został opisany w rozdziale 17. Niestety, sposób przeprowadzania tej operacji z poziomu języka C# 3.0 wygląda jeszcze gorzej niż kod przedstawiony na listingu 18.3. Na szczęście w języku C# 4.0 wprowadzono nowe możliwości dynamiczne, dzięki którym da się pisać kod przypominający listing 18.2, a więc korzystać z funkcji programu Word w taki spo­ sób, jak zaplanowali to twórcy aplikacji .

Typ dynam ie W języku C# 4.0 pojawił się nowy typ o nazwie dynami c . Pod pewnymi względami przypomina on każdy inny typ taki jak i nt, stri ng czy F i l eStream: można stosować go w deklaracjach zmien­ nych lub parametrów funkcji oraz wartości zwracanych, jak zostało to pokazane na listingu 18.4. (Definicja przedstawionej tu metody wygląda nieco dziwnie. Jest to metoda statyczna w tym sensie, że nie jest związana z żadnym określonym obiektem. Jest też jednak dynamiczna w takim rozumieniu, że korzysta z typu dyn ami c w przypadku parametrów i wartości zwracanej) .

Listing 1 8.4. Używanie typu dynamie s t at i c dynami e AddAnyth i ng (dynami c a , dynami e b) { dynami e res u l t = a + b ; Consol e . Wri teli ne (res u l t) ; return res u l t ;

Choć można korzystać ze słowa kluczowego dyn ami c dokładnie w taki sam sposób, jak ma to miejsce w przypadku nazwy każdego innego typu, ma ono nieco nietypowy charakter, ponie­ waż używając go, stwierdzamy tak naprawdę, że nie mamy najmniejszego pojęcia, z jakim typem mamy do czynienia . Oznacza to, że w niektórych sytuacjach nie możemy go zastosować nie da się na przykład utworzyć klasy dziedziczącej po typie dyn ami c ani nie uda się skompi­ lować wyrażenia typeof ( dynam i c ) . Jednak z wyjątkiem miejsc, w których zastosowanie typu dynami c zwyczajnie nie ma sensu, da się go używać dokładnie tak jak każdego innego typu.

694

I

Rozdział 18. Typ dynamie

Aby przekonać się, jak w praktyce działa ten mechanizm, spróbujmy przekazać kilka różnych danych metodzie AddAnyt h i ng przedstawionej na listingu 18.4. Zrobimy to w kodzie zaprezen­ towanym na listingu 18.5.

Listing 1 8.5. Przekazywanie danych różnych typów Conso l e . Wri tel i ne (AddAnyth i ng ( "Wi taj " , "świ ec i e " ) . GetType () . N ame) ; Consol e . Wri teli ne (AddAnyth i ng (3 1 , 1 1 ) . GetType () . Name) ; Consol e . Wri teli ne (AddAnyth i ng ( " 3 1 " , 1 1 ) . GetType () . Name) ; Consol e . Wri teli ne (AddAnyth i ng (3 1 , 1 1 . 5) . GetType () . Name) ;

Metoda AddAnyt h i n g wyświetla na ekranie obliczoną przez siebie wartość, a w powyższym kodzie dodatkowo wyświetlany jest jeszcze każdorazowo typ zwracanej przez nią wartości . Wynik wykonania programu będzie wyglądał następująco: Wi taj świ eci e Stri ng 42 I nt32 3111 Stri ng 42 . 5 Doubl e

Operator + zastosowany w metodzie AddAnyt h i ng zachowuje się inaczej (można stwierdzić: dynamicznie) w zależności od typu danych, które zostały do niej przekazane. Gdy w roli argu­ mentów funkcji występują dwa łańcuchy znaków, są one łączone, a wynikiem operacji jest łań­ cuch znakowy. Gdy argumentami są wartości całkowite, liczby te są dodawane i zwracana jest wartość całkowita. Zastosowanie tekstu i liczby powoduje przekonwertowanie liczby do postaci łańcucha znaków, a następnie dołączenie jej do pierwszego łańcucha. Wreszcie podanie wartości całkowitej i liczby o podwójnej precyzji skutkuje przekonwertowaniem tej pierwszej na wartość doub l e i dodanie jej do drugiej z liczb. Gdybyśmy nie zastosowali tu typu dyn ami c, każda z tych operacji wymagałaby wygenerowa­ nia przez kompilator języka C# całkiem innego kodu. Jeśli korzystamy z operatora + w sytuacji, gdy kompilator wie, że obydwie dodawane wartości są łańcuchami znakowymi, generuje on kod wywołujący metodę Stri ng . Con c a t . Gdy wie, że obydwie wartości są liczbami całkowi­ tymi, zamiast tego generuje kod przeprowadzający operację dodawania arytmetycznego. Gdy wartościami są liczby całkowita i podwójnej precyzji, generuje kod, który konwertuje wartość całkowitą do postaci wartości podwójnej precyzji, oraz kod wykonujący dodawanie arytme­ tyczne. We wszystkich tych przypadkach kompilator korzysta z posiadanej statycznej infor­ macji na temat typów, aby określić, jaki kod powinien wygenerować, by należycie odwzorować wyrażenie a + b . Rzecz jasna, w przypadku kodu przedstawionego na listingu 18.4 kompilator języka C# zrobił coś zupełnie innego. Istnieje tam tylko jedna metoda, co oznacza, że musiał on wygenerować pojedynczy fragment kodu, który w jakiś sposób jest w stanie przeprowadzić każdą z opera­ cji odpowiadających tym różniącym się od siebie znaczeniom operatora + . Kompilator robi to, generując kod, który buduje szczególnego rodzaju obiekt reprezentujący operację dodawania, a następnie obiekt ten w czasie wykonania stosuje podobne zasady do tych, które zostałyby użyte w czasie kompilacji przez kompilator, gdyby znał on typy danych biorących udział w ope­ racji. (To powoduje, że typ dyn ami c bardzo różni się od typu var, co opisuje tekst zamieszony w poniższej ramce) .

Typ dynamie

I

695

Typ dynam ie kontra typ var Na pierwszy rzut oka różnica pomiędzy typami dynami c i var może nie być całkiem oczywista. W przy­ padku obydwu nie trzeba w sposób jawny informować kompilatora, z jakiego typu danych się korzy­ sta - to kompilator ostatecznie sprawia, że dzieje się to, co powinno się stać. Zastosowanie danych typu dynami c lub var w działaniu przeprowadzanym za pomocą operatora + odniesie więc ten sam skutek, jaki odniosłoby użycie zmiennych o jawnie określonym typie. Dlaczego zatem potrzebujemy obu tych typów? Różnica jest tu związana z momentem wykonania pewnych czynności: w przypadku typu var odbywa się to znacznie wcześniej . Kompilator języka C# chce mieć możliwość określenia, jakiego typu dane przechowuje zmienna var, już na etapie kompilacji. W przypadku typu dynami c odpowiednie wyzna­ czenie typów odbywa się w czasie wykonania programu. Oznacza to, że pozwala on na rzeczy, których nie da się zrobić ze zmienną var. Jak zostało to pokazane na przykładzie kodu z listingu 18.4, typu dynami c można używać w deklaracji funkcji w stosunku do jej parametrów, a także wartości zwracanej . Byłoby to jednak nielegalne w przypadku typu var: stati c var Wi l l NotCompi l e (var a, var b) // Błąd { return a + b ;

Kompilator nie ma w tej sytuacji wystarczających informacji, aby mógł na etapie kompilacji okre­ ślić, z jakimi typami ma do czynienia w przypadku argumentów metody i zwracanej przez nią wartości. Nie ma to jednak znaczenia, gdy zostanie tu zastosowany typ dynami c - kompilator nie musi wówczas znać typu używanych danych w czasie kompilacji, ponieważ będzie w stanie wyge­ nerować kod, który określi ten typ w czasie wykonania programu. Oto kolejna rzecz, którą można zrobić z typem dynami c, a której nie da się osiągnąć za pomocą typu var: dynami e di fferentTypes = " Text" ; di fferentTypes = 42 ; di fferentTypes = new obj ect ( ) ; Typ wartości przechowywanej w zmiennej di fferentTypes zmienia się w każdym kolejnym wierszu kodu. Nie byłoby to możliwe, gdyby został tu zastosowany typ var - typ takiej zmiennej zostaje określony przez wyrażenie, za pomocą którego jest ona inicjalizowana. W tym przypadku byłaby ona łańcuchem znakowym, co oznacza, że próba kompilacji drugiego wiersza spowodowałaby zgło­ szenie błędu. Typy dynami c i var wręcz doskonale przedstawiają różnicę pomiędzy stylem dynamicznym a sta­ tycznym: typ zmiennej dynami c (a co za tym idzie również sposób przeprowadzania wszelkich ope­ racji, w których jest ona wykorzystywana) jest określany w czasie wykonania, podczas gdy typ zmiennej var ma charakter statyczny - jest określany już na etapie kompilacji i nie może podlegać żadnym zmianom w czasie późniejszym.

Sposób działania jest tu więc zgodny z tym, do czego zdążyliśmy się przyzwyczaić, używając języka C#. Operator + nadal ma takie samo znaczenie i w dalszym ciągu przeprowadza ope­ racje, których spodziewalibyśmy się po nim w normalnej sytuacji. Różnica uwidacznia się jedy­ nie w czasie wykonania programu i polega na tym, że odpowiednie decyzje są podejmowane dynamicznie . Oczywiście operator ten to niejedyna możliwość języka, która jest w stanie działać w sposób dynamiczny. Jak można się spodziewać, w przypadku typów liczbowych podobnie będą się zachowywać wszystkie operatory matematyczne. W gruncie rzeczy większość konstrukcji języka, z których można korzystać w zwykłych wyrażeniach C#, będzie tu spełniała oczekiwane zadanie. Jednak nie wszystkie operacje mają sens w każdej sytuacji. Gdy spróbu­ jemy na przykład dodać obiekt COM do liczby, zostanie zgłoszony wyjątek. (Dokładnie rzecz biorąc, będzie to wyjątek Runt i me B i n derExcept i on z komunikatem informującym, że operatora 696

I

Rozdział 18. Typ dynamie

+ nie da się zastosować do wybranej kombinacji typów) . Obiekt COM, który reprezentuje arkusz kalkulacyjny programu Excel, dość znacznie różni się od obiektu .NET. Prowadzi to do pytania: jakich rodzajów obiektów możemy używać, korzystając z typu dyn ami c?

Typy obiektów i słowo dynam ie Nie wszystkie obiekty zachowują się w ten sam sposób, gdy korzysta się z nich przy użyciu słowa kluczowego dynami c. W języku C# rozróżnia się trzy rodzaje obiektów, gdy chodzi o zasto­ sowania dynamiczne: obiekty COM, obiekty, które dostosowują swoje dynamiczne działanie, oraz zwykłe obiekty .NET. Zobaczymy kilka przykładów obiektów należących do drugiej z tych kategorii, zaczniemy jednak od przyjrzenia się najważniejszemu przypadkowi zastosowania dynamiczności: współpracy z obiektami COM.

Obiekty COM Obiekty COM, takie jak te, które są oferowane przez programy Microsoft Word lub Excel, podlegają specjalnemu traktowaniu przez typ dynami c. Poszukuje on obsługi mechanizmu auto­ matyzacji COM (to znaczy implementacji interfejsu COM I Di spatch ) i używa go w celu uzyska­ nia dostępu do metod i właściwości . Mechanizm automatyzacji został skonstruowany w taki sposób, aby obsługiwać odkrywanie składowych w czasie wykonania programu, i zapewnia narzędzia umożliwiające korzystanie z opcjonalnych argumentów, dostosowując typy argu­ mentów tam, gdzie jest to konieczne. Użycie słowa kluczowego dynami c powoduje odwoływa­ nie się do tych usług w przypadku wszystkich prób uzyskania dostępu do składowych. Dzia­ łanie kodu przedstawionego na listingu 18.6 opiera się na tym mechanizmie .

Listing 18.6. Automatyzacja COM i słowo kluczowe dynamie s t at i c vo i d Mai n (s tri ng O arg s ) { Type appType = Type . GetType FromProg I D ( "Word . Appl i cat i on " ) ; dynami e wordApp = Act i vato r . Createins tance (appType) ; dynami e doc = wordApp . Documents . Open ( "WordDoc . docx " , ReadOn l y : true) ; dynami e docPropert i es = doc . Bu i l t i n DocumentPropert i es ; s t r i ng authorName = docPropert i es [ " Author"] . Val ue ; doc . Cl ose (SaveChanges : fal se) ; Consol e . Wri teli ne (authorName) ;

W pierwszych dwóch wierszach zaprezentowanej tu metody tworzona jest instancja klasy COM aplikacji Word. Działanie kodu znajdującego się w wierszu, w którym widoczne jest wywołanie metody wordApp . Documents . Open, sprowadza się do wykorzystania automatyzacji COM w celu pobrania właściwości Document z obiektu aplikacji, a następnie wywołania metody Open na rzecz obiektu dokumentu. Metoda ta przyjmuje 16 argumentów, jednak typ dyn ami c korzy­ sta z mechanizmów zapewnianych przez automatyzację COM, dzięki czemu możliwe jest tu podanie jedynie dwóch argumentów widocznych w kodzie i pozostawienie programowi Word zadania uzupełnienia wszystkich innych odpowiednimi wartościami standardowymi. Choć typ dynami c przeprowadza tu wiele operacji specyficznych dla mechanizmu COM, wyko­ rzystana składnia wygląda dokładnie tak jak zwykła składnia języka C#. Jest tak, ponieważ kom­ pilator nie ma pojęcia, co dzieje się w tym fragmencie kodu - jest tak zresztą zawsze tam, gdzie w grę wchodzi zastosowanie typu dyn ami c . A zatem składnia wygląda tak samo nieza­ leżnie od tego, co dzieje się w czasie wykonania. Typ dynamie

I

697

Jeśli Czytelnik zna technologię COM, ma świadomość, że nie wszystkie obiekty COM obsłu­ gują mechanizm automatyzacji. Technologia ta zapewnia też wsparcie dla własnych interfejsów (ang. eustom interfaces), które nie obsługują semantyki dynamicznej - ich działanie uzależ­ nione jest całkowicie od informacji dostępnych w czasie kompilacji. Z racji tego, że nie istnieje żaden ogólny, pracujący w czasie wykonania mechanizm weryfikacji, jakie składowe oferuje interfejs własny, typ dynam i c nie nadaje się do obsługi tego rodzaju interfejsów COM. Interfejsy własne są natomiast dobrze dostosowane do usług współdziałania COM, które zostały opi­ sane w rozdziale 19 . Typ dyn ami c został dodany do języka C# głównie z uwagi na specyficzne problemy związane z automatyzacją, dlatego próba zastosowania go do własnych interfejsów COM stanowi klasyczny przykład użycia niewłaściwego narzędzia do wykonania zadania . Typ ten prawdopodobnie okaże się najbardziej przydatny w przypadku aplikacji Windows, które umożliwiają pewnego rodzaju pracę ze skryptami, ponieważ programy tego rodzaju zwykle korzystają z mechanizmu automatyzacji COM - zwłaszcza te, które pozwalają na korzystanie z VBA jako swojego standardowego języka do tworzenia skryptów.

Obiekty skryptów Si lverl ight

Aplikacje Silverlight mogą być uruchamiane w przeglądarkach stron WWW, co skutkuje poja­ wieniem się kolejnego ważnego scenariusza interoperacyjności: współpracy pomiędzy kodem C# a obiektami przeglądarki. Mogą to być obiekty pochodzące z modelu DOM lub ze skryptu. W każdym z tych przypadków mają one cechy, które powodują, że znacznie lepiej pasują do typu dynami c niż do normalnej składni języka C#, ponieważ obiekty te decydują, które właści­ wości są dostępne w czasie wykonania . Technologia Silverlight 3 wykorzystywała standard C# 3.0, dlatego typ dyn ami c nie był w niej dostępny. Dało się co prawda korzystać z obiektów pochodzących ze świata skryptów prze­ glądarki, lecz wymagana składnia nie była zbyt naturalna . Można było na przykład zdefi­ niować na stronie WWW funkcję JavaScript podobną do tej, która została przedstawiona na listingu 18 .7.

Listing 1 8.7. Kod JavaScript na stronie WWW funct i on s howMes s age (msg) { var msgDi v = document . getEl ementBy i d ( "me s s agePl aceho l der " ) ; msgD i v . i nnerText = msg ;

Przed pojawieniem się języka C# 4.0 można było wywołać tę funkcję na dwa różne sposoby zaprezentowane na listingu 18.8.

Listing 1 8.8. Korzystanie z funkcji JavaScript w języku C# 3 . 0 Scri ptObj ect s howMes s age = (Scri ptObj ect) Html Page . W i ndow . Get P roperty ( " s howMes sage " ) ; showMes s age . InvokeSel f ( "Wi taj , świ ec i e " ) ; li Lub„. Scri ptObj ect wi ndow = Html Page . W i ndow ; wi ndow . I nvoke ( " s howMessage " , "Wi taj , świ eci e " ) ;

Choć obydwa te sposoby są znacznie mniej przerażające niż kod C# 3.0 odpowiedzialny za automatyzację COM, trudno uznać je za szczególnie wygodne i eleganckie. Musimy tu korzy­ stać z metod pomocniczych takich jak Get Property, I nvokeSel f oraz I nvoke w celu uzyskania wła698

I

Rozdział 18. Typ dynamie

ściwości i wywołania funkcji . Jednak technologia Silverlight 4 obsługuje język C# 4.0, dzięki czemu wszystkie obiekty skryptów mogą być używane za pośrednictwem słowa kluczowego dyn ami c, tak jak zostało to pokazane na listingu 18.9.

Listing 1 8.9. Korzystanie z funkcji JavaScript w języku C# 4.0 dynami e wi ndow = H tml Page . W i ndow ; wi ndow . s howMes s age ( "Wi taj , świ ec i e " ) ;

Składnia ta jest o wiele bardziej naturalna . Jest naturalna w aż takim stopniu, że drugi wiersz przedstawionego powyżej kodu jest poprawną instrukcją zarówno w języku JavaScript, jak i w języku C#. (Jest ona co prawda idiomatycznie nietypowa - w przypadku strony WWW obiekt wi ndow jest obiektem globalnym, dlatego w normalnej sytuacji zostałby pominięty, jed­ nak z całą pewnością nic nie stoi na przeszkodzie, aby odwołać się do niego w sposób jawny. Z tego powodu gdybyśmy wkleili ten ostatni wiersz kodu do skryptu na stronie WWW, wyko­ nałby on to samo zadanie, za które odpowiada w języku C#) . Oznacza to, że zastosowanie słowa dyn ami c umożliwiło nam skorzystanie z obiektów JavaScript w kodzie C# za pomocą składni bardzo podobnej do tej, której użylibyśmy w samym kodzie JavaScript. Nie da się chyba już bardziej tego uprościć. '

. '

L---LI"'.

'

Narzędzia do tworzenia aplikacji Silverlight zapewniane przez środowisko Visual Studio nie dodają automatycznie odwołania do biblioteki wspierającej, która umaż­ liwia działanie typu dynami c. Z tego powodu gdy Czytelnik po raz pierwszy umieści zmienną dynami c w aplikacji tego rodzaju, zostanie zgłoszony błąd kompilacji. Odpo­ wiednie odwołanie do biblioteki Mi crosoft . CSharp należy samodzielnie dodać do swojego projektu. Reguła ta odnosi się jedynie do projektów Silverlight - wszystkie inne rodzaje projektów C# automatycznie uwzględniają odwołanie do tej biblioteki.

Zwykłe obiekty .NET Mimo że słowo kluczowe dynami c zostało wprowadzone do języka głównie po to, aby zapew­ niać obsługę rozwiązań związanych z interoperacyjnością, całkiem nieźle sprawdza się ono również w przypadku korzystania ze zwykłych obiektów .NET. Gdy na przykład w standar­ dowy sposób zdefiniujemy w swoim projekcie jakąś klasę, a następnie utworzymy jej instancję, będziemy jej mogli używać za pomocą zmiennej typu dynami c. W takim przypadku język C# korzysta z API odzwierciedlania platformy .NET w celu określenia, które metody należy wywo­ łać w czasie wykonania. Rozwiązanie to zostanie pokazane na przykładzie prostej klasy zdefi­ niowanej na listingu 18.10.

Listing 18.10. Prosta klasa cl ass MyType { publ i c s t r i ng Text { get ; set ; } publ i c i nt Number { get ; set ; } publ i c overr i de stri ng ToStri ng () { return Text + " , " + Number ; publ i c vo i d SetBoth (stri ng t , i nt n ) { Text = t ;

Typ dynamie

I

699

Number = n ; publ i c s t at i c MyType operator + (MyType l eft , MyType ri g h t ) { return new MyType { Text = l eft . Text + ri ght . Text , Number = l eft . Number + r i g h t . Number };

Obiektów tej klasy możemy używać, korzystając ze zmiennej typu dynami c, jak zostało to poka­ zane na listingu 18 .11 .

Listing 1 8. 1 1 . Używanie prostego obiektu za pośrednictwem zmiennej typu dynamie dynami e a = new MyType { Text = " Jeden " , Number = 123 } ; Consol e . Wri teli ne ( a . Text) ; Consol e . Wri teli ne ( a . Number) ; Consol e . Wri teli ne ( a . Probl em) ;

We wszystkich wierszach, w których występują wywołania metody Con sol e . Wri tel i ne, uży­ wana jest zmienna dynamiczna, przy czym odwołania do niej mają składnię zwykłego odwo­ łania do właściwości w języku C#. Kod dwóch pierwszych działa dokładnie tak, jak można by się tego było spodziewać, gdyby zmienna została zadeklarowana jako MyType lub var zamiast dynami c: instrukcje te po prostu wyświetlają na ekranie wartości właściwości Text i N umber. Ciekawszy jest wiersz ostatni - następuje w nim próba użycia właściwości, która nie istnieje. Gdyby zmienna została zadeklarowana jako MyType lub var, kod ten nie zostałby skompilo­ wany - kompilator zgłosiłby błąd związany z próbą odczytu właściwości, o której wiadomo, że nie należy do klasy. Jednak z racji tego, że został tu zastosowany typ dynami c, kompilator nawet nie próbuje sprawdzać tego rodzaju kwestii w czasie kompilacji. Kompiluje więc kod bez zgłaszania błędu, który pojawia się dopiero w czasie wykonania - ostatni wiersz zgłasza wtedy wyjątek Run t i meB i n derExcept i on z komunikatem informującym, że docelowy typ nie defi­ niuje składowej Probl em, której szukamy. Jest to jeden z kosztów, który musimy ponieść, aby móc korzystać z elastyczności dynamicznego działania: kompilator staje się mniej czujny. Pewne błędy programistyczne, które zostałyby wychwycone na etapie kompilacji w przypadku zastosowania stylu statycznego, nie są tu wykrywane aż do czasu wykonania . Wiąże się z tym jeszcze jeden koszt: mechanizm Intelli­ Sense opiera swoje działanie na tych samych informacjach, które są dostępne na etapie kom­ pilacji i które pozwoliłyby wykryć pomyłkę. Gdybyśmy zmienili typ zmiennej z listingu 18.11 na MyType lub var, podczas pisania kodu ujrzelibyśmy okienko podpowiedzi IntelliSense podobne do przedstawionego na rysunku 18.1 . Środowisko Visual Studio jest w stanie prezentować listę dostępnych metod, ponieważ zmienna ma typ o charakterze statycznym - zawsze będzie się ona odwoływać do obiektu typu MyType. W przypadku typu dyn am i c nie możemy już niestety liczyć na podobną pomoc. Jak widać na rysunku 18.2, środowisko Visual Studio po prostu informuje nas, że nie ma pojęcia, jakie skła­ dowe są tu dostępne . W przypadku tego prostego przykładu można oczywiście próbować dowodzić, że środowisko powinno być w stanie odnaleźć te informacje, gdyż mimo że zade­ klarowaliśmy zmienną jako dynami c, na tym etapie programu może ona być wyłącznie typu

700

I

Rozdział 18. Typ dynamie

va r

ai = ne1�

MyType

Con s ole . l�ri t e l i n e

{ TeKt = "Jeden",

(a .�

N lJ b e r

r=---� � Equals �

= 123 } ;

G etH a :s h C o d e

>!:ring MyType.Text

Rysunek 18.1 . Działanie mechanizmu IntelliSense w przypadku zmiennej typu o charakterze statycznym MyType. Jednak Visual Studio z kilku powodów nawet nie próbuje przeprowadzać tego rodzaju analiz. Po pierwsze, rozwiązanie to sprawdzałoby się tylko w stosunkowo trywialnych przy­ padkach takich jak przedstawiony tutaj, a zawiodłoby wszędzie tam, gdzie na poważnie korzy­ stalibyśmy z dynamicznej natury i pełnych możliwości typu dyn ami c . Skoro zaś tak naprawdę nie potrzebujemy dynamizmu, dlaczego po prostu nie trzymać się zmiennych o typach sta­ tycznych? Po drugie, jak przekonamy się w dalszej części tego rozdziału, typ może dostosowy­ wać swoje dynamiczne działanie, a więc nawet gdyby środowisko Visual Studio wiedziało, że zmienna typu dynami c zawsze będzie się odwoływać do obiektu typu MyType, niekoniecznie musiałoby wiedzieć, jakie składowe będą dostępne w czasie wykonania. Skutek zastosowania zmiennej typu dyn ami c w postaci niezbyt pomocnego okienka podpowiedzi wyświetlanego przez mechanizm IntelliSense został pokazany na rysunku 18.2. dyn a mi e ai =

n el�

MyType { Te xt

C on s ol e . Hri t e li n e ( a .

rj

= "J ede n" ,

N u ber

= 1. 2 3 } ;

-

,..=... � (dynamie expressi on) This operation wi l l b e rern lved at runti me.

I

Rysunek 1 8.2 . Działanie mechanizmu IntelliSense w przypadku zmiennej typu o charakterze dynamicznym Kod przedstawiony na listingu 18.11 jedynie odczytuje właściwości, jednak jak Czytelnik z pew­ nością się spodziewa, da się również przypisywać im wartości. Możemy też wywoływać metody, korzystając ze zwykłej składni . Kod z listingu 18 .12 prezentuje obydwie te możliwości i nie powinien Czytelnika zbytnio zaskoczyć.

Listing 18.12. Przypisywanie wartości właściwościom i wywoływanie metod przy użyciu typu dynamicznego dynami e a = new MyType () ; a . Number = 42 ; a . Text = " Foo " ; Consol e . Wri teli ne ( a) ; dynami e b = new MyType () ; b . SetBoth ( " Bar " , 99) ; Consol e . Wri teli ne (b) ;

W przykładzie z klasą MyType jest również przeładowywany operator + określa się w ten sposób, co powinno się zdarzyć, gdy zostanie podjęta próba dodania dwóch obiektów tego typu. Oznacza to, że możemy wziąć dwa obiekty z listingu 18 .12 i przekazać je metodzie Add '-+Anyt h i ng przedstawionej na listingu 18.4, co pokazuje listing 18.13. -

Typ dynamie

I

701

Listing 18.13. Używanie przeładowanego operatora

+

MyType c = AddAnyt h i n g ( a , b) ; Consol e . Wri teli ne (c) ;

Przypomnijmy sobie, że w kodzie z listingu 18.4 użyta została zwykła składnia języka C# umożliwiająca dodawanie dwóch danych. Kod ten powstał jeszcze przed napisaniem klasy MyType, lecz mimo to działa całkiem poprawnie, wyświetlając na ekranie następujące informacje: FooBa r , 141

Własny operator + zdefiniowany w obrębie klasy MyType łączy łańcuchy znakowe przecho­ wywane przez właściwości Text i dodaje wartości właściwości N umber, czego efekt możemy zobaczyć powyżej . Nie powinno to stanowić niespodzianki - jest to kolejny przykład zasto­ sowania podstawowej zasady, która mówi, że operacje przeprowadzane przy użyciu typu dynami c powinny przebiegać w ten sam sposób, jak miałoby to miejsce w przypadku statycznym. Kod przedstawiony na listingu 18.13 pokazuje jeszcze jedną interesującą cechę związaną z przy­ pisywaniem danych typu dynam i c . Da się oczywiście przypisać dowolną wartość zmiennej tego typu, bardziej zaskakujące jest jednak to, że można również wykonać operację przeciwną nic nie stoi na przeszkodzie, aby przypisać wyrażenie typu dyn am i c do zmiennej dowolnego innego typu. W pierwszym wierszu kodu pokazanego na listingu 18.13 następuje przypisanie wartości zwracanej przez metodę AddAnyt h i ng do zmiennej typu MyType. Jak Czytelnik pewnie pamięta, metoda ta zwraca wartość typu dyn ami c, można się więc spodziewać, że niezbędne okaże się w tym miejscu rzutowanie wyniku z powrotem na typ MyType. Tak jednak nie jest. Podobnie jak w przypadku wszystkich innych operacji dynamicznych, język C# na etapie kompilacji umożliwia nam podjęcie próby przeprowadzenia dosłownie dowolnego działania, a następnie w czasie wykonania próbuje zrobić to, czego sobie życzymy. W tym przykładzie przypisanie się powiedzie, ponieważ metoda AddAnyt h i ng dodaje dwa obiekty klasy MyType i zwraca referencję do nowego obiektu tej klasy. Z uwagi na to, że do zmiennej typu MyType zaw­ sze możemy przypisać referencję do obiektu tego typu, przypisanie się udaje. Gdyby jednak wystąpiła tu niezgodność typów, program zgłosiłby wyjątek w czasie wykonania. Stanowi to kolejny przykład tej samej podstawowej zasady. Jest on tylko nieco bardziej subtelny, ponieważ przypisanie jest zwykle w języku C# trywialną operacją, a co za tym idzie, fakt, że mogłaby ona zakończyć się niepowodzeniem w czasie wykonania, wcale nie musi od razu wydawać się oczywisty. Większość operacji da się wykonać dynamicznie, są jednak od tej reguły pewne wyjątki. Nie można wywoływać metod zadeklarowanych przy użyciu słowa kluczowego stat i c za pomocą typu dynami c . Pod pewnym względem jest to dość nieszczęśliwa sytuacja, ponieważ możliwość wybrania określonej statycznej (a więc nieinstancyjnej) metody w sposób dynamiczny - na podstawie typu obiektu, którym się dysponuje - mogłaby się czasem okazać bardzo przy­ datna. Byłoby to jednak niespójne z tym, jak standardowo działa język C#, ponieważ nie da się też wywoływać metod stat i c, korzystając ze zmiennych typu statycznego. Zawsze należy je wywoływać za pośrednictwem definiującego typu (a więc na przykład tak: Con sol e . Wri teli ne) . Słowo kluczowe dyn ami c w tej kwestii nic nie zmienia. Metody rozszerzeń również nie są dostępne za pośrednictwem zmiennych typu dynami c . Z jed­ nej strony ma to sens, ponieważ metody takie są w rzeczywistości po prostu metodami statycz­ nymi ukrytymi za wygodną składnią. Z drugiej strony jednak ta wygodna składnia została opra­ cowana w taki sposób, aby wydawało się, że w gruncie rzeczy mamy do czynienia z metodami instancyjnymi, jak zostało to przedstawione na listingu 18.14.

702 I

Rozdział 18. Typ dynamie

Listing 1 8.14. Metody rozszerze1i ze zmiennymi o typach statycznych us i ng Sys tem . Col l ect i ons . Generi c ; us i ng Sys tem . L i n ą ; cl ass Program { s t at i c vo i d Ma i n () { I Enumerabl e numbers = Enumerabl e . Range ( l , 10) ; i nt total = numbers . Sum () ;

Wywołanie metody n umbers . Sum powoduje, iż może się wydawać, że typ I Enumerab l e defi­ niuje metodę o nazwie Sum. W rzeczywistości nie ma takiej metody, dlatego kompilator wyszu­ kuje metody rozszerzeń - przeszukuje wszystkie typy należące do wszystkich przestrzeni nazw, które przywołaliśmy za pomocą dyrektyw u s i ng. (To właśnie z tego powodu na powyż­ szym listingu pokazany został cały kod programu zamiast tylko jego krótkiego fragmentu. Aby właściwie zrozumieć, co się tu dzieje, potrzebujemy pełnego kontekstu, w tym również dyrektywy us i ng Sys tern . L i nq, która sprawia, że wywołanie tej metody ma sens) . A gdy odnaj­ dzie ten typ I Enumerabl e (w przestrzeni nazw System . L i nq) , oferuje pasującą w tym miejscu metodę rozszerzenia S um . Gdy zmienimy pierwszy wiersz metody Ma i n, tak aby miał on postać przedstawioną na lis­ tingu 18 .15, pojawi się problem.

Listing 1 8.15. Zastąpienie typu IEnumerable typem dynamie dynami e numbers

=

Enumerabl e . Range ( l , 1 0) ;

Kod nadal będzie się dało skompilować, ale w czasie wykonania programu, gdy osiągnięty zostanie punkt, w którym nastąpi wywołanie metody S um, zgłosi on wyjątek Runt i me B i n d e "+ Except i on, narzekając na fakt, że obiekt docelowy nie definiuje metody o tej nazwie . Oznacza to, że w tej sytuacji język C# rezygnuje z przestrzegania standardowej zasady pole­ gającej na zapewnianiu, aby sposób działania w czasie wykonania w przypadku typu dyn ami c odpowiadał temu, co działoby się w przypadku zmiennej o typie statycznym. Powodem tego stanu rzeczy jest fakt, że kod generowany przez kompilator C# dla wywołania dynamicznego nie zawiera wystarczającej ilości informacji. Aby wyznaczyć metodę rozszerzenia, trzeba wiedzieć, które dyrektywy us i ng są obecne w kodzie. Teoretycznie możliwe byłoby poznanie tego kontekstu, jednak znacząco zwiększyłoby to ilość informacji, które kompilator języka C# musiałby ogarnąć. Za każdym razem, gdy wykonywalibyśmy jakąś operację z udziałem zmien­ nej typu dynami c, kompilator byłby zmuszony do zapewnienia, aby dostępna była lista wszyst­ kich istotnych przestrzeni nazw. Nawet to okazałoby się jednak niewystarczające - na etapie kompilacji kompilator C# jedynie szuka metod rozszerzeń w podzespołach, do których odwo­ łuje się nasz projekt, więc zapewnienie w czasie wykonania programu tej samej semantyki wyznaczania metod, z którą mamy do czynienia w przypadku stylu statycznego, wymagałoby, aby dało się uzyskać dostęp również do tych informacji na temat metod rozszerzeń. Co gorsza, odebrałoby to kompilatorowi C# możliwość optymalizacji odwołań znajdujących się w naszym projekcie. W normalnych przypadkach wykrywa on fakt, że występuje w nim odwołanie do podzespołu, do którego nie ma żadnych odwołań w kodzie, i usuwa wszelkie tego

Typ dynamie

I

703

rodzaju odwołania w czasie kompilacji2 . Gdyby jednak w programie pojawiły się jakiekolwiek dynamiczne wywołania metod, kompilator musiałby zachować odwołania do pozornie nie­ używanych podzespołów na wypadek, gdyby okazały się niezbędne w celu wyznaczenia jakichś metod rozszerzeń w czasie wykonania . Z tych powodów, choć zapewnienie takiego mechanizmu przez firmę Microsoft byłoby teo­ retycznie możliwe, wymagałby on od nas poniesienia bardzo wysokich kosztów. W dodatku realna wartość takiego rozwiązania okazałaby się prawdopodobnie dość niewielka, ponieważ nie byłoby ono nawet przydatne w przypadku najczęściej wykorzystywanych metod rozszerzeń. Z metod tych w bibliotece klas platformy .NET najczęściej korzysta mechanizm LINQ standardowym operatorem LINQ jest na przykład używana powyżej metoda S um . Stanowi ona jeden z prostszych operatorów. Większość z nich przyjmuje argumenty, z których wiele jest wyrażeniami lambda. Kompilując je, kompilator języka C# korzysta z informacji dotyczą­ cych statycznych typów w celu utworzenia odpowiednich delegacji. Istnieje na przykład przeładowana wersja operatora Sum, która przyjmuje wyrażenie lambda, umożliwiając nam w ten sposób obliczenie sumy wartości wyznaczonej na podstawie bazowych danych, zamiast jedynie sumować same te bazowe dane. W kodzie pokazanym na listingu 18.16 wykorzystane zostało to przeładowanie w celu obliczenia sumy kwadratów liczb znajdujących się na liście.

Listing 18.16. Wyrażenia lambda i typy i nt total = numbers . Sum (x => x * x) ;

Gdy zmienna n umbers ma typ statyczny (którym w naszym przypadku jest I En umerab l e< i n t>) , kod ten działa wprost doskonale. Jednak gdy zmienna n umbers jest typu dynami c, kompilator po prostu nie ma wystarczających informacji, aby wiedzieć, jaki kod należy wygenerować dla tego wyrażenia lambda . Zakładając, że kompilator podjąłby odpowiednio heroiczne wysiłki, mógłby on zdobyć dostatecznie dużo informacji, aby poradzić sobie z wygenerowaniem całego niezbędnego kodu w czasie wykonania . Co jednak udałoby się w ten sposób zyskać? Tech­ nologia LINQ przeznaczona jest do używania w świecie zmiennych o typach statycznych, zaś typ dynam i c został wymyślony głównie dla potrzeb interoperacyjności . Z tego też powodu firma Microsoft postanowiła nie obsługiwać tego rodzaju rozwiązań z typem dynami c. Oznacza to, że korzystając z mechanizmu LINQ, Czytelnik powinien się trzymać typów statycznych.

Obiekty pochodzące z innych języków dynam icznych Działanie słowa kluczowego dynami c opiera się na ukrytym mechanizmie, który nie jest zarezer­ wowany tylko dla języka C#. Jest on zależny od zestawu bibliotek i konwencji znanych jako DLR (ang. Dynamie Language Runtime - dynamiczne środowisko uruchomieniowe) . Biblioteki te są wbudowane w .NET Framework, dlatego odpowiednie usługi są dostępne wszędzie tam, gdzie dostępna jest platforma .NET 4 lub jej późniejsza wersja. Umożliwia to używanie z poziomu języka C# obiektów pochodzących z innych języków programowania. We wcześniejszej części tego rozdziału wspomnieliśmy, że w języku programowania Ruby da się pisać kod, który decyduje w czasie wykonania, jakie metody mają być oferowane przez określony obiekt. Jeśli korzystamy z implementacji języka Ruby, która używa środowiska DLR (takiej jak IronRuby), możemy używać tego rodzaju obiektów w kodzie C#. W serwisie 2 Tak przy okazji: optymalizacja ta nie występuje w przypadku projektów Silverlight. Sposób, w jaki mechanizm

Silverlight korzysta z bibliotek kontrolek w XAML, powoduje, że środowisko Visual Studio musi być konser­ watywne w kwestiach związanych z odwołaniami projektu.

704

I

Rozdział 18. Typ dynamie

internetowym poświęconym rozwiązaniu DLR udostępnione zostały na zasadach otwar­ tego kodu źródłowego implementacje dwóch języków, które wykorzystują DLR. Chodzi o języki IronPython oraz IronRuby (więcej informacji na ten temat można znaleźć na stronie

http://dlr.codeplex.com/) .

Obiekty ExpandoObject Biblioteka klas platformy .NET zawiera klasę o nazwie ExpandoObj ect, która została opraco­ wana tak, aby umożliwiać jej używanie za pośrednictwem zmiennych typu dyn ami c. Pozwala ona dostosowywać sposób swojego dynamicznego działania. (W tym celu klasa ta implemen­ tuje specjalny interfejs o nazwie ! Dynami cMetaObj ect Prov i d er. Jest on definiowany przez środo­ wisko DLR i jest również odpowiedzialny za to, że obiekty opracowane w innych językach są w stanie udostępniać kodowi C# swoje dynamiczne, specyficzne dla danego języka działania) . Jeśli znamy język JavaScript, idea stojąca za klasą ExpandoObj ect nie powinna wydawać się nam obca: chodzi o to, że można przypisywać właściwościom wartości bez konieczności wcześniej­ szego ich deklarowania, tak jak zostało to zaprezentowane na listingu 18.17.

Listing 18.17. Przypisywanie wartości dynamicznym właściwościom dynami e dx = new ExpandoObj ect () ; dx . MyProperty = true ; dx . AnotherProperty = 42 ;

Gdy przypisujemy wartość właściwości, której obiekt ExpandoObj ect wcześniej nie miał, po prostu powiększa się on o tę nową właściwość, a my będziemy mogli w późniejszym czasie pobrać odpowiednią wartość. Działanie to koncepcyjnie odpowiada zastosowaniu kolekcji D i et i onary, a jedyna różnica polega tu na tym, że w słowniku tego rodzaju pobieramy i ustawiamy wartości, korzystając ze składni akcesora właściwości C# zamiast z indeksem . Możemy nawet iterować po wartościach należących do obiektu ExpandoObj ect dokładnie tak, jak robilibyśmy to w przypadku słownika. Zostało to pokazane na listingu 18.18.

Listing 18.18. Iterowanie dynamicznych właściwości foreach ( KeyVal uePai r prop i n dx) { Consol e . Wri teli ne (prop . Key + " : " + prop . Val ue) ;

Gdy piszemy kod C#, który musi współpracować z innym językiem używającym mechanizmu DLR, zastosowanie tej klasy może się okazać wygodne. W językach w pełni wykorzystujących styl dynamiczny często używa się tego rodzaju dynamicznie wypełnianych obiektów w miej­ scach, w których w przypadku bardziej statycznie zorientowanych języków zwykle stosowało­ by się słowniki, dlatego klasa ExpandoObj ect może zapewnić bardzo wygodny sposób wypełnie­ nia tej luki . Klasa ta implementuje interfejs I Di c t i onary, dlatego jest w stanie porozumiewać się przy użyciu obydwu języków. Przykład zaprezentowany na listingu 18 .19 pokazuje, jak można dodawać właściwości do obiektu klasy ExpandoObj ect za pośrednictwem jej API słownika, a następnie uzyskiwać do nich dostęp jako do właściwości dynamicznych.

Listing 18.19. Obiekt klasy ExpandoObject jako słownik i obiekt dynamiczny ExpandoObj ect xo = new ExpandoObj ect () ; I D i ct i onary d i ct i onary = xo ;

Typ dynamie

I

705

d i cti onary [ " Foo "] = " Bar " ; dynami e dyn = xo ; Consol e . Wri teli ne (dyn . Foo) ;

Ta sztuczka z implementowaniem własnego dynamicznego sposobu działania nie jest czymś zarezerwowanym wyłącznie dla klasy Expan doObj ect swobodnie można tworzyć własne obiekty, które będą wykonywały tego rodzaju operacje. -

Własne obiekty dynamiczne DLR definiuje interfejs o nazwie ! Dynami cMetaObj ect Provi der, a implementujące go obiekty defi­ niują sposób swojego działania w sytuacjach, gdy są używane dynamicznie . Mechanizm ten został opracowany tak, aby zapewniać wysoką wydajność przy zachowaniu maksymalnej elastyczności. Jest to oczywiście wspaniałe dla wszystkich, którzy będą używać naszego typu, wymaga jednak dużego nakładu pracy przy implementacji. Opis sposobu implementacji tego interfejsu wymagałby dość szczegółowego wprowadzenia w tajniki środowiska DLR i wykracza poza zakres tematów, którymi zajmujemy się w niniejszej książce. Na szczęście jednak istnieją prostsze metody. W przestrzeni nazw System . Dynami c zdefiniowana została klasa o nazwie Dyn ami cObj ect. Imple­ mentuje ona interfejs ! Dynami cMetaObj ect Prov i der, dzięki czemu musimy jedynie przesłonić metody odpowiadające za operacje, które ma obsługiwać nasz obiekt dynamiczny. Jeśli chcemy zapew­ nić obsługę dynamicznych właściwości, lecz nie zależy nam na jakichkolwiek innych możli­ wościach dynamicznych, powinniśmy tylko przesłonić jedną metodę o nazwie TryGetMember, tak jak zostało to pokazane na listingu 18.20.

Listing 1 8.20. Własny obiekt dynamiczny us i ng Sys tem ; us i ng Sys tem . Dynam i e ; publ i c cl ass Cus tomDynami c : Dynami cObj ect { pri vate s t at i c DateT i me Fi rstS i ght i ng = new DateT i me ( 1947 , 3 , 13) ; publ i c overri de bool TryGetMember (GetMemberB i nder b i nder, out obj ect res u l t) { var compare = b i nder . I gnoreCase ? Stri ngComparer . Invari antCul turei gnoreCase Stri ngComparer . Invari antCu l ture ; i f (compare . Compare (bi nder. Name , " Bri gadoon " ) == O) { li Wioska Brigadoon słynie z tego, że pojawia się tylko raz na sto lat. DateTi me today = DateT i me . Now . Date ; i f (today . DayOfYear == Fi rs tS i gh t i ng . DayOfYear) { li Odpowiedni dzie1i. Co z rokiem ? i n t yearsS i nceFi rstS i gh t i ng = today . Year - Fi rstS i gh t i ng . Year ; i f (years S i nce F i rstS i gh t i ng % 100 == O) { res u l t = "Wi tamy w Bri gadoon . Zachowaj os trożność na drodze . " ; return true ;

return bas e . TryGetMember (bi nde r , out res u l t) ;

706

I

Rozdział 18. Typ dynamie

W tym przypadku definiowana jest tylko jedna właściwość o nazwie Bri gadoon 3 . Metoda TryGet "+Member będzie wywoływana za każdym razem, gdy jakiś kod podejmie próbę odczytu wła­ ściwości obiektu. Parametr typu GetMemberB i nder przekazuje nazwę właściwości, której poszukuje kod wywołujący metodę, dlatego jest ona porównywana z nazwą jedynej zapewnianej tu właściwości . Parametr b i n der informuje również o tym, czy kodowi wywołującemu zależy na porównaniu, w którym uwzględniana będzie wielkość znaków - w języku C# właściwość I gnoreCase będzie miała wartość fal s e, jednak w przypadku niektórych języków (takich jak YB.NET) preferowane są porównania z uwzględnieniem wielkości liter. Jeśli nazwa będzie pasować, będziemy mogli zdecydować w czasie wykonania, czy właściwość ma być obecna, czy też nie; właściwość ta jest dostępna przez jeden dzień tylko raz na sto lat. Przykład ten nie jest może bardzo użyteczny i praktyczny, ale obrazuje fakt, że na podstawie dowolnie wybra­ nych reguł obiekty mogą decydować, jakie właściwości oferują . ••



.

·

._,..�;

.

L---...iJ"' '

Jeśli Czytelnik zastanawia się, co otrzymujemy w zamian za dodatkowe skomplikowanie kodu związane z użyciem interfejsu I Dynami cMetaObj ectProvi der, spieszymy wyjaśnić, że umożliwia on zastosowanie technik buforowania i generowania kodu w czasie wykonania w celu zapewnienia wysoce wydajnego działania dynamicznego. Rozwiązanie to jest znacznie bardziej złożone niż prosty model oferowany przez klasę Dynami cObj ect, ma jednak znaczący wpływ na wydajność języków, w których model dynamiczny stanowi normę.

Typ dynam ie w zastosowan iach n iezwiązanych z i nteroperacyjnością? Głównym powodem wprowadzenia typu dyn ami c była chęć umożliwienia używania funkcji pakietu Office bez konieczności pisania skomplikowanego kodu. Ma on też zastosowanie w innych rozwiązaniach związanych z interoperacyjnością takich jak współpraca ze skryptami przeglądarek w projektach Silverlight oraz używanie języków dynamicznych. Jednak prawdo­ podobnie nigdy nie wykorzystalibyśmy go w scenariuszu związanym z zastosowaniem moż­ liwości samego języka C#. Styl dynamiczny staje się w ostatnich latach coraz bardziej modny niektóre popularne biblioteki języka JavaScript przeznaczone do używania w kodzie WWW po stronie klienta, podobnie jak niektóre platformy WWW, w bardzo sprytny sposób korzystają z idiomów dynamicznych. Niektórzy twórcy oprogramowania posuwają się wręcz do twier­ dzenia, że styl dynamiczny jest ze swej natury lepszy niż styl statyczny. Skoro wiatr wieje właśnie z tej strony, to czy programiści używający języka C# również powinni dostosować się do ogólnego trendu? Cóż, można śmiało stwierdzić, że osobom zainteresowanym korzystaniem z języków dyna­ micznych typ dyn ami c zapewnił w C# pewne możliwości języków tego rodzaju. Najważniejsze w tym zdaniu jest jednak słowo pewne. W języku C# 4 .0 wprowadzono typ dyn am i c, aby usprawnić określone rozwiązania dotyczące interoperacyjności, nie zaś aby zapewnić obsługę całkowicie nowych idiomów programowania. Nie należy zatem myśleć o tym typie w kate­ goriach „dynamicznych rozszerzeń języka C#" .

3

Zgodnie z popularną legendą Brigadoon to szkocka wioska, która pojawia się na świecie raz na sto lat tylko na jeden dzień.

Typ dynamie w zastosowaniach niezwiązanych z interoperacyjnością?

I

707

Gdy spróbujemy korzystać z C# w taki sposób, jakby był on pełnoprawnym językiem dyna­ micznym, zrezygnujemy z jego podstawowych zalet, a w konsekwencji nieuchronnie wpędzimy się w kłopoty. Przedstawiliśmy już wcześniej przykład zastosowania technologii LINQ, w któ­ rym nienajlepiej sprawdzała się ona w połączeniu z typem dyn ami c . Błąd ten był symptomem problemu o bardziej ogólnym charakterze. Podstawową przyczyną jego powstania był fakt, że delegacje nie są aż tak elastycznym mechanizmem, jak można by się tego spodziewać, gdy rozważa się kwestie działań dynamicznych. Przyjrzyjmy się metodzie przedstawionej na lis­ tingu 18 .21 .

Listing 18.2 1 . Prosty filtr s t at i c bool Tes t ( i nt x) { return x < 100 ;

Metody tej możemy użyć w połączeniu z operatorem Where języka LINQ, jak zostało to zapre­ zentowane na listingu 18.22.

Listing 1 8.22 . Filtrowanie za pomocq LINQ var nums = Enumerabl e . Range ( l , 200) ; var fi l teredNumbers = nums . Where (Tes t) ;

A co by było, gdybyśmy chcieli uczynić z niej metodę o bardziej ogólnym zastosowaniu? Moglibyśmy zmodyfikować kod Test w taki sposób, aby zamiast działać na danych i nt, metoda ta współpracowała z jakimkolwiek wbudowanym typem liczbowym lub nawet dowolnym typem oferującym taką wersję operatora Tes t (x) ) ;

Jest to trochę dziwne, ponieważ zapis ten powinien mieć dokładnie takie samo znaczenie jak odpowiadający mu wiersz kodu przedstawionego na listingu 18 .22. Musimy tu dodać nieco więcej kodu tylko po to, aby spełnić wymagania systemu typów języka C#, a zwykle właśnie od konieczności wykonywania tego rodzaju działań ma nas uwalniać styl dynamiczny. Problem wynika po części z tego, że próbujemy tu skorzystać z mechanizmu LINQ będącego całkowi­ cie statycznie zorientowanym API. Okazuje się jednak, że mamy również do czynienia z pro-

708

I

Rozdział 18. Typ dynamie

blemem tkwiącym jeszcze głębiej, który można zilustrować, próbując napisać własną, bardziej dostosowaną do możliwości typu dynami c wersję metody Where. Metoda Dynami cWh ere pokazana na listingu 18.25 przyjmie w charakterze argumentu test dosłownie wszystko. Nie będzie ona sprawiać kłopotów, dopóki argument test będzie się dało wywołać jako metodę zwracającą wartość typu bool (lub coś, co będzie można niejawnie przekonwertować na ten typ) .

Listing 1 8.25. Implementacja metody Where dostosowana do wymaga1i typu dynamicznego s t at i c I Enumerabl e Dynami cWhere ( I Enumerabl e i nput , dynami e tes t) { foreach (T i tem i n i nput) { i f (tes t ( i tem) ) { yi el d return i tem ;

Kod ten uda się prawidłowo skompilować, a jego działanie będzie zgodne z oczekiwaniami, jeśli tylko będziemy w stanie go wywołać. Niestety jednak pomoże to niewiele. W kodzie przed­ stawionym na listingu 18 .26 podejmowana jest próba skorzystania z tej metody - kompilacja tego kodu już się nie powiedzie.

Listing 1 8.26. Nieudana próba wywołania metody DynamicWhere var fi l teredNumbers

=

Dynami cWhere (nums , Tes t) ; // Błqd kompilatora

Komunikat kompilatora C# jest tu następujący: Argument 2 : cannot convert from ' method group ' to ' dynami e ' ,

Problem polega na tym, że pozostawiliśmy mu zbyt wiele swobody. Kod przedstawiony na listingu 18.25 będzie współdziałał z szerokim spektrum typów delegacji. Poradzi sobie z takimi typami jak Pred i cate, Pred i cate, Pred i cate< i nt>, Fun c, Fun c oraz Fun c. Można by tu też skorzystać ze zdefiniowanego samodzielnie typu delegacji, który byłby odpowiednikiem dowolnego z wymienionych powyżej. Kompilator języka C# wie jedynie, że metoda Dyn ami cWh ere oczekuje argumentu typu dyn ami c, dlatego uważa, że metoda ta mogłaby przyjąć argument dosłownie każdego typu. Wszystko, co musiałby zrobić, ogranicza się do wybrania takiego, który pasuje do sygnatury metody Test - dowolny typ delegacji z jednym parametrem i wartością zwracaną typu boo l sprawdziłby się tu doskonale. Kompilator nie dysponuje jednak żadną regułą pozwalającą mu stwierdzić, który konkretnie typ delegacji powinien zostać standardowo zastosowany w tym miejscu. W przypadku kodu pokazanego na listingu 18 .22 kompilator wiedział, co robić, ponieważ metoda Where spodziewała się szczególnego typu delegacji: Fun c. Z racji tego, że istniała tylko jedna opcja spełniająca to kryterium, kompilator języka C# był w stanie utworzyć delegację właściwego rodzaju. Jednak tutaj, gdy wybór jest zbyt wielki, musimy go odpowiednio zawęzić, aby kompilator znów wiedział, co należy zrobić. Kod przedstawiony na listingu 18.27 stanowi przykład tego, jak można to osiągnąć, choć dałoby się również przeprowadzić rzuto­ wanie na dowolny z wymienionych wcześniej typów delegacji.

4

Argument 2.: nie da się przeprowadzić konwersji z typu ' method group ' na ' dynami c ' .

Typ dynamie w zastosowaniach niezwiązanych z interoperacyjnością?

I

709

Listing 1 8.27. Podpowiedź dla metody DynamicWhere var fi l teredNumbers

=

Dynami cWhere (nums , ( Predi cate) Tes t) ;

Ponownie okazało się, że musieliśmy wykonać dodatkową pracę jedynie po to, aby spełnić wymagania systemu typów języka C#, co stoi w jawnej sprzeczności z tym, czego zwykle spodziewalibyśmy się w przypadku dynamicznego idiomu - typy powinny przecież mieć tu mniejsze znaczenie . W tego rodzaju kłopoty możemy się wpędzić, gdy będziemy próbowali traktować C# jak dyna­ miczny język programowania . Podstawową kwestią, którą należy zrozumieć, jest to, że typ dynami c został opracowany w celu rozwiązywania specyficznych problemów związanych z inter­ operacyjnością. Zadanie to wykonuje on bardzo dobrze, jednak jako całość język C# nie jest tak naprawdę narzędziem do programowania dynamicznego. Z tego powodu wszelkie próby intensywnego korzystania z tego stylu w kodzie C# z pewnością nie są najlepszym pomysłem.

Podsu mowan ie Wprowadzone w C# 4.0 słowo kluczowe dynami c w dużej mierze ułatwia korzystanie z obiek­ tów, które zostały opracowane w taki sposób, aby umożliwiać używanie ich z poziomu dyna­ micznych języków programowania . Znacznie bardziej naturalne niż w poprzednich wersjach języka jest w C# 4.0 zwłaszcza posługiwanie się API automatyzacji COM takimi jak te, które oferuje pakiet Microsoft Office. Nowy standard bardzo upraszcza również współpracę z obiek­ tami skryptów przeglądarek w projektach Silverlight.

710

I

Rozdział 18. Typ dynamie

ROZDZIAŁ 19.

Współdziałanie z COM i Win32

Programiści uwielbiają mieć czyste konto. Myśl o odrzuceniu całego kodu, który napisaliśmy wcześniej, i rozpoczęciu wszystkiego od nowa wydaje się bardzo kusząca, jednak dla więk­ szości firm takie rozwiązanie zazwyczaj nie wchodzi w grę. Wiele organizacji zajmujących się wytwarzaniem oprogramowania poczyniło znaczące wydatki na rozwój lub zakup kompo­ nentów COM i kontrolek ActiveX . Firma Microsoft dołożyła dużych starań, by z tych starych komponentów można było korzystać w aplikacjach .NET, jak również (choć ta możliwość jest zapewne nieco mniej ważna) by z komponentów .NET można było korzystać w aplikacjach pisanych w technologii COM. Możliwość jednoczesnego korzystania z zarządzanego kodu .NET oraz z kodu niezarządzanego pochodzącego ze starszych aplikacji Win32 i COM jest nazywana współdziałaniem (ang. interoperatibility) . W tym rozdziale zostało opisane wsparcie .NET Framework dla stosowania kontrolek ActiveX i komponentów COM w aplikacjach .NET, udostępnianie klas .NET tak, by można z nich było korzystać w aplikacjach COM, jak również stosowanie bezpośrednich wywołań Win32 API. Czytelnik znajdzie tu także informacje dotyczące stosowania wskaźników oraz słów kluczowych języka C# zapewniających możliwość bezpośredniego odwoływania się do pamięci, co może być niezbędne w przypadku korzystania z niektórych niezarządzanych interfejsów programo­ wania aplikacji.

I mportowan ie kontrolek ActiveX Kontrolki ActiveX są komponentami COM zaprojektowanymi po to, by można je było umiesz­ czać w tworzonych formularzach przy użyciu techniki „przeciągnij i upuść" . Zazwyczaj posia­ dają one jakiś interfejs użytkownika, choć można także znaleźć kontrolki, które go nie mają. Kiedy firma Microsoft opracowała standard OCX, który pozwalał programistom na tworzenie kontrolek ActiveX w języku C++ i korzystanie z nich w aplikacjach VB (i na odwrót), rozpoczęła się prawdziwa rewolucja. Było to w ubiegłym wieku, w 1994 roku, a od tego czasu tysiące takich kontrolek zostały napisane, sprzedane i użyte we wszelkiego typu aplikacjach. Są one niewielkie, stosowanie ich zazwyczaj jest bardzo łatwe i stanowią doskonały przykład kodu binarnego nadającego się do wielokrotnego stosowania. Fakt, że kontrolki ActiveX wciąż są popularne, choć od ich pojawienia się upłynęło ponad 15 lat, świadczy jedynie o tym, jak bardzo są użyteczne .

711

Pod względem wewnętrznych sposobów działania obiekty COM znacząco różnią się od obiek­ tów .NET. Jednak w momencie wprowadzania platformy .NET firma Microsoft doskonale zdawała sobie sprawę z tego, jak wielką popularnością cieszą się kontrolki ActiveX, dlatego dołożyła wszelkich starań, by zarówno .NET Framework, jak i Visual Studio w możliwie naj­ większym stopniu redukowały różnice pomiędzy światem .NET i COM. Visual Studio zapew­ nia możliwość importowania komponentów COM do dowolnych projektów .NET i sprawia, że korzystanie z kontrolek ActiveX w formularzach Windows Forms staje się wyjątkowo proste.

I mportowanie kontrolek do projektów . N ET W ramach pierwszego przykładu spróbujemy użyć bardzo popularnej kontrolki ActiveX, która była dostępna na komputerach wszystkich autorów tej książki: kontrolki Adobe PDF Reader. Każdy, kto na swoim komputerze zainstalował czytnik plików PDF bądź też opro­ gramowanie Adobe Acrobat, będzie tą kontrolką dysponował. Pozwala ona wyświetlać pliki PDF i można jej używać w różnych aplikacjach. Zacznijmy od utworzenia w Visual Studio 2010 aplikacji Windows Forms . Następnie upew­ nijmy się, że jest widoczny panel Toolbox - jeśli go nie widać, to można go wyświetlić przy użyciu odpowiedniej opcji w menu View. Następnie należy kliknąć na panelu prawym przy­ ciskiem myszy, z wyświetlonego menu kontekstowego wybrać opcję Add Tab, po czym wpisać nazwę nowej zakładki, na przykład ActiveX. W obszarze tej nowej zakładki należy kliknąć prawym przyciskiem myszy i wybrać opcję Choose Items. W efekcie na ekranie zostanie wyświe­ tlone okno dialogowe Choose Toolbox Items. W oknie tym należy przejść na kartę COM Compo­ nents, jak pokazaliśmy to na rysunku 19.1 . Teraz można wybrać dowolną liczbę kontrolek w naszym przypadku zaznaczyliśmy tylko Adobe PDF Reader. Il· -®"- l....a.-1

C lmose Too l boJ< Items

I

Silverl ight Compo nents System.Wo rkflow Cornponents COM Co mpon ents . N ET Fram ewnrk Cnmponents

I

I

,.____ -------'-------'

System .Activiti es Corn pon ents WPF C omponents

I

IEI : -) Vi d eo Soft FlexA.rray Control C:\Wi n d ows\system32WSFLEX3, O CX: :-) Vi d eo.So·ft vsFI e„, O d in rin W :\ IEI : - ) Vi d eo�s·oft� 0CX = Fl ex g C · o nt ro I C ow s\s;ystem32WSFLEX3. :-) Vi deo .So ft vsFI e„. St ��� � ��'""'������� � �����_ · [CłJ� Adobe PDF Read er C: Pro ram Fii ;�c;;;·;;:;�;;�-F i l �;\Ad�·b·;\�.·�·-·:;;;;;·;;·b;A·�;�·b·;t7�·�·-·" l>Jame

Path

Li brary

IO Apple Quic kTi me Contro·l 2 .0 IO Basic IDE Co·ntrol w - Enterpri>e IEI Basic N oUI Co ntro·I w - Enterp rise IEI Behavi or Object IO C C RPDTP6.c crpDtp IEI C ha rt Obj ect IEI Commo·nD·ialog C l ass IFI �

I

C omacmentOn e FlexGrid 8 .0

Ad obe PDF Reader La n g uage: Version :

\ i) (:\Pro gram Fi les\QuickTime\QTOContr„. C:\Program Fi les\Min djet\Mind M an ager„. (:\Program Fi les\Min djet\Mind M an ager„. C:\Program Fi les\Qui c kTime\QTPlugi n„„ C:\Program Fi les\MyPho· n eE:.:p lorer\D LL„. C:\Program Fi les\Min djet\Mind M an ager„. C:\Windo-ws\System32\wiaautd ll [UNIC OD„. C:\Proaram Fi les\ Min diet\ Mind M an aa er„. m

CCRP D ateTimePi„. ChartFX 6.2 Client.. . Micra.soft Wi nd o- „. Com a onentOne V„.

.I

"'" �

!l_rowse„.

Język neutral ny 1 .0

OK

Rysunek 19.1 . Dodawanie komponentu COM

712

Ap pl e QuickTime „. Sa·x Basi c Engi ne v„. Sa·x Ba si c Engi ne v„.

Rozdział 19. Współdziałanie z COM i Win32

]I

Cancel

11

Reset

Po kliknięciu przycisku OK wybrany komponent powinien się pojawić w zakładce, jak zostało to pokazane na rysunku 19.2. lft��

� ActiveX

I

P o i nter

Ado be PDF Reader

Rysunek 19.2 . Kontrolka ActiveX wyświetlona w panelu Toolbox Teraz można już przeciągnąć kontrolkę i umieścić ją na formularzu. Rysunek 19.3 pokazuje, jak wygląda ona na formularzu wyświetlonym w projektancie formularzy. Jej rozmiar i poło­ żenie można określać tak samo jak w przypadku wszystkich innych kontrolek. Działają także wszelkie inne mechanizmy charakterystyczne dla Windows Forms takie jak kotwiczenie można zakotwiczyć kontrolkę do wszystkich czterech krawędzi kontenera, by była powiększana wraz ze zmianami wielkości okna formularza. •9

Forml

Rysunek 19.3. Formularz z kontrolką ActiveX Adobe PDF Reader Pozostawiona sama sobie ta kontrolka nie zrobi nic sensownego - nawet aby wyświetliła jaki­ kolwiek interfejs użytkownika, musimy wskazać jej plik PDF, a to oznacza konieczność skorzy­ stania z jej API. Na szczęście jedną z rzeczy, jakie Visual Studio wykonało za nas w momencie przeciągania kontrolki na formularz, było zaimportowanie jej biblioteki typu (ang. type library) . Biblioteka typu COM zawiera metadane - listę dostępnych klas oraz opisy ich metod, właści­ wości i zdarzeń. Przypominają one nieco metadane .NET, o których wspominaliśmy w roz­ dziale 17., jednak znacząco różnią się od nich w szczegółach. Różnice te całe szczęście nie sta­ nowią żadnego problemu, gdyż w momencie zaimportowania biblioteki typu Visual Studio wygenerowało bibliotekę DLL zawierającą te same informacje zapisane w postaci komponentu .NET. To właśnie dzięki temu można używać komponentu ActiveX w kodzie C#. Tę wygene­ rowaną bibliotekę można zobaczyć w panelu Solution Explorer w sekcji References, jak pokazali­ śmy to na rysunku 19.4 . A cro PD F L i b jest zaimportowaną biblioteką typu. Takie biblioteki DLL będzie można zobaczyć za każdym razem, gdy do projektu .NET zaimportujemy jakikolwiek komponent COM. Jednak na liście przedstawionej na rysunku 19.4 można zauważyć jeszcze jeden element - AxAcroPD "+FL i b - powiązany z samą kontrolką ActiveX. (Nie wszystkie komponenty COM są tworzone

Importowanie kontrolek ActiveX

I

713



lif!l t>



WfAx]



Prop erti es References

-O AcroPD FLi b -O AxAcroPDFLib -O

Micro �o.ft.CSharp

Rysunek 19.4. Zaimportowana biblioteka typu oraz opakowanie kontrolki ActiveX z myślą o stosowaniu ich w interfejsach użytkownika aplikacji) . Ta biblioteka DLL wygene­ rowana dla kontrolki ActiveX zawiera klasę dziedziczącą po specjalnej klasie bazowej o nazwie AxHost będącą kontrolką Windows Forms, w której można umieszczać dowolne kontrolki ActiveX. Visual Studio generuje klasę AxAcroPDF dziedziczącą po AxHost i umieszcza ją w biblio­ tece AxAcroPDFL i b . To właśnie kontrolka klasy AxAcroPDF została w rzeczywistości umieszczona na formularzu. To opakowanie kontrolki ActiveX definiuje dostępne dla programów .NET wer­ sje metod, które oryginalna kontrolka udostępnia programom pisanym w technologii COM. Efektem tych wszystkich zabiegów jest to, że możemy umieszczać w pisanym przez nas kodzie ukrytym wywołania metod kontrolki, ustawiać jej właściwości i obsługiwać generowane przez nią zdarzenia. Innymi słowy, możemy się poczuć tak, jakbyśmy używali zwyczajnej kontrolki. I dokładnie o to chodziło - generując tę bibliotekę, Visual Studio chytrze ukryło fakt, że wew­ nętrzne mechanizmy działania .NET i technologii COM są całkowicie odmienne. Listing 19.1 pokazuje, w jaki sposób możemy korzystać z kontrolki w kodzie programu. Konkretnie rzecz biorąc, pokazuje on konstruktor formularza w kodzie ukrytym, w którym po standardowym wywołaniu metody I n i t i a l i z eComponent ustawiamy wartość właściwości src kontrolki, infor­ mując ją, jaki plik PDF ma wyświetlić.

Listing 19. 1 . Ustawianie właściwości kontrolki publ i c Forml () { I n i t i al i zeComponen t () ; s t r i ng pdf = " http : //www . i nteract - sw . eo . u k/down l oads/Exampl ePdf . pdf " ; pdfAxCtl . src = pdf ; }

Jeśli uruchomimy ten program, wczyta on wskazany dokument PDF. Jak można się przeko­ nać, patrząc na rysunek 19.5, kontrolka wyświetla swoje własne elementy interfejsu użytkow­ nika służące do prowadzenia interakcji z dokumentem. Jeśli Czytelnik korzysta z programu Adobe PDF Reader, to zapewne poznaje te kontrolki . Co więcej, zazwyczaj dokładnie ta sama kontrolka ActiveX jest używana także podczas przeglądania dokumentów PDF w przeglądar­ kach WWW.

Podzespoły współdziałan ia Jak pokazaliśmy w poprzednim podrozdziale, Visual Studio jest w stanie generować podzespoły zawierające dostosowaną do potrzeb .NET reprezentację informacji o typach zapisanych w bibliotekach typów COM. Podzespoły takiego rodzaju są nazywane podzespołami współdziała­ nia (ang. interop assemblies) . Kryje się tu jednak pewien potencjalny problem: co się stanie, jeśli wielu programistów wyge­ neruje takie podzespoły współdziałania dla tego samego komponentu COM? W większości

714

I

Rozdział 19. Współdziałanie z COM i Win32

oSJ Forml Tools

One of the fun things about writi11g a book is that you need to be careful abm1t the resources );ou point to if those resources will end up in the boo� in

some

fom1. For example, th e Interop chapter of Programming C# 4 . 0 sh.ows

how to use Adobe's PDF >�ewe.r control from C#. (It's an ActiveX control, so

it is d es igneil for COiM clie11ts, so we n.eed Interop to use the thing from C# . ) Since we would like t o show a screenshot of the finished resuJt, tliat means

sho\1�ng a PDF documeot. We neeil it to be a docurnent that we're entitle.d to

copy into the book. We can't link to just ;my old PDF.

This document'.s, sole purpose is to be something we're free to inclu d e because we created it. It is not vel}' interesting. There's nothing to see here.

M.ove a!o11g , please .

Rysunek 19.5. Kontrolka ActiveX w aplikacji Windows Forms przypadków wszystko będzie w porządku, gdyż te powielające się podzespoły będą się znaj­ dowały w niezależnych aplikacjach. Co się jednak stanie, jeśli będziemy dysponowali dwiema bibliotekami .NET korzystającymi z tego samego komponentu COM? Załóżmy, że tworzymy system przetwarzania dokumentów korzystający z API programu Microsoft Word udostępnianego przy użyciu komponentów COM. Jeśli nasz system będzie zawierał dwie biblioteki, z których każda będzie musiała korzystać z programu Word, to ich programiści będą musieli dodać odpowiednie odwołania . W rezultacie uzyskamy dwa różne podzespoły współdziałania dla tego samego typu COM używane w jednym procesie. A to już większy problem, gdyż pojawią się dwie różne reprezentacje czegoś, co powinno być jednym typem. ••

• .·

.._,..�;

.______..,.

.

'

API programu Word utworzone w technologii COM obsługuje automatyzację, co oznacza, że używając go, możemy korzystać ze słowa kluczowego dynami c opisanego w rozdziale 18. Dzięki temu w ogóle nie musimy korzystać z podzespołów współdziałania, co definitywnie rozwiązuje nasz problem. Niemniej jednak pewne obszary API programu Word pozwalaj ą na wykorzystanie takich statycznych typów, a zatem programiści wcale nie muszą korzystać ze słowa kluczowego dynami c, zyskując tym samym wszystkie wygody, jakie niesie ze sobą technologia IntelliSense, oraz dodatkowe mechanizmy sprawdzania typów w czasie kompilacji kodu. Może się także zdarzyć, że kod, którego chcemy używać, został napisany jeszcze przed wprowadzeniem słowa kluczowego dynami c .

Podzespoły współdziałania

715

Aby uniknąć sytuacji, gdy dla jednego typu COM pojawia się kilka typów .NET, twórca typu COM może udostępnić tak zwany główny podzespół wspo1działania (ang. primary interop assembly, w skrócie PIA) . Firma Microsoft udostępnia na przykład taki PIA dla pakietu Office - jeśli zajrzymy na kartę .NET okna dialogowego Add Reference, na liście tam wyświetlonej znajdziemy podzespoły o nazwach zaczynających się od Microsoft.Office.Interop. PIA dla programu Word ma nazwę Microsoft.Office.Interop.Word. Jeśli w ramach jednej aplikacji istnieje wiele komponen­ tów, z których każdy korzysta z możliwości programu Word, to o ile tylko wszystkie będą korzystały z tego PIA, będą także używały jednej reprezentacji typów programu Word w .NET Framework. Pewnym kłopotliwym aspektem korzystania z PIA jest to, że instalowanie ich na docelowym komputerze może się okazać sporym problemem. Podzespoły te są bowiem stosunkowo duże pakiet instalacyjny PIA dla pakietu Office 2007 ma 6,3 MB wielkości . Dodatkowego nakładu pracy wymaga także zintegrowanie PIA z procesem instalacji naszej aplikacji. Nie są to pro­ blemy nie do przezwyciężenia, jednak bez wątpienia stopień ich złożoności jest całkiem duży. Dlatego też w języku C# 4.0 pojawiło się rozwiązanie alternatywne określane jako „bez PIA" (ang. no PIA) .

Bez PIA D o systemu typów .NET Framework 4 dodano nową możliwość określaną jako równoważność typów (ang. type equivalence) . Pozwala ona, by dwie różne definicje typów były traktowane jako ten sam, jeden typ . Dzięki temu podczas uruchamiania aplikacji możemy się obyć bez PIA jeśli wszystkie podzespoły współdziałania będą takie same, to nie trzeba będzie z niego korzystać. '

. .

,

...._ .__ � · •,

Choć równoważność typów sprawia, że możemy zrezygnować ze stosowania PIA, to jednak możliwość ta nie jest dostępna domyślnie - musimy o to zadbać sami. Aby dwa typy mogły być sobie równoważne, muszą mieć taką samą strukturę, a dodatkowo trzeba je oznaczyć własnym atrybutem Typeldenti fi er. (Atrybuty zostały opisane w rozdziale 17.). Dlatego też ta nowa możliwość nie zmienia działania już istniejącego kodu.

Możliwość pominięcia korzystania z PIA, jaką zapewnia C# 4.0, bazuje na wykorzystaniu rów­ noważności typów: możemy umieścić w naszych podzespołach informacje o typach współ­ działania, eliminując tym samym konieczność stosowania jakichkolwiek modułów współdzia­ łania. Kiedy w projekcie znajdzie się odwołanie do PIA takiego jak Microsoft.Office.Interop.Word, Visual Studio określi, z jakich typów w rzeczywistości korzysta nasz kod, i skopiuje je do skompilowanego kodu wynikowego, dodając przy tym niezbędne adnotacje umożliwiające wykorzystanie równoważności typów. A zatem choć w projekcie będzie występowało odwo­ łanie do PIA, to w skompilowanym kodzie go już nie będzie - on sam będzie już bowiem zawierał wszystkie niezbędne informacje. Takie rozwiązanie zapewnia dwie korzyści . Przede wszystkim nie trzeba już instalować PIA na komputerze docelowym. Po drugie, sprawia ono, że programy zazwyczaj stają się nieco mniejsze. Może się to wydawać nieco dziwne - skoro każdy podzespół zawiera własne kopie typów współdziałania, można by oczekiwać, że podzespoły te powinny być większe . Jednak w rzeczywistości większość podzespołów korzysta wyłącznie z drobnego ułamka typów zdefi­ niowanych w używanych komponentach COM. Jeśli każdy podzespół będzie korzystał tylko z kilku definicji typów, to jest całkiem prawdopodobne, że ich wielkość i tak będzie mniejsza od całego PIA dla pakietu Microsoft Office, który ma 6,3 MB wielkości .

716

I

Rozdział 19. Współdziałanie z COM i Win32

'

. .

,

..._-__-� ·

Istnieje możliwość umieszczania typów współdziałania w opakowaniach kontrolek ActiveX, gdyż generowane opakowania nie zawierają żadnych informacji o typach COM. Zawierają one natomiast wygenerowany kod .

Nie zawsze będziemy chcieli umieszczać w naszym kodzie informacje o typach współdziałania. Jeśli tylko zdecydujemy się na zastosowanie takiej strategii wdrażania aplikacji, to zawsze możemy bazować na założeniu, że odpowiednie PIA będą dostępne na komputerze docelo­ wym, na którym wcześniej zostały zainstalowane odpowiednie komponenty. W takim przy­ padku instalacja PIA będzie dla naszej aplikacji kwestią drugorzędną, a powiększanie jej poprzez dodawanie do niej typów stanie się całkowicie zbędne. Będziemy mogli wówczas bezpiecz­ nie zrezygnować z techniki dodawania typów współdziałania . Dodawanie ich odbywa się na podstawie odwołania i dla wszystkich nowych zaznaczanych odwołań jest domyślnie włą­ czane. Jeśli w sekcji References w panelu Solution Explorer Visual Studio umieścimy jakieś odwoła­ nie do podzespołu współdziałania, to w panelu Properties pojawi się dodatkowa opcja Embed Interop Types. Przypisanie jej wartości Fal se sprawi, że powrócimy do starego rozwiązania bazu­ jącego na PIA.

64 czy 32 bity? Współdziałanie z kodem niezarządzanym stanowi nie lada wyzwanie dla systemów 64-bitowych. Niezależnie od tego, czy używamy komponentów COM takich jak kontrolki ActiveX, czy też zwyczajnych, starych, niezarządzanych bibliotek DLL, trzeba wiedzieć, czy dany kod jest 32-, czy też 64-bitowy. Jeśli pominiemy to zagadnienie, może się okazać, że nasza aplikacja nie będzie działać w 64-bitowych wersjach systemu Windows. Ogólnie rzecz biorąc, nie jest możliwe, by jeden fragment kodu maszynowego był prawidłowo wykonywany zarówno w środowisku 32-, jak i 64-bitowym. Architektura 64-bitowego proce­ sora Intel Itanium jest całkowicie odmienna od architektury x86, dla której były przeznaczone 32-bitowe systemy Windows, więc w trybie 64-bitowym komputer wykorzystuje i rozumie całkiem inny zestaw instrukcji. Bardziej popularna architektura x64, którą można znaleźć w więk­ szości nowoczesnych komputerów 64-bitowych, ma znacznie więcej wspólnego z jej poprzed­ niczką - architekturą x86 - jednak nawet pomimo tego jej tryby 32- i 64-bitowy będą wyma­ gały zastosowania zupełnie innych plików binarnych. Jedynym powodem, dla którego istniejące aplikacje 32-bitowe mogą w ogóle działać w 64-bitowych wersjach systemu Windows, jest to, że systemy te są w stanie uruchamiać procesy 32-bitowe. (Łatwo można się przekonać, które to są procesy. Wystarczy wyświetlić kartę Procesy Menedżera zadań i spojrzeć na kolumnę Nazwa obrazu - w przypadku procesów 32-bitowych będą w niej widoczne znaki *32 . Oczywiście zobaczymy je wyłącznie wtedy, gdy będziemy używali 64-bitowej wersji systemu Windows, gdyż w przypadku wersji 32-bitowej taka informacja byłaby zupełnie niepotrzebna) . W przy­ padku takich procesów Windows uruchamia procesor w innym trybie - jego 64-bitowe moż­ liwości zostają ukryte, co pozwala na wykonywanie starego, 32-bitowego kodu. Pomimo tego programy .NET często nie muszą zwracać uwagi na to, czy są uruchamiane na komputerach 32-, czy na 64-bitowych. C# kompiluje kod źródłowy do niezależnego od proce­ sora kodu pośredniego, który następnie - w momencie wykonywania aplikacji - jest kom­ pilowany przez kompilator JIT (ang. Just in time) do kodu wykonywalnego. Jeśli aplikacja jest uruchamiana w 32-bitowej wersji systemu Windows (bądź w ramach 32-bitowego procesu działającego w 64-bitowej wersji systemu), to środowisko uruchomieniowe (CLR) platformy 64 czy 32 bity?

I

717

.NET skompiluje ją do postaci 32-bitowego kodu wykonywalnego. Jeśli natomiast zostanie ona uruchomiona jako proces 64-bitowy, to wygenerowany przez CLR kod wynikowy także będzie 64-bitowy. Oznacza to, że pisząc kod aplikacji, nie trzeba zwracać uwagi na to, czy będzie ona wykonywana na komputerze 32-, czy też na 64-bitowym. Jednak kod niezarządzany nie zapewnia takich luksusów, gdyż wykonywalny kod binarny aplikacji jest generowany już podczas jej kompilacji - programista musi jasno określić, czy kompilator ma go wygenerować w wersji 32-, czy też 64-bitowej . A zatem w przypadku sto­ sowania niezarządzanej biblioteki DLL lub komponentu COM będą one w stanie działać wyłącznie na komputerach 32- lub 64-bitowych. Jeden proces realizowany przez system Win­ dows nie może składać się z fragmentów kodu 32- i 64-bitowego - może on zawierać wyłącz­ nie jeden z nich. Jeśli do 64-bitowego procesu spróbujemy wczytać niezarządzany kompo­ nent 32-bitowy, to Windows stanowczo się temu sprzeciwi . Jeśli będzie to komponent COM (na przykład kontrolka ActiveX), to zostanie zgłoszony wyjątek COM Except i on z następującym komunikatem: Cl ass not reg i s tered (Excepti on from H RESU LT : Ox80040 154 (REGDB_E_CLASSNOTREG) ) '

Taki problem może być szalenie kłopotliwy, zwłaszcza jeśli potrzebny nam komponent wydaje się być zainstalowany. W rzeczywistości polega on na tym, że .NET stara się odnaleźć 64-bitową wersję komponentu i nie jest w stanie tego zrobić, więc wyświetla komunikat, że poszukiwana klasa COM najprawdopodobniej nie została zainstalowana. '

.'

, '---�.

Istnieje możliwość zainstalowania zarówno 32-, jak i 64-bitowej wersji komponentu COM. W takim przypadku opisywany wcześniej błąd by się nie pojawił, jednak takie rozwiązania są raczej sporadycznie stosowane. Kontrolki ActiveX są w większości 32-bitowe.

Byłoby miło, gdyby bądź to COM, bądź platforma .NET były w stanie wygenerować bardziej zrozumiały komunikat o przyczynie błędu taki jak: „Komponent, którego chcesz użyć, jest dostępny wyłącznie w wersji 32-bitowej, podczas gdy proces jest 64-bitowy" . Jednak to by wymagało wykonania dodatkowych czynności związanych z procesem, który i tak niebawem zostanie zamknięty, i to wyłącznie po to, by przekazać nam informację, do której możemy dotrzeć sami. Takie postępowanie oznaczałoby marnowanie cykli procesora i zapewne dlatego platforma .NET tego nie robi. Dokładnie ten sam problem istnieje w przypadku stosowania bibliotek DLL, choć tym razem zgłaszany jest nieco inny błąd: Bad image Format Except i on . (Angielskie słowo image - obraz - jest czasami używane w odniesieniu do skompilowanego, binarnego komponentu przeznaczonego do wczytania i wykonania w ramach jakiegoś procesu) . Ten błąd może być bardzo frustrujący, gdyż po przeczytaniu jego komunikatu lub po przejrzeniu dokumentacji wyjątku łatwo odnieść wrażenie, że to biblioteka DLL, którą staramy się wczytać, uległa uszkodzeniu. Jednak w rzeczy­ wistości chodzi o to, że jest ona zapisana w formacie, którego nie można wczytać do danego procesu, co oznacza, że w 64-bitowym procesie próbowaliśmy wczytać 32-bitową bibliotekę DLL. Aby uniknąć takich problemów, Visual Studio automatycznie konfiguruje projekty aplikacji WPF oraz Windows Forms tak, by działały w trybie 32-bitowym. Jeśli wyświetlimy właściwości projektu i przejdziemy na kartę Build, zobaczymy na niej opcję Platform target: - w przypadku projektów aplikacji z graficznym interfejsem użytkownika domyślnie będzie w niej wybrana 1 Klasa niezarejestrowana (wyjątek z HRESULT: Ox80040154 (REGDB_E_CLASSNOTREG))

718

I

Rozdział 19. Współdziałanie z COM i Win32

-

przyp. tłum.

wartość x86, jak pokazaliśmy to na rysunku 19.6. W takim przypadku aplikacja zawsze będzie uruchamiana jako proces 32-bitowy, nawet w 64-bitowej wersji systemu Windows . W zdecy­ dowanej większości sytuacji jest raczej mało prawdopodobne, żeby takie ustawienia przyspo­ rzyły nam problemów. Bardzo rzadko zdarza się, by aplikacje z graficznym interfejsem użyt­ kownika przetwarzały tak ogromne ilości danych, by niezbędne było uruchamianie ich w trybie 64-bitowym. Znacznie bardziej prawdopodobne jest to, że takie aplikacje będą korzystały z kon­ trolek ActiveX, dlatego też takie zachowawcze ustawienie domyślne ma sens. (A jeśli już się zdarzy, że będziemy pisać niezwykłą aplikację wymagającą wielogigabajtowej przestrzeni adre­ sowej, to zawsze będziemy mogli zmienić ustawienia projektu) . IO Allow unsafe c ode Pl atform ta rg et:

IO Opti m i!Ze code

x86

Anv C P U x64

Itanilll m

... j



I

Rysunek 19.6. Ustawienia projektu dotyczące architektury komputera W przypadku tworzenia bibliotek klas sprawy mogą się jednak nieco skomplikować. W pro­ jektach bibliotek Visual Studio domyślnie ustawia opcję platformy docelowej (Platform target) na Any CPU2 • Podczas tworzenia bibliotek klas ustawienie platformy docelowej ma nieco inne znaczenie, gdyż to nie biblioteka będzie określać, czy proces będzie wykonywany w trybie 32-, czy też w 64-bitowym. Decyzja o wyborze jednego z tych trybów jest podejmowana w momencie uruchamiania procesu, a zatem jest przy tym uwzględniana platforma docelowa wykonywanego pliku .exe. W chwili, gdy zostaje wczytany kod biblioteki, jest już zbyt późno na ewentualną zmianę tej decyzji. Dlatego też wybór wartości Any CPU jest całkowicie uza­ sadniony - biblioteki klas muszą być bardziej elastyczne. Niemniej jednak, pisząc bibliotekę wykorzystującą mechanizmy współdziałania, możemy zdecydować się na zmianę tego ustawie­ nia. Jeśli działanie naszej biblioteki zależy od niezarządzanego kodu dostępnego wyłącznie w wersji 32-bitowej, to nie będzie ona mogła działać w ramach 64-bitowego procesu, a my powinniśmy poinformować o tym fakcie, zmieniając ustawienie platformy docelowej na x86. Kiedy to zrobimy, nasza biblioteka DLL nie będzie mogła być wczytywana przez procesy 64-bitowe, co może być nieco denerwujące, jednak i tak jest znacznie lepsze od sytuacji, gdy bibliotekę można wczytać bez problemu, lecz później, podczas działania programu, pojawiają się błędy. Będzie znacznie lepiej, jeśli komponent z góry jasno poinformuje, że może być uży­ wany wyłącznie w ramach procesu 32-bitowego. Swoją drogą bardzo często nic nie powstrzymuje twórców niezarządzanych komponentów przed udostępnianiem ich zarówno w 32-, jak i w 64-bitowej wersji. Co prawda zarówno tworzenie, jak i testowanie, wdrażanie oraz wspieranie takich komponentów jest nieco bardziej skompli­ kowane, niemniej jednak jest ono jednocześnie całkowicie realne. Zagadnienia związane z kodem 32- i 64-bitowym mają jakiekolwiek znaczenie dla platformy .NET wyłącznie dlatego, że więk­ szość aktualnie używanych niezarządzanych komponentów jest dostępna w wersji 32-bitowej . (Choć twórcy komponentów mogliby udostępniać je w wersjach 64-bitowych, to zazwyczaj tego nie robią) . Jeśli niezarządzany kod niezbędny do działania naszej aplikacji jest dostępny w różnych wersjach, to nic nie stoi na przeszkodzie, by jako platformę docelową wybrać Any CPU. 2 Dowolny procesor

-

przyp. tłum.

64 czy 32 bity?

I

719

Czasami może się zdarzyć, że niektóre komponenty będą dostępne w wersji 64-bitowej przeznaczonej dla systemów z architekturą x86, lecz nie dla komputerów z procesorami Itanium. A zatem określenie platformy docelowej powinno mieć w ich przypadku nieco inną formę: „x86 lub x64, lecz nie Itanium". Niestety takiej wartości nie ma, więc w rzeczywistości należałoby wybrać wartość Any CPU, aby kod mógł działać zarówno na komputerach z architekturą x86, jak i x64. W systemach z procesorem Itanium wykorzystanie takiego kodu spowoduje wystąpienie błędu w czasie działania pro­ gramu, jednak administrator może ten problem ominąć, wymuszając wykonywanie aplikacji w procesie 32-bitowym. W kwietniu 2010 roku firma Microsoft ogłosiła, że przyszłe wersje systemu Windows oraz Visual Studio nie będą obsługiwały procesorów Itanium, a zatem wygląda na to, że w perspektywie dłuższego czasu ten problem ze współdziałaniem kodu i tak zniknie.

Jeśli biblioteki DLL używane przy wykorzystaniu technik współdziałania należą do Win32 API, to zazwyczaj można uruchamiać aplikacje zarówno w trybie 32-, jak i w 64-bitowym, gdyż system Windows udostępnia swój interfejs programowania aplikacji w obu wersjach. Prze­ konajmy się zatem, w jaki sposób można korzystać z bibliotek DLL takich jak te, które wchodzą w skład Win32 API .

Mechan izm P /l nvoke Komponenty COM nie są jedynym rodzajem kodu niezarządzanego, którego być może będziemy musieli używać. Czasami może się na przykład zdarzyć, że będziemy chcieli skorzystać z metod Win32 API. Wraz z każdą nową wersją .NET potrzeba stosowania takich rozwiązań staje się coraz mniejsza, gdyż biblioteka klas .NET udostępnia przyjazne opakowania dla coraz to więk­ szej liczby usług systemowych, niemniej jednak wciąż można wskazać kilka sytuacji, w których warto wykorzystać bezpośrednio możliwości Win32 API. By to zrobić, będziemy musieli sko­ rzystać z możliwości C# określanej jako P/Invoke. . .·

ł

'

Litera P jest skrótem od angielskiego słowa platform (platforma), gdyż rozwiązanie to zostało początkowo opracowane wyłącznie w celu zapewnienia możliwości dostępu do API systemu operacyjnego, w jakim działała platforma .NET Framework. W rzeczy­ wistości można z niego jednak korzystać w celu wywoływania funkcji dostępnych w dowolnych bibliotekach DLL - nie tylko w tych wchodzących w skład Win32 API.

Aby zobaczyć, jak to wszystko działa, przyjrzyjmy się jednej z metod udostępnianych przez systemową bibliotekę kernel32 .dll - funkcji Move F i l e3 . W odróżnieniu od komponentów COM zwyczajne biblioteki DLL nie zawierają dostatecznie wielu informacji, by całkowicie opisać udostępniane funkcje - zostały one zaprojektowane pod kątem wywoływania tych funkcji z kodu C lub C++, natomiast kompletne informacje na temat samych funkcji są umieszczane w plikach nagłówkowych dostarczanych wraz z Windows SDK. Jednak kompilator C# nie wie, jak odczytywać pliki nagłówkowe C, dlatego musimy dostarczyć mu pełen opis sygnatury funkcji, którą chcemy wywoływać. W tym celu zadeklarujemy funkcję jako stat i c extern i uży­ jemy dodatkowo atrybutu Dl l I mport : 3

Ten przykład został zamieszczony wyłącznie w celach demonstracyjnych - w rzeczywistym programie sko­ rzystalibyśmy z metody MoveTo klasy Fi l e l n fo, gdyż jest ona znacznie wygodniejsza w użyciu. Klasa ta sama korzysta z mechanizmu P/Invoke. Kiedy my wywołujemy jej metodę MoveTo, ona wywołuje systemową funkcję MoveFi l e.

720

I

Rozdział 19. Współdziałanie z COM i Win32

[Dl l Import ( " kernel 32 . dl l " , EntryPoi nt= " Move Fi l e " , ExactSpel l i ng=fal s e , CharSet=CharSet . Un i code , Setlast Error=true ) ] s t at i c extern bool Move Fi l e ( stri ng source Fi l e , stri ng des t i nat i on F i l e) ;

Atrybutu Dl l Import używamy po to, by zaznaczyć, że niezarządzana funkcja będzie wywoły­ wana z wykorzystaniem P/Invoke. Poniżej opisaliśmy parametry tego atrybutu.

Nazwa biblioteki DLL To nazwa biblioteki DLL zawierającej wywoływaną funkcję. EntryPo i n t Ten parametr określa wywoływany punkt wejścia do biblioteki (czyli funkcję) . ExactSpel l i ng CLR rozumie pewne konwencje określania nazw funkcji umieszczanych w bibliotekach DLL. Na przykład w rzeczywistości nie ma żadnej funkcji Move F i l e są dwie funkcje o nazwach Mov e F i l eA oraz MoveFi l ew zaprojektowane odpowiednio do obsługi łańcuchów znaków ANSI oraz Unicode. Przypisanie atrybutowi Exa ctSpel l i ng wartości fal s e sprawi, że CLR będzie wybierać funkcję zgodnie z tymi regułami. -

C harSet Ten parametr określa, w jaki sposób mają być szeregowane argumenty łańcuchowe. Set last Error Przekazanie wartości true pozwala na wywołanie metody Mars hal . GetlastWi n32 Error i spraw­ dzenie, czy podczas wywoływania funkcji wystąpił jakiś błąd.

W rzeczywistości z wyjątkiem nazwy biblioteki DLL wszystkie parametry są opcjonalne . Jeśli nie określimy wartości parametru Entry Poi nt, to .NET jako nazwy punktu wejścia użyje nazwy metody. Parametr ExactSpel l i ng domyślnie przyjmuje wartość fal s e wartość t rue przypi­ sujemy mu wyłącznie w przypadku, gdy nie chcemy stosować zwyczajnych konwencji na­ zewniczych. W razie pominięcia parametru C harSet CLR spróbuje użyć opcji szeregowania Unicode, o ile tylko będzie ona dostępna. I w końcu parametr Set last Error domyślnie przyjmuje wartość fal s e, a zatem choć jest on opcjonalny, to jednak warto z niego skorzystać i przypisać mu wartość true. Dlatego też w praktyce zastosowalibyśmy zapewne atrybut Dl l Import o nastę­ pującej postaci: -

[Dl l lmport ( " kernel 32 . dl l " , Set las tError=true) ] s t at i c extern bool Move Fi l e ( stri ng source Fi l e , stri ng des t i nat i on F i l e) ;

Podstawowym powodem, dla którego P/Invoke udostępnia te wszystkie ustawienia opcjonalne, jest fakt, że nie we wszystkich bibliotekach DLL są stosowane standardowe konwencje. W więk­ szości przypadków ustawienia domyślne zadziałają prawidłowo, jednak od czasu do czasu zdarzy się, że trzeba będzie je zmodyfikować. Po dodaniu powyższej deklaracji będziemy już mogli wywoływać funkcję MoveFi l e jak zwy­ czajną metodę statyczną. A zatem jeśli jej deklarację umieściliśmy w klasie Test er, to możemy użyć następującego wywołania: Tes ter . Move Fi l e (fi l e . Fu l l Name , fi l e . Fu l l Name + " . ba k " ) ;

Mechanizm P /lnvoke

I

721

W wywołaniu naszej metody przekazujemy oryginalną oraz nową nazwę pliku, a system Windows przeniesie plik zgodnie z naszym żądaniem. W przedstawionym przykładzie sko­ rzystanie z mechanizmu P/Invoke nie daje nam żadnych korzyści, a wprost przeciwnie - jest bardzo niekorzystne. (Sytuacje, w których skorzystanie z niego jest niezbędne, pojawiają się coraz rzadziej i są coraz bardziej niejasne. Aby zilustrować zastosowanie tego mechanizmu, wybraliśmy przykład na tyle prosty, by nie zaciemnił szczegółów jego działania. Oznacza to tym samym, że nie jest to scenariusz, w którym w rzeczywistości zastosowalibyśmy mechanizm P/Invoke) . Porzuciliśmy świat kodu zarządzanego, a w rezultacie także mechanizmy bezpie­ czeństwa typów, dlatego też nasz kod nie będzie już działał w sytuacjach „ograniczonego zaufa­ nia" . Listing 19.2 przedstawia pełny kod źródłowy przykładu wykorzystującego mechanizm P/Invoke do przenoszenia plików.

Listing 19.2 . Korzystanie z mechanizmu P/Invoke do wywoływania fankcji Win32 API us i ng Sys tem ; us i ng Sys tem . I O ; us i ng Sys tem . Runt i me . InteropServ i ces ; namespace U s i ngPinvo ke { cl ass Tester

li Zadeklarowanie funkcji Win API, którą chcemy wywoływać li przy użyciu mechanizmu P/Invoke [Dl l lmport ( 11 kernel 32 . dl l 11 , EntryPo i nt = 11 Move F i l e 11 , ExactSpel l i ng = fal se , CharSet = CharSet . Un i code , Set last Error = true) ] stat i c extern bool MoveFi l e ( s t r i ng source F i l e , stri ng des t i nat i on F i l e) ; publ i c stat i c vo i d Mai n () { li Utworzenie obiektu naszej klasy i uruchomienie go Tester t = new Tes ter () ; s t r i ng theDi rectory = @ 11 c : \test\med i a 11 ; D i rectorylnfo d i r = new D i rectory i n fo (theDi rectory) ; t . Expl oreDi rectory (d i r) ;

li W wywołaniu funkcji należy określić katalog. pri vate vo i d Expl oreD i rectory ( D i rectory i n fo d i r) { li Utworzenie nowego katalogu s t r i ng newD i rectory = 11 newTes t 11 ; D i rectorylnfo newSubD i r = di r . CreateSubd i rectory (newD i rectory) ; li Pobranie plików z katalogu i skopiowanie li ich do innego katalogu Fi l e l n fo [] fi l es i n D i r = d i r . Ge t F i l es () ; foreach ( Fi l e l n fo fi l e i n fi l es i nDi r) { s t r i ng ful l Name = newSubD i r . Ful l Name + 11 \\ 11 + fi l e . N ame ; fi l e . CopyTo (fu l l Name) ; Consol e . Wri teli ne ( 11 Pl i k { O } s kopi owano do kat al ogu newTes t . 11 , fi l e . Fu l l Name) ;

722

I

Rozdział 19. Współdziałanie z COM i Win32

li Pobranie kolekcji skopiowanych plików fi l es i n D i r = newSubDi r . Ge t F i l es () ; li Usunięcie niektórych plików i zmiana nazwy innych i nt counter = O ; foreach ( F i l e l n fo fi l e i n fi l es i nDi r) { s t r i ng ful l Name = fi l e . Fu l l Name ; i f (counter++ % 2 == O) { li Wywalanie funkcji Win API przy użyciu P/Invoke Tes ter . MoveFi l e (ful l Name , ful l Name + " . ba k " ) ; Consol e . Wri teli ne ( " Nazwę pl i ku { O } zmi en i ono na { 1 } . " , ful l Name , fi l e . Fu l l Name) ; el s e { fi l e . Del ete () ; Consol e . Wri teli ne ( " Us u n i ęto pl i k { O } . " , ful l Name) ;

li Usunięcie podkatalogu newSubD i r . Del ete (true) ;

A oto przykładowe wyniki generowane przez ten program: Pl i k c : \test\med i a\ch i mes . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\chord . wav s kopi owano do katal ogu newTest . Pl i k c : \test\med i a\des ktop . i n i s kop i owano do katal ogu newTest . Pl i k c : \test\med i a\di ng . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\dts . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\fl ouri s h .mi d s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\ i r-beg i n . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\ i r-end . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\ i r i nter . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\not i fy . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\onestop .mi d s kop i owano do katal ogu newTest . Pl i k c : \test\med i a\recycl e . wav s kop i owano do katal ogu newTest . Pl i k c : \test\med i a\ri ngout . wav s kop i owano do katal ogu newTest . Pl i k c : \test\med i a\Speech D i s amb i guat i on . wav s kopi owano do katal ogu newTes t . Pl i k c : \test\med i a\Speech M i s recogn i t i on . wav s kopi owano do katal ogu newTes t . Nazwę pl i ku c : \tes t \med i a\newTes t\ch i mes . wav zmi en i ono na c : \ test\med i a\newTes t\ch i mes . wav Usuni ęto pl i k c : \test\med i a\newTest\chord . wav . Nazwę pl i ku c : \tes t \med i a\newTes t\des ktop . i n i zmi en i ono na c : \test\med i a\newTest\des ktop . i n i . Usuni ęto pl i k c : \test\med i a\newTest\d i ng . wav . Nazwę pl i ku c : \tes t \med i a\newTest\dts . wav zmi en i ono na c : \tes t\medi a\newTes t\dts . wav . Usuni ęto pl i k c : \test\med i a\newTest\fl ouri s h . mi d . Nazwę pl i ku c : \tes t \med i a\newTest\ i r beg i n . wav zmi en i ono n a c : \test\med i a\newTest \ i r_beg i n . wav . Usuni ęto pl i k c : \tes t\med i a\newTest\ir end . wav . Nazwę pl i ku c : \tes t \med i a\newTest\ i r i nter . wav zmi en i ono na c : \test\med i a\newTest\ i r_i nter . wav . Usuni ęto pl i k c : \tes t\med i a\newTest\not i fy . wav . Nazwę pl i ku c : \tes t \med i a\newTes t\onestop . m i d zmi en i ono na c : \test\medi a\newTes t\ones top . m i d . Usuni ęto pl i k c : \test\med i a\newTest\recycl e . wav . Nazwę pl i ku c : \tes t \med i a\newTes t\ri ngout . wav zmi en i ono na c : \test\med i a\newTes t\ri ngout . wav . Usuni ęto pl i k c : \test\med i a\newTest\Speech D i samb i guat i on . wav .

Mechanizm P /lnvoke

723

Wskaźniki Jak do tej pory w książce tej nie mieliśmy okazji zobaczyć wskaźników doskonale znanych z języków C i C++. Dla rodziny języków C wskaźniki mają kluczowe znaczenie, jednak w C# ich zastosowanie ograniczono do bardzo nietypowych i złożonych sytuacji. Zazwyczaj są one używane wraz z mechanizmem P/Invoke oraz, okazjonalnie, w przypadkach stosowania kom­ ponentów COM. C# obsługuje standardowe operatory służące do posługiwania się wskaźni­ kami w języku C. Przedstawiliśmy je w tabeli 19.1 .

Tabela 19.1 . Operatory C# związane ze wskaźnikami Operator

Znaczenie

&

Operator pobrania adresu; zwraca wskaźnik do swego argu ment u .

*

Operator wyłuskania; zwraca wartość, na jaką wskazuje wskaźnik.

->

Operator dostępu do składowej ; pozwala odwołać się do składowej obiektu, na który wskazuje wskaźnik.

Teoretycznie rzecz biorąc, wskaźników w C# możemy używać wszędzie, jednak w praktyce nie są one potrzebne niemal nigdzie z wyjątkiem niektórych rozwiązań wykorzystujących mecha­ nizmy współdziałania i właściwie zawsze odradza się ich stosowanie. W przypadku korzysta­ nia ze wskaźników musimy umieścić w kodzie modyfikator unsafe . Kod jest oznaczany jako niebezpieczny4, gdyż wskaźniki pozwalają bezpośrednio manipulować przydzielaną pamięcią, co prowadzi do pominięcia różnego typu zabezpieczeń związanych z kontrolą typów. W takim niebezpiecznym kodzie możemy uzyskiwać bezpośredni dostęp do pamięci, przeprowadzać konwersje pomiędzy typami wskaźnikowymi i całkowitymi, określać adresy zmiennych, prze­ prowadzać działania arytmetyczne na wskaźnikach i tak dalej . W zamian tracimy możliwo­ ści: korzystania z mechanizmu odzyskiwania pamięci, ochrony przed niezainicjalizowanymi zmiennymi i wskaźnikami odwołującymi się do nieistniejących obiektów oraz odwoływania się do elementów położonych poza zakresem tablic. W zasadzie modyfikator unsafe tworzy w bezpiecznym kodzie aplikacji C# niebezpieczną enklawę zawierającą kod podatny na wystę­ powanie wszelkiego typu problemów związanych ze stosowaniem wskaźników, które są tak dobrze znane z języka C ++. Co więcej, taki kod nie będzie działał w sytuacjach ograniczonego zaufania . ••



.

·

.._,..�;

.

......___� ·

Technologia Silverlight w ogóle nie obsługuje niebezpiecznego kodu, gdyż pisane w niej aplikacje działają wyłącznie w środowisku ograniczonego zaufania. Kod Silverlight wykonywany w przeglądarce zawsze podlega ograniczeniom, gdyż kod pobierany z internetu jest uznawany za potencjalnie niebezpieczny. Także kod Silverlight wykonywany poza przeglądarką jest ograniczany i nawet „podwyższone" uprawnienia, o jakie może on poprosić, nie gwarantują mu pełnego zaufania. Technologia Silverlight korzysta z reguł bezpieczeństwa typów, by zapewniać bezpieczeństwo aplikacji, i to właśnie z tego powodu stosowanie w niej niebezpiecznego kodu nie jest dozwolone.

W ramach przykładu prezentującego zastosowanie wskaźników wyświetlimy w oknie kon­ soli zawartość pliku, korzystając przy tym z dwóch funkcji Win32 API: Create F i l e oraz ReadFi l e. Drugim argumentem wywołania funkcji Rea d F i l e ma być wskaźnik do bufora. Deklaracja obu tych importowanych funkcji jest bardzo prosta: 4

Ang. unsafe

724

I

-

przyp. tłum.

Rozdział 19. Współdziałanie z COM i Win32

[Dl l lmport ( " kernel 32 " , Set las t Error=true) ] s t at i c extern unsafe i nt Create F i l e ( s t r i ng fi l ename , u i nt des i redAcces s , u i nt shareMode , u i nt attri butes , u i nt creat i on D i spos i t i on , u i nt fl agsAndAt t r i butes , u i nt templ ateFi l e) ; [Dl l lmport ( " kernel 32 " , Set las t Error=true) ] s t at i c extern unsafe bool ReadFi l e ( i nt h Fi l e , voi d* l pBuffer , i nt nBytesToRead , i nt* nBytes Read , i nt overl apped) ;

Utworzymy też nową klasę A P I F i l eReader, której konstruktor będzie wywoływał funkcję Create 4 F i l e. W wywołaniu konstruktora będziemy podawali nazwę pliku, która następnie zostanie przekazana do wywołania funkcji Create F i l e: publ i c A P I F i l eReader (stri ng fi l ename) { fi l eHandl e = Create Fi l e ( fi l ename , llfilename Generi cRead , li desiredAccess UseDefaul t , li shareMode UseDefaul t , li attributes Open Ex i s t i ng , li creationDisposition UseDefaul t , liflagsAndAttributes UseDefau l t ) ; li templateFile

Oprócz tego klasa A P I F i l eReader implementuje tylko jedną dodatkową metodę, Read, która będzie wywoływać systemową funkcję Rea d F i l e. W wywołaniu tej funkcji będziemy przekazywać uchwyt pliku utworzony w konstruktorze, wskaźnik do bufora, liczbę bajtów do odczytania oraz wskaźnik do zmiennej, w której zostanie zapisana liczba odczytanych bajtów. Nas w tym przykładzie najbardziej interesuje wskaźnik do bufora. Aby wywołać tę funkcję, będziemy musieli użyć wskaźnika. Ponieważ do bufora będziemy się odwoływali przy użyciu wskaźnika, musi on być „przypięty" (ang. pinned) w pamięci - wskaźnik ten przekażemy do wywołania funkcji Rea d F i l e, a zatem nie możemy dopuścić, by przed jej zakończeniem mechanizm odzyskiwania pamięci .NET Framework przeniósł bufor w inne miejsce. (Zazwyczaj mechanizm ten przez cały czas prze­ nosi elementy z jednego miejsca w drugie dla zoptymalizowania użycia pamięci) . W tym celu musimy skorzystać z instrukcji fi xed dostępnej w C#. Słowo to pozwala pobrać wskaźnik do bloku pamięci przydzielonego buforowi i oznaczyć go w taki sposób, by mechanizm odzyski­ wania go nie przeniósł. Takie „przypinanie" obniża efektywność mechanizmu odzyskiwania pamięci. Jeśli używane mechanizmy współdziałania zmuszają nas do korzystania ze wskaźników, to należy dołożyć wszelkich starań, by jak najbardziej skrócić czas stosowania takich „przypiętych" danych. To kolejny powód, by unikać wskaźników i stosować je tylko wtedy, gdy jest to absolutnie konieczne.

VVskaźniki

I

725

Blok instrukcji umieszczony po słowie kluczowym fi xed wyznacza zakres, w którym pamięć będzie „przypinana" . Po zakończeniu bloku fi xed pamięć zmiennej zostanie „odpięta", a mecha­ nizm odzyskiwania będzie mógł ją dowolnie przemieszczać. Takie postępowanie jest określane jako przypinanie deklaratywne (ang. declarative pinning) . publ i c unsafe i nt Read (byte [] buffe r , i nt i ndex , i nt count) { i nt byte s Read O; fi xed (byte* bytePo i nter buffer) { ReadFi l e ( fi l eHandl e , bytePo i nter + i ndex , coun t , &bytes Read , O) ; =

=

return bytes Read ;

Czytelnik może się zastanawiać, dlaczego nie musieliśmy „przypinać" zmiennej byt esRead także w jej przypadku funkcja Rea d F i l e wymaga przekazania wskaźnika . Otóż nie było to potrzebne, gdyż zmienna byt esRead jest przechowywana na stosie, a nie na stercie, a zatem mechanizm odzyskiwania pamięci nigdy nie spróbuje jej przenieść. C# wie o tym i dlatego pozwala nam użyć operatora & do pobrania wskaźnika do tej zmiennej bez konieczności sto­ sowania instrukcji fi xed . Gdybyśmy jednak spróbowali użyć tego samego operatora w celu pobrania wskaźnika do zmiennej typu i nt będącej składową obiektu, to kompilator zgłosiłby błąd, informując nas o konieczności użycia instrukcji fi xed. Koniecznie należy się upewnić, że pamięć nie zostanie „odpięta" zbyt szybko. W nie­ których przypadkach wskaźnik przekazany do funkcji będzie używany jeszcze po jej zakończeniu. Na przykład funkcja ReadFi l eEx należąca do Win32 API może działać w spo­ sób asynchroniczny - jej wywołanie może się zakończyć, zanim zostaną zwrócone dane odczytane z pliku. W takim przypadku używany bufor musiałby być „przypięty" aż do momentu zakończenia operacji, a nie jedynie do chwili zakończenia wywoła­ nia funkcji.

Trzeba także zwrócić uwagę, że nasza metoda musi być oznaczona przy użyciu modyfikatora unsafe . Pozwoli to na utworzenie niebezpiecznego kontekstu, w którym będzie można two­ rzyć wskaźniki i ich używać - bez tego kompilator nie pozwoli ani na stosowanie wskaźni­ ków, ani na używanie instrukcji fi xed. W rzeczywistości kompilator tak gorliwie stara się nas zniechęcić do stosowania niebezpiecznego kodu, że musimy go prosić o to dwa razy: użycie słowa kluczowego unsafe spowoduje zgłoszenie przez niego błędu, jeśli nie została jednocze­ śnie zaznaczona opcja kompilatora /un safe. Aby ją znaleźć w Visual Studio, należy wyświetlić panel właściwości projektu. Gdy przejdziemy na kartę Build, znajdziemy na niej pole wyboru Allow unsafe code przedstawione na rysunku 19 .7. Pl atfo rm target:

rł] Allow umafe code

Rysunek 19.7. Włączanie możliwości stosowania niebezpiecznego kodu

726

I

Rozdział 19. Współdziałanie z COM i Win32

Program testowy przedstawiony na listingu 19.3 tworzy obiekty API Fi l eReader oraz ASC I I En cod ei ng. Przekazuje on nazwę pliku (houndofB.txt) do konstruktora klasy API F i l eReader, a następnie uruchamia pętlę, która wypełnia bufor, wywołując jej metodę Rea d . Ta z kolei wywołuje sys­ temową funkcję Rea d F i l e. W efekcie uzyskujemy tablicę bajtów, którą następnie konwertujemy na łańcuch znaków, używając do tego celu metody GetStri ng obiektu ASC I I Encod i ng. Łańcuch ten zostaje w dalszej kolejności przekazany w wywołaniu metody Con so l e . Wri te, która go wyświetla. (Podobnie jak w poprzednim przykładzie, gdzie korzystaliśmy z funkcji Move F i l e, także i tu w praktyce na pewno użylibyśmy odpowiednich zarządzanych klas i metod dostępnych w bibliotece .NET Framework i zdefiniowanych w przestrzeni nazw System . IO. Ten przykład ma jedynie demonstrować techniki programistyczne pozwalające na korzystanie ze wskaźników) . '

. '

Tekst, który zostanie wyświetlony, jest fragmentem opowiadania sir Arthura Conan Doyle'a Pies Baskervillów, które aktualnie jest publicznie dostępne i które można sko­ piować ze strony Projektu Gutenberg (http://www.gutenberg.org/) .

Listing 19.3. Stosowanie wskaźników w programie C# us i ng Sys tem ; us i ng Sys tem . Runt i me . InteropServ i ces ; us i ng Sys tem . Text ; namespace U s i ngPoi nters { cl ass API Fi l eReader { con s t u i nt Generi cRead = Ox80000000 ; con s t u i nt Open Ex i s t i ng = 3 ; con s t u i nt UseDefau l t = O ; i nt fi l eHandl e ; [Dl l lmport ( " kernel 32 " , SetlastError = true) ] stat i c extern uns afe i nt Create Fi l e ( s t r i ng fi l ename , u i nt des i redAcces s , u i nt shareMode , u i nt attri butes , u i nt creat i on D i spos i t i on , u i nt fl agsAndAttri butes , u i nt templ ateFi l e) ; [Dl l lmport ( " kernel 32 " , SetlastError stat i c extern uns afe bool Read Fi l e ( i nt h F i l e , voi d* l pBuffe r , i nt nBytesToRead , i nt* n Bytes Read , i nt overl apped) ;

=

true) ]

li Konstruktor otwiera istniejący plik li i ustawia wartość składowej fileHandle. publ i c API Fi l eReader (stri ng fi l ename) { fi l eHandl e = Create F i l e ( fi l ename , llfilename Generi cRead , li desiredAccess UseDefaul t , li shareMode

VVskaźniki

I

727

UseDefaul t , li attributes Open Ex i s t i ng , li creationDisposition UseDefau l t , liflagsAndAttributes UseDefau l t) ; li templateFile publ i c uns afe i nt Read (byte [] buffe r , i nt i ndex , i nt count) { i nt byt e s Read = O ; fi xed (byte* byte Poi nter = buffer) { ReadFi 1 e ( fi l eHandl e , li hfile bytePoi nter + i ndex , li lpBuffer coun t , li nBytesToRead &bytesRead , li nBytesRead O) ; li overlapped }

return bytes Read ;

cl ass Test publ i c stat i c vo i d Mai n () { li Utworzenie obiektu klasy APIFileReader li i przekazanie do niego nazwy istniejqcego pliku API Fi l e Reader fi l eReader = new A P I F i l eReader ( " houndofB . txt " ) ;

li Utworzenie bufora i obiektu kodującego w kodzie ASCII cons t i nt BuffS i ze = 128 ; byte [] buffer = new byte [BuffS i ze] ; ASC I I Encodi ng asci i Encoder = new ASC I I Encod i ng () ; li Wczytanie pliku do bufora i wyświetlenie go w oknie konsoli wh i l e (fi l eReader . Read (buffer , O , BuffS i ze) ! = O) { Consol e . Wri te ( " { O } " , a s c i i Encoder . GetStri ng (buffer) ) ;

Kluczowy fragment kodu, w którym tworzymy wskaźnik do bufora i używamy instrukcji fi xed, został wyróżniony pogrubioną czcionką . Wykonanie powyższego programu spowoduje wyświetlenie całkiem sporej ilości tekstu, dlatego też poniżej przedstawiliśmy jedynie jego fragment. Kochany Hol mes ! Moj e poprzedn i e l i s ty i depes ze powi adami ały ci ę dokładn i e i s zczegółowo o wszys t k i em , co s i ę dz i ało w tym pon urym z a kąt ku . Im dłużej t u bawi ę , tern bardz i ej czuj ę s i ę smutny i zan i epo koj ony . Te bagna rzucaj ą c i eń na duszę . Zdaj e mi s i ę , żem s i ę przen i ós ł n i etyl ko do i nnego kraj u , al e i w i nną epokę , ż e żyj ę w czasach przedh i s torycznych .

728

Rozdział 19. Współdziałanie z COM i Win32

Rozszerzen ia składn i C# 4.0 Wcześniej w tym rozdziale dowiedzieliśmy się, że C# zapewnia możliwość umieszczania infor­ macji o typach współdziałania w kodzie, dzięki czemu możemy uniknąć konieczności stoso­ wania głównych podzespołów współdziałania . Jak się przekonaliśmy, możliwość ta nie ma żadnego wpływu na składnię języka, a jedynie ułatwia nam życie podczas instalowania aplikacji. Jednak w C# 4.0 pojawiło się kilka nowych możliwości, których zmodyfikowana składnia lepiej nadaje się do wykorzystania w rozwiązaniach korzystających z mechanizmów współdziałania.

Właściwości i ndeksowane Załóżmy, że musimy użyć następującego kodu C#: someObj ect . MyProperty [ " foo "]

=

42 ;

Przed pojawieniem się C# 4.0 taką instrukcję można było interpretować tylko w jeden sposób: powyższy kod pobierał właściwość MyProperty, a następnie używał indeksatora zwróconego obiektu, by przypisać liczbę 42 elementowi zwróconemu przez ten indeksator dla wartości " foo " . Pamiętajmy, że właściwości to jedynie ukryte wywołania metod, a zatem powyższy kod jest odpowiednikiem następującego: someObj ect . get_MyProperty () . set_ I tem ( " foo " , 42) ;

W C# metody służące w indeksatorach do pobierania wartości elementu oraz jej ustawiania są odpowiednio nazywane get_I tem oraz set_I tem.

Niestety, właściwości niektórych obiektów COM działają nieco inaczej - są one nazywane właściwościami indeksowanymi (ang. indexed properties) . O ile w C# indeksatory są elementami skojarzonymi z typami, to w technologii COM można je tworzyć dla poszczególnych właściwo­ ści komponentów. Podobnie jak w C#, także w technologii COM właściwości są tak naprawdę jedynie wywołaniami metod, jednak w przypadku właściwości indeksowanych jawny kod odwołania do nich wyglądałby podobnie do poniższego. someObj ect . set_MyProperty ( " foo " , 42) ;

Właściwości indeksowane wymagają stosowania mniejszej liczby obiektów. Tradycyjna inter­ pretacja stosowana w języku C# wymaga, by właściwość MyProperty zwracała unikalny obiekt udostępniający indeksator, za pomocą którego można odwołać się do interesującej nas warto­ ści . Jednak w przypadku stosowania właściwości indeksowanych używanie tego pośredniego obiektu nie jest konieczne - obiekt s omeObj ect udostępnia akcesory, przy użyciu których, możemy się odwoływać do wartości bezpośrednio. Przed pojawieniem się C# 4.0 jedynym sposobem korzystania z właściwości indeksowanych było użycie składni wywołania metody. Teraz można jednak stosować także składnię indeksatorów, która sprawia, że kod wygląda bardziej naturalnie, gdyż twórcy komponentów COM planowali używać właściwości indeksowanych właśnie w taki sposób.

Rozszerzenia składni C# 4.0

I

729

'

. .

C# 4.0 zapewnia możliwość korzystania z właściwości indeksowanych, nie pozwala jednak ich tworzyć. Projektanci C# nie chcą wprowadzać zamętu, udostępniając w języku dwa sposoby implementacji tego samego rozwiązania. Istnieje tylko jeden sposób tworzenia właściwości, z których można korzystać przy użyciu składni indeksatora, dzięki czemu programista piszący kod musi podjąć o jedną decyzję mniej . Wsparcie dla właściwości indeksowanych zostało dodane wyłącznie w celu ułatwienia tworze­ nia kodu wykorzystującego mechanizmy współdziałania.

Opcjonal ny modyfi kator ref Jak przekonaliśmy się w rozdziale 18., niektóre komponenty COM mają metody, których para­ metry zostały zadeklarowane jako ref obj ect, co oznacza, że są to referencje do referencji do obiektu. Prowadzi to do powstawania dosyć paskudnego kodu takiego jak ten przedstawiony na listingu 19.4.

Listing 19.4. Brzydka strona referencji obj ect fi l eName = @ "Word Fi l e . docx " ; obj ect mi ss i ng = Sys tem . Refl ect i on . M i s s i ng . Val ue ; obj ect readOn l y = t rue ; var doc = wordApp . Documents . Open (ref fi l eName , ref mi s s i ng , ref readOn l y , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref m i s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng , ref m i s s i ng , ref mi s s i ng , ref mi s s i ng , ref mi s s i ng) ;

Powtarzający się fragment ref mi s s i ng oznacza, że należy przekazać referencję do obiektu zapi­ saną w zmiennej mi s s i ng i jednocześnie zapewnić metodzie możliwość modyfikacji tej zmiennej, tak by odwoływała się do innego obiektu lub, jeśli tak zechce, do wartości n ul l . W przypadku korzystania z bibliotek komponentów COM takie rozwiązania są stosowane bardzo często, gdyż zapewniają dużą elastyczność, jednak w C# 3.0 stosowanie ich wiązało się z pisaniem niezbyt miło wyglądającego kodu. Na szczęście w C# 4.0 podczas korzystania z typów współdziałania modyfikator ref jest opcjonalny, co oznacza, że ostatni wiersz z lis­ tingu 19.4 można zmodyfikować, tak jak pokazaliśmy to na listingu 19.5 .

Listing 19.5. Pominięcie modyfikatora ref var doc = wordApp . Documents . Open (fi l eName , mi ss i ng , readOn l y , mi s s i ng , mi s s i ng , mi s s i ng , mi ss i ng , mi s s i ng , mi s s i ng , mi s s i ng , mi s s i ng , mi ss i ng , mi s s i ng , mi s s i ng , mi s s i ng , mi s s i ng) ;

To rozwiązanie jest znacznie lepsze, ale możemy pójść jeszcze dalej . W C# 4.0 dodano obsługę argumentów opcjonalnych - w metodzie można zdefiniować domyślą wartość argumentu, która zostanie użyta, jeśli argument ten nie zostanie jawnie podany w wywołaniu. Możliwość ta sama w sobie nie na wiele by się nam w tym przypadku przydała, gdyż ogólnie rzecz biorąc, C# nie pozwala, by argumenty oznaczane modyfikatorem ref były opcjonalne. Niemniej jednak skoro zdecydowano, że modyfikator ref może być opcjonalny - tak jak to ma miejsce w roz­ wiązaniach związanych ze współdziałaniem - to także same argumenty mogą być opcjonalne, jeśli tylko będą miały wartość domyślną. A ponieważ PIA zawierający obiekty umożliwiające korzystanie z programu Word określa domyślne wartości dla wszystkich argumentów metod używanych przez nas w tym przykładzie, możemy zredukować całe wywołanie do postaci przedstawionej na listingu 19.6 .

730

I

Rozdział 19. Współdziałanie z COM i Win32

Listing 19.6. Argumenty opcjonalne var doc = wordApp . Documents . Open ( Fi l eName : fi l eName , ReadOn l y : readOn l y) ;

Zastosowaliśmy tu nazwane argumenty, gdyż nie są one podawane w ściśle określonej kolej­ ności - chcemy podać tylko pierwszy i trzeci z nich, dlatego też musimy jasno określić, o które nam chodzi. Nazwane argumenty zostały opisane w rozdziale 3 . ••

. .·

.._,..�;

.

L-------11.J"' '

Jak pokazuje przykład z listingu 19.6, możliwość stosowania opcjonalnych argumentów z modyfikatorem ref rozwiązuje wiele problemów, które w rozdziale 18. skłoniły nas do stosowania typu dynami c. Obecnie w języku C# niektóre problemy związane ze współdziałaniem można rozwiązywać na wiele sposobów, pojawia się więc pytanie, który z nich wybrać. Cóż, typ dynami c nabiera szczególnego znaczenia w przypadkach, gdy brakuje nam informacji o typach . Czasami API korzystające z automatyzacji COM nie dostarczają nam dostatecznej ilości danych w czasie kompilacji kodu, co ozna­ cza, że musimy posługiwać się właściwościami, o których wiemy tylko tyle, że są typu obj ect. W takich sytuacjach zastosowanie typu dynami c jest najlepszym możliwym rozwiązaniem. Z drugiej strony statyczne typowanie pozwala nam korzystać z Intelli­ Sense i zapewnia lepszą kontrolę typów w czasie kompilacji kodu. Dlatego też tam, gdzie jest to tylko możliwe, warto korzystać właśnie z typowania statycznego.

Podsu mowan ie Czasami będziemy musieli korzystać z komponentów lub interfejsów API, które nie zostały stworzone z myślą o stosowaniu ich w programach na platformę .NET. Dzięki usługom współ­ działania .NET Framework w programach C# możemy używać zarówno komponentów COM, jak i bibliotek Win32 DLL. Visual Studio zapewnia dodatkowe możliwości ułatwiające korzystanie z kontrolek ActiveX, dzięki czemu stosowanie ich w aplikacjach Windows Forms staje się bezproblemowe. Świat kodu niezarządzanego zmusza nas jednak czasami do stosowa­ nia niebezpiecznych bezpośrednich odwołań do pamięci. Aby można było z nich korzystać, C# udostępnia wskaźniki takie jak te znane z języka C . Zdecydowanie odradzamy stosowanie ich gdziekolwiek indziej niż w rozwiązaniach wykorzystujących mechanizmy współdziałania.

Podsumowanie

731

732

I

Rozdział 19. Współdziałanie z COM i Win32

ROZDZIAŁ 20.

WPF i Silverl ight

WPF oraz Silverlight są powiązanymi ze sobą technologiami służącymi do tworzenia interfejsu użytkownika aplikacji .NET. Choć zostały one opracowane z myślą o stosowaniu ich w nieco odmiennych sytuacjach, to jednak posiadają tak wiele cech wspólnych, że opisywanie ich w jed­ nym rozdziale jest ze wszech miar uzasadnione - niemal wszystkie zamieszczone tu infor­ macje odnoszą się zarówno do WPF, jak i do Silverlight. Zgodnie z tym, co sugeruje nazwa WPF - Windows Presentation Foundation - technologia ta służy do tworzenia interaktywnych aplikacji działających w systemie Windows. Aplikacje WPF działają zazwyczaj jako standardowe aplikacje Windows i wymagają odpowiedniego zain­ stalowania na komputerze docelowym, co wynika z faktu, że do prawidłowej pracy mogą potrzebować uprzedniego zainstalowania innych zasobów lub oprogramowania. (WPF jest technologią platformy .NET, a zatem aplikacje WPF mogą działać wyłącznie wtedy, gdy na komputerze będzie zainstalowany .NET Framework) . Oznacza to jednocześnie, że aplikacje te są uruchamiane tak jak klasyczne aplikacje systemu Windows. Niemniej jednak WPF pozwala aplikacjom wykorzystywać potencjał graficzny nowoczesnych komputerów na sposoby, które niezwykle trudno byłoby uzyskać, korzystając ze standardowych dla systemu Windows tech­ nologii tworzenia interfejsów użytkownika. Aplikacje WPF wcale nie muszą wyglądać podobnie do tradycyjnych aplikacji Windows. Z kolei technologia Silverlight służy do tworzenia aplikacji internetowych, a konkretnie rzecz biorąc, tak zwanych aplikacji RIA - Rich Internet Applications (bogatych aplikacji interneto­ wych) . Aplikacje tego typu nie wymagają pełnej wersji .NET Framework - potrzebują jedynie specjalnej wtyczki do przeglądarki stanowiącej lekką i niezależną od niej wersję platformy .NET. Pełne środowisko uruchomieniowe Silverlight ma wielkość około 5 MB, natomiast pełna wersja .NET Framework - powyżej 200 MB 1 . Co więcej, instalacja Silver light zabiera parę sekund, a nie parę minut. Po zainstalowaniu wtyczki zawartość Silverlight jest pobierana jako element strony WWW, tak jak zawartość AJAX lub Flash, a aplikacje Silverlight nie wyma­ gają żadnej instalacji . (Podobnie jak aplikacje Adobe AIR bazujące na technologii Flash, także aplikacje Silverlight można wykonywać poza przeglądarką WWW, o ile tylko zostaną one wcześniej pobrane na lokalny komputer i użytkownik wyrazi zgodę na ich uruchomienie) . 1 Zazwyczaj nie ma potrzeby pobierania pełnej wersji .NET Framework - internetowy instalator może okre­

ślić, których spośród wymaganych elementów platformy brakuje na komputerze docelowym. Jednak nawet w takim przypadku wielkość wtyczki Silverlight stanowi około jednej piątej wielkości najmniejszej pobieranej wersji pełnej platformy .NET.

733

Niemniej jednak, ponieważ Silverlight zawiera wybrane elementy platformy .NET, można z nich skorzystać, by używając języka C#, pisać aplikacje działające w większości przeglądarek, i to zarówno w systemie Windows, jak i w Mac OS X. '

r.

.



.

L---LI"'.

:

W czasie gdy powstawała ta książka, firma Microsoft nie pracowała nad wtyczką Silverlight dla systemu Linux. Niemniej jednak otwarty projekt Moonlight udostępnia wersję Silverlight zgodną z tym systemem. Moonlight bazuje na projekcie Mono otwartej wersji języka C# oraz platformy .NET przeznaczonej dla wielu różnych sys­ temów operacyjnych innych niż Windows, w tym także dla systemu Linux. Firma Microsoft służyła pewnym wsparciem podczas tworzenia projektu Moonlight, pomagając jego twórcom uzyskać zgodność z wersją Silverlight działającą w systemie Windows. Trzeba jednak pamiętać, że projekt Moonlight zawsze pozostawał nieco w tyle za Silverlight - w czasie powstawania tej książki jego oficjalna wersja jest o dwa główne numery niższa od oficjalnej wersji technologii Silverlight. Jeśli zatem tworzona aplikacja Silverlight ma działać także na komputerach z systemem Linux, opóźnienie to będzie ograniczało jej możliwości.

Pomijając całkowitą odmienność środowisk, w jakich działają aplikacje WPF oraz Silverlight, technologie te mają jednak bardzo wiele wspólnego. W obu układ oraz struktura interfejsu użytkownika są definiowane przy użyciu języka znaczników o nazwie XAML. Interfejsy API obu technologii są wystarczająco podobne, by zapewnić możliwość napisania jednego kodu, który następnie będzie można skompilować zarówno do postaci aplikacji WPF, jak i Silverlight. W obu technologiach występują niezwykle ważne koncepcje takie jak system wiązania danych oraz szablony, które należy zrozumieć, by móc wydajnie tworzyć aplikacje z ich użyciem. Stwierdzenie, że Silverlight stanowi podzbiór WPF, nie jest precyzyjne, co nie powstrzymuje wcale niektórych osób przed jego wygłaszaniem; takie sformułowania pojawiają się nawet czasami w wypowiedziach i materiałach firmy Microsoft. Są one jednak całkowicie niepraw­ dziwe: WPF posiada wiele możliwości, które nie są dostępne w Silverlight, choć także Silver­ light posiada pewne cechy, których nie znajdziemy w WPF. A zatem nie można powiedzieć, że którakolwiek z tych technologii jest podzbiorem tej drugiej . Nawet gdybyśmy przyjęli bardzo luźną interpretację słowa „podzbiór", to takie określenie związku pomiędzy tymi dwiema technologiami byłoby dosyć mylące . Choć obie udostępniają bliźniacze możliwości, to jednak nie zawsze działają one w dokładnie taki sam sposób. Wystarczy kilka minut spędzonych przy jakimś dekompilatorze, takim jak Reflector lub ILDASM, by dobitnie się przekonać, że pod zewnętrzną powłoką kryją one całkowicie odmienne zawartości . A zatem jeśli chodzi nam po głowie pomysł napisania aplikacji, która będzie działać zarówno w przeglądarce jako aplikacja Silverlight - jak i jako niezależna aplikacja WPF, to koniecznie powinniśmy dobrze zrozumieć treść poniższego ostrzeżenia. Choć można napisać jeden kod, który będzie używany zarówno w aplikacji Silverlight jak i WPF, to jednak nie uzyskujemy tej możliwości automatycznie. Przed uruchomie­ niem kodu aplikacji Silverlight w aplikacji WPF najprawdopodobniej trzeba w nim będzie wprowadzić kilka modyfikacji. Jeśli z kolei dysponujemy kodem aplikacji WPF, to przed zastosowaniem go w aplikacji Silverlight trzeba będzie zapewne zmie­ nić jego znaczne fragmenty.

Kod stosowany w aplikacjach WPF i Silverlight zazwyczaj korzysta z kompilacji warunkowej, tzn. dzięki dyrektywom preprocesora C# - #i f, #el se oraz #endi f - w tych miejscach, w których

734

I

Rozdział 20. WPF i Silverlight

pojawiają się różnice, w jednym pliku źródłowym są umieszczane dwie różne wersje tego samego kodu. Oznacza to, że proces pisania aplikacji i jej testowania musi się odbywać rów­ nocześnie dla obu platform docelowych: WPF i Silverlight. W praktyce rzadko pojawia się konieczność pisania jednego kodu, który będzie działał w obu środowiskach. Mogłoby to być przydatne, gdybyśmy pisali komponenty interfejsu użytkow­ nika nadające się do wielokrotnego stosowania z zamiarem używania ich w wielu aplikacjach. Jednak konkretna aplikacja będzie zazwyczaj przeznaczona do działania na jednej platformie bądź to WPF, bądź Silverlight - zależnie od tego, gdzie tę aplikację będziemy chcieli wdrażać. Przykłady zamieszczone w tym rozdziale będą korzystały z technologii Silverlight, jednak ich odpowiedniki korzystające z WPF byłyby bardzo podobne. Wyraźnie zaznaczymy te frag­ menty, w których kod aplikacji WPF byłby znacząco odmienny. Zaczniemy od przedstawienia jednej z najważniejszych cech wspólnych dla aplikacji WPF i Silverlight.

XAML i kod ukryty XAML jest językiem znacznikowym bazującym na XML-u, którego można używać do two­ rzenia interfejsu użytkownika aplikacji. Nazwa XAML stanowiła niegdyś akronim - skrót pochodzący od słów eXtensible Application Markup Language (rozszerzalny język znaczników aplikacji) - jednak jak to się często zdarza, z niejasnych powodów marketingowych nie jest już uważana za skrót. Zresztą szczerze mówiąc, akronimy są zwykle wymyślane z wykorzy­ staniem techniki inżynierii wstecznej - zaczyna się od analizy listy nieużywanych i możliwych do wymówienia kombinacji trzech lub czterech liter (swoją drogą, XAML wymawia się jako „zammel") i próbuje się dopasować do nich potencjalne znaczenie. Ponieważ etymologia nie jest nam w stanie wyjaśnić przeznaczenia języka XAML, przyjrzyjmy mu się na przykładzie . Jak zwykle gorąco zachęcamy, by Czytelnik podczas lektury tworzył przykładową aplikację w Visual Studio. W tym celu konieczne będzie utworzenie nowego projektu Silverlight. W oknie dialogowym New Project w sekcji Visual C# dostępna jest cała grupa szablonów projektów Silverlight. Należy z niej wybrać szablon o nazwie Silverlight Application. (Jeśli Czytelnik woli utworzyć aplikację WPF, to może znaleźć odpowiedni szablon w grupie Windows w sekcji Visual C# okna dialogowego New Project, jednak w takim przypadku nie­ które szczegóły tworzonych aplikacji mogą się różnić od informacji zamieszczonych w tym rozdziale) . W przypadku tworzenia aplikacji Silverlight Visual Studio zapyta, czy chcemy utworzyć projekt nowej aplikacji internetowej, w ramach której będzie ona działać. (Jeśli nową aplikację Silverlight dodamy do solucji, która już zawiera projekt aplikacji internetowej, to Visual Studio zaoferuje także możliwość skojarzenia nowej aplikacji z tą istniejącą) . Aplikacje Silver light działają w przeglądarkach WWW (przynajmniej początkowo), a zatem do uruchomienia naszego kodu będziemy potrzebowali jakiejś strony WWW. W zasadzie by uruchomić aplikację Silver­ light, nie trzeba wcale tworzyć całej aplikacji internetowej, gdyż jeśli się na to nie zdecydujemy, to podczas uruchamiania lub testowania aplikacji Visual Studio będzie generować zwyczajną stronę WWW. Niemniej jednak projekty Silverlight stanowią zazwyczaj jeden z elementów aplikacji internetowych, dlatego też w takich przypadkach będziemy chcieli, by w naszej solucji znalazły się oba rodzaje projektów.

XAML i kod ukryty

I

735

'

..

W przypadku tworzenia aplikacji WPF żaden dodatkowy projekt nie byłby tworzony, gdyż aplikacje WPF są niezależnymi programami systemu Windows.

Kiedy Visual Studio utworzy już nowy projekt, wyświetli plik o nazwie MainPage.xaml. Jest to plik XAML definiujący wygląd oraz układ interfejsu użytkownika . Początkowo zawiera on tylko kilka elementów: element pełniący rolę elementu głównego (w aplikacji WPF będzie to element ) oraz umieszczony wewnątrz niego element . Do tego interfejsu dodamy kilka nowych elementów, byśmy mogli prowadzić jakąś interakcję z naszą aplikacją. Listing 20.1 przedstawia kod źródłowy pliku XAML generowanego domyślnie podczas tworze­ nia nowej aplikacji Silverlight, w którym dodaliśmy dwa nowe elementy: Button oraz Text Bl ock; zmiany wprowadzone w stosunku do oryginalnej zawartości pliku zostały wyróżnione pogru­ bioną czcionką.

Listing 20. 1 . Tworzenie interfejsu użytkownika przy użyciu języka XAML



'

..

Visual Studio wyświetla pliki XAML w widoku podzielonym (Split) . Jego górna część pokazuje, jak będzie wyglądał interfejs użytkownika definiowany przez plik, a dolna - kod źródłowy pliku. Ten ostatni można edytować bądź to bezpośrednio, bądź wybierając elementy z panelu Toolbox i przeciągając je w widoku projektu w gór­ nej części okna. Zmiany wprowadzane w jednej części okna są automatycznie wyświe­ tlane w drugiej .

Aplikację można uruchomić, naciskając klawisz F5. W takim przypadku Visual Studio uruchomi ją na stronie WWW, tak jak to pokazano na rysunku 20 .1 . Nasz prosty przykład aplikacji Silverlight zawiera przycisk, jednak kiedy go klikniemy, nic się nie stanie, gdyż jeszcze nie zdefiniowaliśmy jego działania . W Visual Studio plikom XAML

736

I

Rozdział 20. WPF i Silverlight

� http://localhost:23760/Sim ple5ilverligf P

...

§l C X

,

SimpleSilverli g ht

IK i k n ij m n ie ! J

X

'Tu będzie wyśw i'etl a n y ko m u n i k,at

� 100%

...

Rysunek 20. 1 . Aplikacja Silverlight uruchomiona na stronie WWW Aplikację Silverlight będzie można zobaczyć wyłącznie w przypadku, gdy w prze­ glądarce WWW wyświetlimy odpowiednią stronę aplikacji internetowej . Visual Studio zazwyczaj to robi, po warunkiem że wraz z nową aplikacją Silverlight utworzona została także nowa aplikacja internetowa. Trzeba jednak pamiętać o tym, że jeśli do aplikacji internetowej zostały dodane inne strony, to Visual Studio może wybrać jedną z nich i podczas debugowania możemy nie zobaczyć interfejsu użytkownika aplika­ cji Silverlight. Można nakazać, by środowisko zawsze używało konkretnej strony aplikacji internetowej . W tym celu w panelu Solution Explorer należy kliknąć wybraną stronę prawym przyciskiem myszy i z wyświetlonego menu wybrać opcję Set as Start Page. (Visual Studio tworzy dwie strony startowe dla aplikacji Silverlight: jedną z roz­ szerzeniem aspx oraz drugą z rozszerzeniem html, przy czym obie będą posiadały nazwę odpowiadającą nazwie projektu Silverlight z dodanymi słowami TestPage. Można korzystać z obu tych stron - Visual Studio udostępnia je obie po to, byśmy mogli uruchamiać aplikację Silverlight, korzystając bądź to z dynamicznej strony ASP.NET, bądź to ze statycznego pliku HTML) .

wchodzącym w skład aplikacji WPF lub Silverlight towarzyszą zazwyczaj tak zwane pliki kodu ukrytego (ang. code behind) . Są to pliki źródłowe C# (lub w jakimkolwiek innym używanym języku) zawierające kod skojarzony z danym plikiem XAML. To właśnie w takim pliku kodu ukrytego możemy umieścić kod obsługujący nasz przycisk. Najprostszym sposobem dodania kodu obsługującego kliknięcie przycisku jest skorzystanie z pliku XAML. Wystarczy dwukrotnie kliknąć przycisk wyświetlony w widoku projektu, a Visual Studio doda odpowiednią procedurę obsługi zdarzenia. Okazuje się, że większość elementów interfejsu użytkownika udostępnia szeroki wachlarz zdarzeń, dlatego też możemy potrzebować nieco większej kontroli nad tym, jakie zdarzenie chcemy obsługiwać. Aby ją uzyskać, można zaznaczyć element w widoku projektu, a następnie wyświetlić panel Properties udostępnia on kartę Events, na której jest wyświetlana lista wszystkich dostępnych zdarzeń. Teraz wystar­ czy dwukrotnie kliknąć to zdarzenie, które chcemy obsługiwać. Jeśli jednak lubimy pisać na -

XAML i kod ukryty

I

737

klawiaturze, to procedurę obsługi zdarzenia można także dodać bezpośrednio w kodzie źró­ dłowym XAML. W takim przypadku należy odszukać w nim element Button i zacząć dodawać nowy atrybutu C l i c k . Kiedy wpiszemy cudzysłów otwierający wartości atrybutu, IntelliSense wyświetli okienko z tekstem . Teraz wystarczy nacisnąć klawisz Tab lub Enter, a Visual Studio uzupełni wartość atrybutu, umieszczając z nim tekst myB utton _Cl i c k . Niezależnie od tego, jaki sposób dodawania procedury obsługi zdarzenia wybierzemy, Visual Studio umieści w nim wartość składającą się z nazwy wybranego elementu interfejsu użyt­ kownika (określonej atrybutem x : Name) i zakończoną znakiem podkreślenia i nazwą obsługiwa­ nego zdarzenia:

Jednak wykonanie powyższych czynności spowoduje wprowadzenie zmian nie tylko w pliku XAML - zmodyfikowany zostanie także plik kodu ukrytego, do którego Visual Studio doda odpowiednią metodę. Kod ukryty można wyświetlić, naciskając klawisz F7. Ewentualnie można także odszukać opcję pliku XAML w panelu Solution Explorer i rozwinąć ją. Zobaczymy wtedy kolejny plik z rozszerzeniem xaml.cs - będzie to właśnie plik kodu ukrytego. Listing 20.2 przedstawia metodę obsługującą kliknięcia wraz z dodatkowym kodem, który w niej umie­ ściliśmy (został on wyróżniony pogrubioną czcionką) . (Nie ma obowiązku stosowania właśnie takiej konwencji podczas określania nazw procedur obsługi zdarzeń. Nic nie stoi na prze­ szkodzie, by po utworzeniu takiej procedury przez Visual Studio zmienić jej nazwę, o ile tylko będziemy pamiętać, by dokonać tej zmiany zarówno w pliku XAML, jak i w pliku kodu ukrytego) .

Listing 20.2 . Metoda obsługujqca kliknięcia umieszczona w kodzie ukrytym pri vate voi d myButton-Cl i c k (obj ect sender , RoutedEventArgs e) { mes sageText . Text = "Wi taj , §wi eci e ! " ;

Ponieważ kod XAML odwołuje się do tej metody w atrybucie C l i c k elementu B u t ton, będzie ona wykonywana za każdym razem, gdy użytkownik kliknie ten przycisk. Jedyny wiersz kodu, jaki dodaliśmy wewnątrz metody, odwołuje się do elementu TextBl o c k . Jeśli przeanali­ zujemy kod XAML, zauważymy, że atrybut x : N ame tego elementu ma wartość mes s ageText; to sprawia, że możemy używać tej nazwy w odwołaniach do tego elementu stosowanych w kodzie ukrytym. Kod przedstawiony na listingu 20 .2 ustawia wartość właściwości Text, co - jak Czytelnik się zapewne domyślił - sprawia, że w odpowiedzi na kliknięcie przycisku w elemen­ cie TextBl ock zostaje wyświetlony podany tekst. Kod XAML z listingu 20 .1 oraz kod C# z listingu 20 .2 ustawiają wartość właściwości T ext elementu TextBl oc k . Kod XAML robi to, korzystając ze zwyczajnych atrybutów XML, nato­ miast w kodzie C# używana jest standardowa składnia odwołań do właściwości . Pokazuje to bardzo ważną cechę języka XAML: jego elementy odpowiadają zazwyczaj obiektom, natomiast atrybuty - właściwościom lub zdarzeniom.

738

I

Rozdział 20. WPF i Silverlight

'

Żebyśmy się dobrze zrozumieli: wszystkie te czynności s ą wykonywane po stronie klienta. Wtyczka Silverlight pobiera naszą aplikację, a następnie wyświetla jej inter­ fejs użytkownika zdefiniowany w pliku XAML. Wykonuje także wewnątrz procesu przeglądarki kod ukryty naszej aplikacji Gak również cały inny kod wchodzący w jej skład) i wywołuje procedury obsługi zdarzeń bez konieczności prowadzenia jakiej­ kolwiek dodatkowej komunikacji z serwerem WWW. Aplikacje Silverlight mogą komu­ nikować się z serwerem, kiedy zostaną już pobrane, niemniej jednak - w odróżnieniu od klikania przycisków w formularzach umieszczanych na zwyczajnych stronach WWW - w ich przypadku działania związane z obsługą klikania przycisków nie stwa­ rzają takiej konieczności.

. .

XAML i obiekty Choć XAML jest standardowym sposobem definiowania interfejsów użytkownika w aplika­ cjach WPF oraz Silverlight, to jednak okazuje się, że korzystanie z niego wcale nie jest konieczne. Z listingu 20 .1 moglibyśmy usunąć wyróżniony fragment kodu XAML zawierający elementy B ut ton i TextBl ock i odpowiednio zmodyfikować definicję klasy oraz jej konstruktor umiesz­ czone w pliku kodu ukrytego. Niezbędne modyfikacje przedstawia listing 20 .3.

Listing 20.3. Tworzenie elementów interfejsu użytkownika w kodzie ukrytym publ i c part i al cl a s s Ma i n Page : UserControl { pri vate Button myButton ; pri vate TextBl ock messageText ;

publ i c Mai nPage () { I n i t i al i zeComponent () ; myButton = new Button { Hori zontal Al i gnment = Hori z ontal Al i gnment . Center , Verti cal Al i gnment = Verti calAl i gnment . Top , FontSi z e = 2 0 , Content = " Kl i kni j mn i e ! " }; myButton . Cl i ck += myButton_Cl i ck; messageText = new TextBl ock { Text = " T u będz i e wyświ etl any komun i kat" , TextWrappi ng = TextWrappi ng .Wrap , TextAl i gnment = TextAl i gnment . Center , FontSi ze = 30 , FontWe i ght = FontWei ghts . Bol d , Hori zontal Al i gnment = Hori z ontal Al i gnment . Center , Verti cal Al i gnment = Verti calAl i gnment . Center }; LayoutRoot . Ch i l dren . Add (myButton) ; LayoutRoot . Ch i l dren . Add (mes sageText) ;

XAML i kod ukryty

I

739

Każdy element zawierający atrybut x : N ame został zastąpiony polem, a pola te są inicjowane w konstruktorze. W tym przykładzie skorzystaliśmy ze składni inicjalizatorów obiektów dostęp­ nej w C#, by określić wartości właściwości tworzonych obiektów, a jednocześnie podkreślić strukturalne podobieństwo pomiędzy tym kodem C# oraz kodem XAML, który on zastępuje. Oczywiście równie dobrze można zastosować standardową składnię odwołań do właściwości. Wartości atrybutów XML są zwyczajnym tekstem, natomiast w kodzie C# musimy podawać wartości odpowiednich typów - typów wyliczeniowych, liczb lub łańcuchów znaków zależnie od tego, która z nich będzie w danym przypadku prawidłowa. Kompilator XAML sam określi, w jaki sposób należy przekonwertować tę wartość na odpowiedni typ . (Do tego celu używany jest system TypeConverter wchodzący w skład biblioteki klas .NET Framework) . Oprócz tego, jak Czytelnik zapewne pamięta, C# używa innej składni do określania procedur obsługi zdarzeń, a innej do określania wartości właściwości - w powyższym listingu zasto­ sowaliśmy operator +=, natomiast w kodzie XAML zarówno do określania procedur obsługi zdarzeń, jak i do ustalania wartości właściwości są używane wyłącznie atrybuty. Zastosowanie powyższego kodu da dokładnie taki sam efekt jak użycie przedstawionego wcześniej pliku XAML. W rzeczywistości XAML jest jedynie językiem służącym do tworzenia obiektów oraz określania wartości ich właściwości i procedur obsługi zdarzeń, dlatego też w większości przypadków nie ma większego znaczenia, czy do tworzenia interfejsu użytkow­ nika używamy języka XAML, czy kodu C#. W ten sposób dochodzimy do pytania, po co w ogóle istnieje XAML, skoro ten sam efekt możemy uzyskać, korzystając wyłącznie z C#. Głównym powodem istnienia języka XAML jest chęć zapewnienia możliwości tworzenia interfejsów użytkownika w innych narzędziach niż edytory tekstów. Przykładowo firma Microsoft udostęp­ nia program o nazwie Expression Blend, który wchodzi w skład rodziny programów Expression służących do projektowania. Blend jest właśnie programem do tworzenia interfejsów użytkow­ nika aplikacji WPF i Silverlight działającym głównie z wykorzystaniem języka XAML. Ta separacja jest czymś więcej niż jedynie ukłonem w kierunku osób pragnących pisać pro­ gramy do projektowania . Jest ona czymś wygodnym zarówno dla programistów, jak i dla projektantów. Wymusza ona pewien podział, sprawiając, że projektanci mogą zajmować się wizualnymi aspektami aplikacji bez konieczności stosowania narzędzi pozwalających na edycję kodu źródłowego C#. W rzeczywistości jednak sprawna współpraca pomiędzy programistami i projektantami wymaga czegoś więcej - rozdzielenie plików XAML oraz plików kodu ukry­ tego nie wystarczy, gdyż wciąż stosunkowo łatwo jedni i drudzy mogą sobie wzajemnie wcho­ dzić w drogę. Jeśli programista pisze kod ukryty, którego działanie jest uzależnione od istnie­ nia pewnych elementów interfejsu użytkownika posiadających atrybuty x : N ame o określonych nazwach, a projektant uzna, że są one paskudne, i zastąpi je innymi elementami, lecz zapo­ mni nadać im te same nazwy, to bez wątpienia pojawią się spore problemy. W praktyce dobra współpraca pomiędzy programistami i projektantami wymaga czegoś więcej i bazuje na wyko­ rzystaniu innych możliwości technologii WPF i Silverlight - głównie chodzi tu o szablony, które zostały przedstawione w dalszej części rozdziału. Niemniej jednak XAML jest ważną częścią tego równania . ••

• .·

.._,..�;

.

,

.....___� ,

740

I

Atrybut x : Name jest opcjonalny. W praktyce przeważająca część elementów interfejsu użytkownika tworzonych przy użyciu języka XAML nie posiada nazw - są one określane wyłącznie w tych elementach, które muszą być używane w kodzie ukrytym . Dzięki temu kod XAML staje się mniej zaśmiecony, a jeśli współpracujemy z projektantami, to łatwiej orientują się oni, które elementy interfejsu mają kluczowe znaczenie, a które można spokojnie modyfikować w ramach procesu projektowania.

Rozdział 20. WPF i Silverlight

Równoważność elementów i obiektów sugeruje, że XAML nie musi być wcale używany wyłącz­ nie do tworzenia interfejsu użytkownika . Jego składnia pozwala na tworzenie obiektów .NET niemal dowolnych typów. O ile tylko dany typ udostępnia konstruktor domyślny i może być konfigurowany przy użyciu właściwości oraz odpowiednich konwerterów typów, to jego obiekty będzie można tworzyć na podstawie kodu XAML. Na przykład z technicznego punktu widzenia możliwe jest utworzenie na podstawie pliku XAML interfejsu użytkownika aplikacji Windows Forms. Niemniej jednak stosowanie tego języka do tworzenia obiektów, które nie zostały zaprojektowane z myślą o takim rozwiązaniu, może być kłopotliwe, dlatego w prak­ tyce XAML najlepiej nadaje się do zastosowania w aplikacjach WPF, Silverlight oraz Workflow Foundation, spośród których wszystkie były tworzone z myślą o jego wykorzystaniu.

XAML i JavaScript Wersja 1 .0 technologii Silverlight w ogóle nie obsługiwała platformy .NET. Była ona wyposażona w obsługę języka XAML, jeśli jednak chcieliśmy wykorzystać cokolwiek innego prócz statycznej, nieinteraktywnej zawartości, to konieczne było definiowanie działania aplikacji Silverlight przy użyciu jakiegoś języka skryptowego wykonywanego po stronie przeglądarki. W tym przypadku elementy XAML w żadnym razie nie odpowiadały obiektom .NET, niemniej jednak wciąż odpo­ wiadały one obiektom - kod JavaScript mógł się odwoływać do wszystkich obiektów reprezentu­ jących elementy kodu XAML. Takie rozwiązanie wciąż można stosować w aktualnych wersjach Silverlight - obiektów tworzo­ nych na podstawie kodu XAML można używać w kodzie C#, JavaScript lub w obu jednocześnie. Obsługę języka JavaScript można wykorzystać do tworzenia interaktywnych okienek startowych wyświetlanych podczas oczekiwania na wczytanie właściwego interfejsu użytkownika aplikacji. Niemniej jednak obiekty JavaScript nie są tym samym co obiekty .NET, a to prowadzi do pytania: jakiego rodzaju obiekty są tak naprawd� tworzone na podstawie kodu XAML? Czy są to obiekty .NET opakowane jak obiekty JavaScript, czy też obiekty JavaScript opakowane jak obiekty .NET? Otóż okazuje się, że to zależy. Nie są to rodzime obiekty JavaScript, lecz równocześnie nie zawsze są to rodzime obiekty .NET. W przypadku podstawowych elementów pozbawionych jakichkol­ wiek działań dynamicznych, takich jak kształty graficzne lub bloki tekstu, zarówno obiekty .NET, jak i JavaScript okazują się być opakowaniami dla jeszcze innych obiektów tworzonych wewnątrz wtyczki Silverlight. W przypadku bardziej złożonych obiektów, takich jak przyciski lub listy, są to jednak rzeczywiste obiekty .NET, gdyż ich działanie zostało zaimplementowane jako kod .NET.

Skoro możemy wybierać pomiędzy tworzeniem interfejsu użytkownika w języku XAML bądź w języku C#, to które z tych rozwiązań powinniśmy zastosować? Wykorzystanie XAML jest zazwyczaj łatwiejsze, gdyż do edycji wyglądu i układu interfejsów można używać narzędzi takich jak projektant XAML dostępny w Visual Studio (a nawet program Expression Blend) będzie to zdecydowanie prostsze niż wielokrotne wprowadzanie zmian w kodzie w celu uzy­ skania odpowiedniego wyglądu aplikacji. Oczywiście jeśli przy tworzeniu programu pracują zarówno programiści, jak i projektanci, to XAML będzie zdecydowanie preferowanym roz­ wiązaniem, gdyż zapewni on projektantom możliwość modyfikowania wyglądu aplikacji bez konieczności angażowania programistów do wprowadzania takich zmian. Nawet w przypad­ kach, gdy cały interfejs użytkownika jest tworzony przez programistów, interaktywne narzędzia do jego tworzenia będą stanowiły znacznie bardziej efektywne rozwiązanie niż definiowanie go w kodzie . Nie oznacza to jednak wcale, że należy całkowicie zrezygnować z tworzenia elementów interfejsu użytkownika w taki sposób, zwłaszcza jeśli zastosowanie kodu wydaje się najprostszą metodą rozwiązania problemu. Należy zatem używać tego rozwiązania, które będzie najwygodniejsze w konkretnej sytuacji. XAML i kod ukryty

I

741

Teraz, kiedy już przekonaliśmy się, że XAML jest w rzeczywistości jedynie sposobem two­ rzenia obiektów, zobaczmy, jakie typy obiektów udostępniają technologie WPF i Silverlight.

Elementy i kontrolki Niektóre spośród typów używanych do tworzenia interfejsów użytkownika są elementami interaktywnymi definiującymi własne zachowania. Elementami takimi są na przykład przy­ ciski, pola wyboru oraz listy. Choć elementy tego typu trzeba podłączać do tworzonych apli­ kacji, to jednak dysponują one swoimi własnymi interaktywnymi zachowaniami. Przyciski są na przykład wyróżniane, kiedy umieścimy na nich wskaźnik myszy, a kiedy je klikniemy, wyglą­ dają, jakby zostały naciśnięte, natomiast listy pozwalają na zaznaczanie elementów. Jednak inne elementy są znacznie prostsze. Są to elementy prezentujące kształty graficzne oraz elementy tekstowe, które, choć są widoczne dla użytkownika, to jednak nie dysponują żadnymi inte­ raktywnymi zachowaniami - jeśli oczekujemy od nich czegoś więcej niż tylko tego, by były widoczne, to musimy w tym celu napisać odpowiedni kod. Co więcej, niektóre elementy nawet nie są widoczne bezpośrednio. Na przykład istnieją elementy służące do określania układu, których nie widać, gdyż ich zadanie polega na określaniu miejsca, w jakim należy wyświetlać inne elementy. To, z jakim typem elementu mamy do czynienia, można określić, sprawdzając klasę bazową typu obiektu .NET odpowiadającego elementowi . Większość elementów interfejsu użytkow­ nika bezpośrednio lub pośrednio dziedziczy po klasie Framework E l ement, klasa ta definiuje jed­ nak kilka bardziej wyspecjalizowanych typów pochodnych. Klasą bazową dla elementów służących do określania układu jest Panel . Z kolei elementy związane z graficzną reprezentacją figur płaskich dziedziczą po klasie S h ape. Control jest natomiast klasą bazową dla elementów posiadających jakieś własne interaktywne zachowania. '

. .

'

L---�. '

Oznacza to, ż e nie wszystkie elementy interfejsu użytkownika s ą kontrolkami. W rzeczywistości okazuj e się, że większość z nich nimi nie jest. Skoro to już wyja­ śniliśmy, trzeba dodać, że termin kontrolka jest zazwyczaj stosowany dosyć luźno niektórzy autorzy, a nawet niektóre fragmenty dokumentacji opracowanej przez firmę Microsoft, używają go w odniesieniu do wszelkich elementów interfejsu użyt­ kownika, nawet tych, które nie dziedziczą po klasie Control . Całą sytuację dodatkowo komplikuje to, że istnieje także przestrzeń nazw System.Wi ndows . Control s, lecz nie wszystkie typy w niej zdefiniowane dziedziczą po klasie Control . Zdajemy sobie sprawę z tego, że to wszystko jest dosyć zagmatwane, dlatego też w niniejszej książce będziemy używali terminu kontrolka wyłącznie w odniesieniu do typów dziedziczących po Control . Opisując zagadnienia i możliwości dotyczące wszystkich obiektów interfejsu użytkownika dziedziczących po klasie FrameworkEl ement (czyli również kontrolek), będziemy używali ogólnego terminu element. Trzeba jednak pamiętać, że zarówno w internecie, jak i w innych książkach można napotkać inne, nieco bardziej mylące konwencje.

Zanim zaczniemy zajmować się kontrolkami, przyjrzymy się sposobom rozmieszczania elemen­ tów oraz określania ich wielkości - interaktywne elementy interfejsu użytkownika na nie­ wiele się nam przydadzą, jeśli nie będziemy w stanie precyzyjnie określić, gdzie mają być wyświetlane .

742

I

Rozdział 20. WPF i Silverlight

Panele układów Panel jest abstrakcyjną klasą bazową elementów interfejsu użytkownika kontrolujących układ innych elementów. Interesujący nas mechanizm rozmieszczania wybieramy, używając konkretnej klasy pochodnej tego typu. Trzecia wersja technologii Silverlight udostępnia trzy2 typy paneli: Gri d, StackPanel oraz Canvas. W technologii WPF dostępne są te same trzy typy oraz kilka dodatkowych.

Spośród tych trzech typów paneli największe możliwości posiada panel Gri d . To właśnie z tego powodu Visual Studio, tworząc nowy interfejs użytkownika, automatycznie daje nam do dys­ pozycji jeden taki panel. Zgodnie z tym, co sugeruje nazwa tej klasy3, panele tego typu dzielą swój obszar na wiersze i kolumny, a następnie umieszczają elementy podrzędne wewnątrz utworzonych w ten sposób komórek. Domyślnie panel Gri d składa się z jednego wiersza oraz jednej kolumny, co oznacza, że tworzy jedną dużą komórkę . Oczywiście można ich utworzyć więcej, tak jak to pokazaliśmy na listingu 20 .4. Poniższy listing demonstruje specyficzną cechę języka XAML nazywaną elementem właściwości (ang. property element). Element nie reprezentuje

. .·

'

'

obiektu podrzędnego, który chcemy umieścić w panelu Gri d, informuje natomiast, że chcielibyśmy określić wartość właściwości Col umnDefi ni ti ons obiektu Gri d. Umieszczane wewnątrz niego elementy reprezentują obiekty, które będą dodawane do kolekcji w tej właściwości; w odróżnieniu od elementów , które zostaną dodane do kolekcji przechowywanej we właściwości Chi l dren obiektu Gri d.

Listing 20.4. Siatka składająca się z wierszy i kolumn



Rysunek 20.2 pokazuje wygląd interfejsu użytkownika definiowanego przez powyższy frag­ ment kodu. To, że są cztery wiersze, jest raczej zrozumiałe - każdy z przycisków zostanie umieszczony tylko w jednym wierszu. Z kolumnami sytuacja jest nieco bardziej skomplikowana. W pierwszym wierszu możemy zobaczyć wszystkie trzy kolumny, gdyż w każdej z nich znaj­ duje się dokładnie jeden przycisk, jednak pozostałe dwa wiersze zawierają tylko po jednym przycisku, który rozciąga się na wszystkie trzy kolumny. W końcu w ostatnim, czwartym wier­ szu widzimy tylko jeden przycisk umieszczony w drugiej kolumnie .

I

( 1 1 D)

W i er.sz 1 . ,

(2, D)

3 kolum ny szerokości .

W iersz 2 . , 3 kolum ny szero kości .

Rysunek 20.2 . Elementy podrzędne w panelu Grid Panel Gri d wie, do jakich wierszy i kolumn należą poszczególne elementy oraz jaki obszar zajmują, gdyż każdy z przycisków użytych na listingu 20.4 ma właściwości, które to określają. Przeznaczenie właściwości Gri d . Col umn oraz Gri d . Row dokładnie odpowiada ich nazwom, nato­ miast właściwości Gri d . Col umnSpan oraz Gri d . RowSpan określają, ile komórek zajmuje dany element. Domyślną wartością pierwszych dwóch właściwości jest O, natomiast dwóch kolejnych - 1 . '

.

'

Te właściwości są kolejną specjalną cechą języka XAML określaną jako właściwości dołą­ czone (ang. attached properties). Właściwość dołączona to taka, która została zdefiniowana w innym typie (na przykład w elemencie Gri d) niż ten, w którym została użyta (na przy­

kład element Button). Właściwości dołączone użyte w przykładzie przedstawionym na listingu 20.4 zostały określone jako atrybuty, można je jednak także określać przy użyciu przedstawionej wcześniej składni właściwości elementów. Na przykład gdyby element mógł zawierać element , to moglibyśmy użyć tej składni, by określić dołączoną właściwość Tool T i p zdefiniowaną w klasie Tool Ti pServi ce. Choć Silverlight, WPF oraz XAML pozwalają na stosowanie właściwości, które nie­ koniecznie zostały zdefiniowane w obiekcie, w którym ich wartości są określane, to jednak C# nie dysponuje żadną składnią, która zapewniałaby podobne możliwości. Dlatego też klasy definiujące właściwości dołączone definiują także odpowiednie metody get i set zapewniające możliwość korzystania z tych właściwości w kodzie aplikacji. Za przykład mogą tu posłużyć metody SetCol umn oraz GetCol urn klasy Gri d.

744

I

Rozdział 20. WPF i Silverlight

Wiersze i kolumny panelu Gri d przedstawionego na rysunku 20.2 mają różne wielkości. Wynika to z wartości przypisanych elementom oraz . Właściwość W i d t h pierwszej kolumny ma wartość Auto, a zatem jej szerokość jest określana na podstawie szerokości najszerszego umieszczonego w niej elementu. Pozostałe dwie kolumny mają domyślną szerokość 1 *, co oznacza, że pozostały obszar panelu zostanie równo podzielony pomiędzy nie. Wysokość wierszy określana jest podobnie, z tym że w pierwszym wierszu została podana konkretna wartość 30, przez co wszystkie umieszczone w nim elementy będą miały wysokość 30 pikseli niezależnie od ich zawartości . Wysokość ostatniego wiersza została określona jako Auto, a ponieważ umieszczony w nim element używa dosyć dużej czcionki, także sam wiersz będzie stosunkowo wysoki . Z kolei w dwóch środkowych wierszach używana jest tak zwana notacja z gwiazdką, a zatem, podobnie jak to było w przypadku drugiej i trzeciej kolumny, także i tu oba wiersze będą zajmowały pozostały dostępny obszar panelu. Jednakże ze względu na zastosowanie innych wartości - odpowiednio 1 * oraz 2* - wierszom tym zostaną przydzielone obszary o różnej wysokości. Wiersz, w którym użyliśmy wartości 2*, będzie dwu­ krotnie wyższy od tego, którego wysokość ma domyślną wartość l*. Warto zwrócić uwagę, że w przypadku takiego określania wysokości liczy się jedynie wzajemny stosunek obu wartości - zmiana z 1 * i 2* na 10* i 20* nie spowodowałaby zmiany faktycznej wysokości wierszy, gdyż wartość 20* wciąż jest dwukrotnie większa od 10* . Jak widać, panel G r i d pozwala nadawać swym wierszom i kolumnom określone wymiary, dostosowywać je do wymiarów zawartości lub proporcjonalnie dzielić dostępny obszar. To sprawia, że jest on niezwykle elastycznym mechanizmem określania układu interfejsu użyt­ kownika . Określając wymiary wierszy i kolumn przy użyciu wartości Auto, można tworzyć układy przypominające działaniem mechanizm dokowania, gdzie elementy są wyrównywane do górnej, dolnej, prawej lub lewej krawędzi dostępnego obszaru oraz sprawiając by zajmo­ wały one wszystkie dostępne wiersze w przypadku dokowania do lewej lub prawej krawędzi, bądź wszystkie kolumny w razie dokowania do krawędzi górnej lub dolnej . Stosując kilka wierszy lub kolumn, w których użyto wartości Auto, można układać elementy jeden na drugim lub jeden obok drugiego. Jak mieliśmy się okazję przekonać, można nawet podawać konkretne wymiary elementów i precyzyjnie je rozmieszczać w siatce panelu Gri d . Jedynym drobnym problemem jest to, że kod XAML niezbędny do zdefiniowania tego panelu wraz z jego zawar­ tością może być dosyć rozbudowany. Dlatego też dostępne są również inne, prostsze typy paneli . Panel S t a c k Pa n e l rozmieszcza elementy w pionowych lub poziomych stosach. Listing 20 .5 przedstawia StackPane l , w którym właściwości Ori entat i on przypisana została wartość Vert i cal . Bez wątpienia Czytelnik domyśla się, w jaki sposób utworzyć panel, w którym elementy będą rozmieszczane w poziomie . (W rzeczywistości domyślnym układem wykorzystywanym w panelach StackPanel jest właśnie układ pionowy, a zatem z listingu 20.5 moglibyśmy usunąć właściwość Ori ent a t i on, a działanie panelu nie uległoby zmianie) .

Listing 20.5. Panel StackPanel o układzie pionowym

Elementy i kontrolki

I

745

Wygląd interfejsu użytkownika generowanego przez powyższy kod przedstawia rysunek 20 .3. Warto zwrócić uwagę, że w kierunku, w jakim są grupowane elementy, sposób określania wymiarów wierszy przypomina działanie wartości Auto - są one na tyle wysokie, by pomieścić zawartość. W drugim kierunku elementy zostaną natomiast rozciągnięte, tak by zajmowały całą dostępną szerokość, choć jak się niebawem przekonamy, łatwo można to zmienić.

P rzyc i s ki ułożon e w

stos.

Rysunek 20.3. Panel StackPanel o układzie pionowym Jeszcze prostsze rozwiązanie zastosowano w panelu Canvas - nie korzysta on z żadnej strategii rozmieszczania elementów, a zamiast tego umieszcza je tam, gdzie mu każemy. Panel Gri d udo­ stępnia dołączone właściwości pozwalające określać, w której komórce układu należy umieścić dany element, o czym mieliśmy okazję się przekonać na jednym z poprzednich listingów. Także i panel Canva s udostępnia właściwości dołączone - w jego przypadku są to właściwości Left i Top, a pozwalają one określać współrzędne elementu. Przykład ich użycia przedstawiliśmy na listingu 20 .6 .

Listing 20.6. Podawanie konkretnych współrzędnych elementów w panelu Canvas

Jak pokazuje rysunek 20 .4, możliwość precyzyjnego rozmieszczania, jaką zapewnia nam panel Canvas, pozwala umieszczać elementy w taki sposób, że będą na siebie zachodziły. (Na tym rysunku są także widoczne elementy okna przeglądarki, co pokazuje, że położenie elementów jest określane względem lewego górnego wierzchołka panelu Canvas ) . Koniecznie należy zwró­ cić uwagę, że elementy umieszczane w tym panelu mają wymiary dostosowane do wielkości ich zawartości . Podobnie działały wiersze i kolumny, w których zastosowano wartość Auto, jednak w tym przypadku do zawartości dostosowywane są oba wymiary elementów. Jeśli nie podamy konkretnych wymiarów elementów podrzędnych, to panel Canvas przydzieli im dokład­ nie tyle miejsca, ile potrzeba, by wyświetlić ich zawartość. System określania układu elementów stosowany w technologiach Silverlight oraz WPF jest rozszerzalny, można zatem tworzyć własne klasy pochodne klasy Panel bądź też korzystać z bibliotek udostępniających inne rodzaje paneli. Microsoft udostępnia na przykład Silverlight Toolkit - darmową bibliotekę, którą można pobrać ze strony http://silverlight.codeplex.com/ (zarówno w formie pliku binarnego, jak i kodów źródłowych) i która definiuje przeróżne kontrolki, panele oraz wiele innych użytecznych komponentów. Można wśród nich znaleźć dwa panele utworzone na podstawie paneli dostępnych w WPF. Pierwszym z nich jest Wra p Panel ,

746

I

Rozdział 20. WPF i Silverlight

Rysunek 20.4. Przyciski umieszczone w panelu Canvas który rozmieszcza elementy podrzędne w sposób przypominający prezentowanie tekstu w prze­ glądarce lub edytorze - kolejne elementy są umieszczane jeden przy drugim, w wierszu od lewej strony do prawej, tak długo, dopóki starcza dla nich miejsca, a kiedy go zabraknie, kolejne elementy są umieszczane w następnym wierszu. Drugim z dostępnych paneli jest Doc k Panel . Pozwala on umieszczać elementy w stosach wyrównanych do lewej, prawej, górnej lub dolnej krawędzi jego obszaru. (Doc k Panel nie robi niczego, czego nie moglibyśmy zrobić przy użyciu panelu Gri d, jednak może być nieco prostszy w użyciu) . Określanie układu elementów w technologiach WPF i Silverlight to nie tylko panele. Panele definiują strategię, na podstawie której poszczególnym elementom są przydzielane tak zwane gniazda układu, czyli obszary na ekranie, w których dany element musi się zmieścić. Jednak właściwości są dostępne we wszystkich elementach - niezależnie od tego, w jakim panelu zostaną one umieszczone - i mogą mieć wpływ zarówno na wielkość gniazda układu, jak i na to, co element zrobi z obszarem, który został mu zaoferowany.

Ogól ne właściwości układów Wszystkie elementy posiadają pewne wspólne właściwości mające wpływ na układ. Zaliczają się do nich właściwości Wi dth oraz Hei ght pozwalające nadawać elementom konkretne wymiary, zamiast określać je na podstawie zawartości i dostępnego miejsca. Możliwość ta jest bardzo ważna w przypadku elementów, które nie mają swojej własnej, naturalnej wielkości. Zawartość tekstowa ma takie naturalne wymiary, jednak niektóre elementy graficzne, takie jak E l l i pse lub Rectangl e, ich nie posiadają. Gdybyśmy chcieli utworzyć element E l l i p s e bez określania jego wysokości i umieścić go w panelu StackPanel , to okazałoby się, że element ten zniknie, gdyż panel poprosi go o wyliczenie swojej minimalnej wysokości, a ponieważ nie podaliśmy jej jawnie, będzie nią wartość O. Z tego powodu w elementach nieposiadających naturalnej wielkości zazwyczaj są jawnie podawane wartości właściwości W i d t h i Hei g h t . Ewentualnie można także użyć właściwości M i n W i d t h oraz M i nHei ght, by zapewnić, że element nigdy nie zniknie, lecz w razie potrzeby zostanie rozszerzony, by zająć cały dostępny obszar - w nie­ których układach, gdy użytkownik powiększy okno aplikacji, pojawi się więcej miejsca, a zatem przydatne może być zastosowanie obszaru, który będzie w stanie to dodatkowe miejsce wyko­ rzystać. Z kolei właściwości MaxW i d t h oraz MaxHei ght pozwalają określać maksymalne wymiary elementu. Te różne właściwości dotyczące wymiarów przydają się, gdy element zostanie poproszony o określenie swojej wielkości, co może nastąpić na przykład w sytuacji, gdy zostanie on umieszczony w komórce panelu Gri d, której wielkość jest określona jako Auto. Jednak czasami

Elementy i kontrolki

I

747

zdarza się, że wymiary układu są odgórnie narzucane - na przykład gdy interfejs użytkow­ nika aplikacji Silverlight zostanie skonfigurowany tak, by zajmować cały dostępny obszar okna przeglądarki, to jego wielkość będzie zależeć od użytkownika . W takich sytuacjach, kiedy system układu musi dostosować wszystkie elementy do obszaru o z góry określonej wielkości i nie może określić, ile miejsca potrzeba na jego wyświetlenie, mówimy o układzie ograniczonym. W większości przypadków interfejs użytkownika stanowi mieszankę układu ograniczonego i nieograniczonego - na najwyższym poziomie jest on zazwyczaj ograniczony wymiarami okna, jednak wewnątrz niego mogą być umieszczane elementy takie jak bloki tekstu bądź przyciski, które będą musiały być odpowiednio duże, by cała ich zawartość była widoczna. '

. '

1.---.-1.1"'

,

Kiedy nie podamy wysokości i szerokości elementów, które nie posiadają żadnej naturalnej wielkości, i zostaną one umieszczone w układzie ograniczonym, to zajmą cały przydzielony im obszar. Na przykład jeśli jako jedyny, główny element panelu Gri d zastosujemy element El l i pse i nie określimy jego właściwości Wi dth i Hei ght, to wypełni on cały obszar panelu.

Czasami można nawet uzyskać połączenie układu ograniczonego i nieograniczonego w jed­ nym elemencie. Rysunek 20 .3 przedstawiał pionowy stos elementów, których wysokość była dostosowana do ich zawartości, a ponieważ elementy te mogły określać swoją wysokość dowol­ nie, to w kierunku pionowym ich układ nie był ograniczony . Niemniej jednak szerokość wszystkich tych elementów była taka sama i niezależna od ich zawartości, a to oznaczało, że w poziomie ich układ jest ograniczony. Panele tworzące stosy elementów zawsze działają wła­ śnie w taki sposób - układ nie jest ograniczony w kierunku tworzenia stosu, jednak w drugim kierunku wszystkie gniazda układu mają taką samą wielkość. W przypadkach gdy ze względu na zastosowanie układu ograniczonego elementy mają do dyspozycji więcej miejsca, niż im potrzeba, sposób jego wykorzystania można określać przy użyciu kolejnej grupy właściwości . Atrybut Hari za n tal Al i gnment pozwala określić położenie elementu wewnątrz gniazda układu. Listing 20 .7 przedstawia zmodyfikowaną wersję kodu z listingu 20.5, w której zastosowane zostały wszystkie cztery dostępne wartości właściwości Hari zanta l Al i gnment .

Listing 20.7. Wyrównanie elementu w poziomie < / Stac kPanel >

Efekty zastosowania takiego kodu przedstawia rysunek 20.5. Podobnie jak wcześniej, także i teraz każdy element podrzędny został umieszczony w gnieździe układu zajmującym całą szerokość panelu Stac k Panel , jednak wszystkie elementy z wyjątkiem trzeciego mają wielkość dopasowaną do wymiarów zawartości, a położenie ich wszystkich wewnątrz gniazda zostało określone przy użyciu właściwości Hari za n tal Al i gnmen t . Trzeci przycisk wciąż zajmuje całą szerokość wiersza, gdyż w jego przypadku właściwości Hari zantal Al i gnment została przypisana wartość Stret c h .

748

I

Rozdział 20. WPF i Silverlight

I P rz yc i s k i I I

w

j.stos. j

I ułożon eI I

Rysunek 20.5. Wyrównywanie elementów w poziomie Należy zauważyć, że jest to wartość domyślna, dlatego też jeśli jawnie nie podamy wyrów­ nania elementu, to będzie on zajmował całą szerokość gniazda układu. Dokładnie w taki sam sposób działa właściwość Vert i cal Al i g nment, przy czym udostępnia ona wartości Top, Bot tom, C en t er oraz Stret c h . '

. .

Zastosowanie właściwości określających wyrównanie daje jakikolwiek efekt wyłącznie w przypadkach, gdy gniazdo układu jest większe od obszaru wymaganego przez dany element. Jeśli do dyspozycji elementu oddano gniazdo, którego wymiar w poziomie lub w pionie dokładnie odpowiada wielkości, o jaką element poprosił, to zastosowanie właściwości wyrównania w danym kierunku nie da żadnego rezultatu. A zatem użycie właściwości Verti cal Al i gnment w elemencie umieszczonym w panelu StackPanel nic nam nie da - gniazdo układu, w jakim zostanie umieszczony taki element, będzie miało wysokość dokładnie odpowiadającą jego wymaganiom, a więc będzie on jednocześnie wyrównany do górnej i do dolnej krawędzi gniazda oraz zostanie w nim wyśrodkowany.

Kolejną bardzo ważną i wszechobecną właściwością związaną z układem jest Marg i n . Pozwala ona określać wielkość obszaru pomiędzy brzegami elementu oraz krawędziami gniazda układu, w jakim się on znajduje. W przypadku użycia układu nieograniczonego zastosowanie marginesu sprawi, że gniazdo dla elementu będzie większe od tego, które element by uzy­ skał, gdyby marginesy nie zostały określone. Listing 20.8 ilustruje zastosowanie marginesów na przykładzie panelu StackPanel o układzie pionowym. Ponieważ korzysta on z ograniczonego układu poziomego oraz nieograniczonego układu pionowego, możemy zobaczyć efekty, jakie dają marginesy w obu tych przypadkach.

Listing 20.8. Przyciski z marginesami < / Stac kPanel >

Jak widać na rysunku 20 .6, pierwszy przycisk wypełnia całą szerokość dostępnego obszaru, gdyż nie ma żadnych marginesów, jednak każdy kolejny przycisk jest węższy, gdyż ma mar­ ginesy większe niż poprzedni. Ponieważ szerokość elementu jest ograniczona, system okre­ ślania układu musi zwęzić przycisk, by zapewnić dostatecznie dużo miejsca na marginesy umieszczane pomiędzy brzegami elementu oraz granicami gniazda układu, w jakim jest on umieszczony. Jednocześnie, jako że elementy podrzędne układu nie są ograniczone w pionie, zastosowanie marginesów nie będzie miało wpływu na ich wysokość. Zamiast tego dodawanie coraz to większych marginesów elementów powoduje w tym przypadku powiększanie gniazd układu.

Elementy i kontrolki

I

749

P rzyc i s ki ułożon e

I

-I

w

I

stos .

[

l

Rysunek 20.6. Przyciski z marginesami W przykładzie przedstawionym na listingu 20.8 marginesy są definiowane przy użyciu jednej liczby - określa ona wspólną wielkość marginesu dla wszystkich czterech krawędzi elementu. Jednak marginesy można także określać bardziej szczegółowo. Można podać dwie liczby, ustalając w ten sposób oddzielnie marginesy w poziomie oraz w pionie. Można także podać cztery liczby określające niezależnie wielkość odpowiednio: lewego, górnego, prawego oraz dolnego marginesu4 . W ten sposób można zapewnić dokładne umiejscowienie elementów w panelu Gri d - okazuje się, że wcale nie trzeba stosować panelu Canvas, by móc precyzyjnie określić ich położenie. Jeśli element zostanie wyrównany do lewej i górnej krawędzi, to pierwsze dwie wartości jego marginesów określą w zasadzie jego położenie wewnątrz gniazda układu, czyli spełnią dokładnie taką samą rolę jak właściwości Canva s . Left oraz Canva s . Top w panelu Canva s . Interaktywne narzędzia do projektowania interfejsów użytkownika dostępne w Visual Studio oraz w programie Blend korzystają z tego, by zapewnić nam możliwość prze­ ciągania elementów w obszarze panelu i umieszczania ich dokładnie tam, gdzie chcemy. Można przy tym odnieść wrażenie, że element pozostaje niezależny od układu, jeśli jednak przeanali­ zujemy kod XAML generowany przez te narzędzia podczas przesuwania elementów, to okaże się, że korzystają one jedynie z opcji wyrównania i odpowiednio określają wielkości marginesów. Wszystkie spośród przedstawionych do tej pory mechanizmów określania układów są ze swej natury bardzo prostokątne - wszystko w nich jest albo dokładnie poziome, albo dokładnie pionowe. W rzeczywistości dzięki zastosowaniu przekształceń WPF oraz Silverlight są nieco bardziej elastyczne.

Przekształcen ia Przekształcenia można stosować we wszystkich elementach, modyfikując ich wielkość, poło­ żenie, orientację, a nawet przekręcając je. (Jeśli Czytelnik zna się na mechanizmach przekształ­ ceń współrzędnych geometrycznych dostępnych we wszystkich nowoczesnych systemach graficznych, to bez wątpienia rozpozna w nich zwyczajne dwuwymiarowe przekształcenia afi­ niczne, ang. affine transformations, które można wykonać przy wykorzystaniu macierzy 2x35) . Listing 20.9 prezentuje kolejną wersję przedstawionego już wcześniej przykładu z panelem Stac kPanel , w której elementy umieszczone w panelu zostały poddane przekształceniu. 4

Owszem, kolejność, w jakiej podawane są poszczególne marginesy, jest inna niż w CSS. Silverlight oraz WPF zachowują zgodność z konwencją zapisu współrzędnych geometrycznych, w której zawsze najpierw jest poda­ wana współrzędna pozioma, a potem pionowa x jest zawsze przed y. Dlatego najpierw określa się margines lewy i górny, a potem prawy i dolny. -

5

Konkretnie rzecz biorąc, jest to macierz 3x3, lecz jej ostatnia kolumna ma stałą postać (O, O, 1 ) .

750

I

Rozdział 20. WPF i Silverlight

Listing 20.9. Przekształcenia < / Button . RenderTransform> < / Button> < / Button . RenderTransform> < / Button> < / Button . RenderTransform> < / Button> < / Button . RenderTransform> < / Button> < / Stac kPanel >

Jak widać na rysunku 20.7, zastosowanie wykorzystanej w tym przykładzie właściwości Render "+Tra n s form może bardzo namieszać w układzie . Przekształcenie jest stosowane już po prze­ prowadzeniu wszystkich wyliczeń związanych z określaniem układu i wymiarów elementów. Dlatego też przekształcenie Scal eTran s form zastosowane w pierwszym przycisku sprawi, że będzie on zbyt duży, by zmieścił się w gnieździe układu. Podczas określania jego wielkości jest używana domyślna opcja Stret c h właściwości Hari zon t a l Al i gnment, dlatego początkowa sze­ rokość przycisku odpowiadała szerokości zawierającego go panelu StackPanel , jednak potem przycisk został przeskalowany - jego szerokość została pomnożona przez 1 . 5, a wysokość przez 0 . 5 . W efekcie został on przycięty na szerokość. Na podobnej zasadzie w przypadku elementów, które przy użyciu przekształcenia obróciliśmy i przekrzywiliśmy, pojawiły się obcięte rogi. WPF udostępnia właściwość LayoutTra n s form, która uwzględnia przekształcenie podczas obliczania układu, co może nas uchronić przed występowaniem tego typu problemów, jednak nie jest ona dostępna w Silverlight - tu rozwiązanie problemu będzie wymagało ręcz­ nego zmodyfikowania układu.

I

P r::z: -y c: i

:s

1-c i

stos.

'

'ł<

�. -

��



'�!

Rysunek 20.7. Przekształcone przyciski Elementy i kontrolki

751

'

. .

, ..._-__-� ·

Przekształcenia odnoszą się nie tylko do elementu docelowego, lecz także do wszyst­ kich elementów podrzędnych umieszczonych wewnątrz niego . Na przykład jeśli w panelu zastosujemy przekształcenie RotateTransform, to obrócone zostaną wszystkie umieszczone w nim elementy.

Obsługa obrotów, skalowania oraz odkształcania pokazuje, że technologie WPF i Silverlight zostały zaprojektowane z myślą o tworzeniu bardziej interesujących interfejsów graficznych niż standardowe prostokątne interfejsy tradycyjnych aplikacji systemu Windows. A zatem nadeszła chyba odpowiednia pora, by przyjrzeć się niektórym spośród elementów graficznych udostęp­ nianych przez te technologie.

Elementy graficzne WPF i Silverlight udostępniają kilka rodzajów elementów graficznych. Tak zwane kształty to elementy pozwalające tworzyć skalowalne wektorowe figury dwuwymiarowe . Dostępnych jest także wiele sposobów korzystania z bitmap oraz element pozwalający prezentować klipy wideo. Poza tym zarówno WPF, jak i Silverlight udostępniają możliwość tworzenia grafiki 3D, choć każda z tych technologii robi to nieco inaczej .

Kształty S hape jest klasą bazową dla różnych dwuwymiarowych figur. Jest to klasa abstrakcyjna definiu­ jąca wspólne właściwości takie jak F i 1 1 oraz Stro ke, które określają sposób rysowania wnętrza oraz krawędzi figury. Przeznaczenie niektórych spośród jej klas pochodnych, takich jak El l i pse, Rectangl e oraz L i n e, jest oczywiste. Nieco dokładniejszego objaśnienia wymagają natomiast klasy Pol yl i n e, Pol ygon oraz Pat h .

Klasa Pol y l i n e pozwala nam utworzyć kształt składający się z sekwencji linii prostych - wystar­ czy podać listę par współrzędnych definiujących położenie kolejnych wierzchołków figury. Klasa Pol ygon robi to samo, jednak prezentowana przez nią figura jest zamknięta; klasa ta automatycznie łączy pierwszy i ostatni z podanych punktów. Jednak obie te klasy są raczej rzadko stosowane, gdyż klasa Path udostępnia wszystkie ich możliwości oraz wiele innych. (Pro­ gram Expression Blend nigdy nie stosuje elementów Pol yl i ne oraz Pol ygon . Nawet w przypadku tworzenia figury składającej się wyłącznie z prostych odcinków i tak będzie on tworzył ścieżki, korzystając przy tym z elementu Pat h . Co więcej, dokładnie tak samo postępuje większość narzędzi eksportujących kod XAML stosowanych w innych programach takich jak Adobe Illustrator. A zatem w praktyce będziemy się zazwyczaj spotykali z elementem Pat h . Pozostałe dwa istnieją, gdyż nieco łatwiej można się nimi posługiwać z poziomu kodu) . Element Pat h pozwala definiować kształty stanowiące połączenie odcinków prostych i frag­ mentów linii krzywych. Listing 20 .10 przedstawia ścieżkę składającą się wyłącznie z prostych krawędzi.

Listing 20.10. Ścieżka składająca się z odcinków prostych < / UserControl . Resources>

Elementy i kontrolki

I

757

Animacje to osobne obiekty niezależne od elementów/ które animują. Zazwyczaj są one umieszczane w sekcji Resources - właściwość Resources/ stanowiącą wygodne miejsce/ w którym można umieszczać użyteczne obiekty/ posiadają wszystkie elementy. Jest ona po prostu słow­ nikiem - kolekcją par nazwa-wartość - a właściwie wyspecjalizowanym rodzajem słownika przypominającym te przedstawione w rozdziale 9 . W naszym przykładzie animacja byłaby obiektem podrzędnym obiektu U s erCon t ro l stanowiącego element główny interfejsu użyt­ kownika . Choć powyższy przykład jest bardzo prosty/ to jednak ilustruje wszystkie istotne zagadnienia związane z animacjami. Wszystko/ co najważniejsze/ jest umieszczone w obiekcie Storyboard kolekcji animacji. Animacje zawsze są definiowane w obiektach Storyboard/ gdyż dzięki temu można operować w nich na wielu właściwościach oraz tworzyć sekwencje animacji odpo­ wiednio zsynchronizowane w czasie. Nasz przykład jest bardzo prosty i zawiera tylko jedną animację; niemniej jednak nawet w takim przypadku musi ona zostać umieszczona w obiek­ cie Storyboard . Sama animacja posiada właściwości From oraz To określające zakres wartości właściwości/ która będzie modyfikowana w czasie. W naszym przypadku są to liczby/ gdyż używamy animacji typu Doub l eAn i mat i on (operującej na wartościach zmiennoprzecinkowych typu Sys t em . Doubl e) / natomiast gdybyśmy zastosowali animację typu Col orAn i mat i on/ to zamiast liczb podalibyśmy wartości kolorów. Użycie właściwości AutoReverse oraz Repeat Behavi or oznacza/ że nasza ani­ macja będzie wykonywana tam i z powrotem nieskończenie wiele razy. Ostatnie dwie właści­ wości określają element oraz nazwę właściwości animowanej. A zatem gdzieś w kodzie XAML musi się znaleźć element o podanej nazwie/ na przykład:

Coś musi animację rozpocząć. Można ją pobrać z zasobów i rozpocząć jej wykonywanie w kodzie ukrytym. W tym celu należałoby użyć następującego fragmentu kodu: Storyboard an i m = (Storyboard) Resources [ " e l l i pseAn i mat i on " ] ; an i m . Beg i n () ;

Istnieją także inne sposoby rozpoczynania animacji . WPF udostępnia tak zwane wyzwalacze (ang. triggers)/ które pozwoliłyby umieścić w kodzie XAML instrukcję nakazującą urucho­ mienie konkretnej animacji w odpowiedzi na zajście konkretnego zdarzenia . A zatem mogli­ byśmy skojarzyć animację na przykład z wystąpieniem zdarzenia Mou seEn ter lub uruchomić ją w chwili/ gdy zmieni się wartość określonej właściwości. Podobne rozwiązania można tworzyć w aplikacjach Silverlight/ korzystając z tak zwanych zachowa1i (ang. behaviors)/ dzięki którym w programie Expression Blend można bardzo łatwo definiować przeróżne reakcje interfejsu użytkownika (takie jak uruchomienie animacji) . Zarówno technologia WPF/ jak i Silverlight obsługują automatyczne wykonywanie animacji w szablonach kontrolek/ o czym się przeko­ namy w dalszej części rozdziału.

Grafi ka trójwym iarowa Technologia WPF obsługuje grafikę trójwymiarową/ jednak jest to zagadnienie/ któremu nale­ żałoby poświęcić osobny rozdziat dlatego też nie będziemy się nim zajmować w tej książce . Silverlight nie dysponuje podobnymi funkcjonalnościami co WPF/ jednak udostępnia pewne możliwości obsługi grafiki trójwymiarowej w postaci specjalnych przekształceń. Oprócz przedstawionego już wcześniej obiektu Ren derTra n s form można także skorzystać z właściwości

758

I

Rozdział 20. WPF i Silverlight

Proj ect i on elementu i przekształcić go tak, by wyglądał, jakby został obrócony w przestrzeni; obejmuje to także użycie efektu perspektywy, którego nie można uzyskać, stosując dwuwy­ miarowe przekształcenia afiniczne. Oczywiście możliwości tych nie da się porównać z pełnym modelem przestrzennym dostępnym w WPF, niemniej jednak stanowią one podstawę niezbędną do wyposażenia interfejsu użytkownika w pewne aspekty trójwymiarowości .

Mechanizmy rozmieszczania oraz usługi graficzne są konieczne do wyświetlania elementów na ekranie, jednak większość aplikacji wymaga czegoś na wyższym poziomie abstrakcji - stan­ dardowych elementów, których użytkownik mógłby używać do interakcji z aplikacją. Właśnie dlatego zarówno WPF, jak i Silverlight udostępniają kontrolki .

Kontrolki Technologie Silverlight oraz WPF udostępniają wiele kontrolek, spośród których wiele przy­ pomina kontrolki powszechnie występujące w typowych aplikacjach przeznaczonych dla systemu Windows . Dostępne są na przykład przyciski: C h ec k Box oraz Rad i oB utton służące do dokonywania wyboru, B u t ton, aby można było coś kliknąć, oraz Hyperl i n k B u t t on stosowany w sytuacjach, gdy chcemy, by przycisk wyglądał jak hiperłącze. Dostępny jest także przycisk Repeat Button, który wygląda jak zwyczajny przycisk, lecz cyklicznie generuje zdarzenie klik­ nięcia tak długo, dopóki jest wciśnięty . W większości przypadków kontrolki te działają w bardzo prosty sposób - już wcześniej, na listingu 20 .2 oraz 20 .3, pokazaliśmy, jak można obsługiwać zdarzenie C l i c k . Zgodnie z tym, czego się można spodziewać, dwa przyciski służące do dokonywania wyboru generują zdarze­ nia Chec ked oraz U n c h ec ked informujące o ich zaznaczeniu lub usunięciu zaznaczenia (udostęp­ niają także właściwość I sChec ked pozwalającą odczytać ich stan) . Niemniej jednak istnieje pewna zaskakująca cecha, którą przyciski dziedziczą po swojej klasie bazowej Con t en t Control .

Kontrolki z zawartością Wiele kontrolek posiada coś w rodzaju tytułu - na przyciskach jest zazwyczaj prezentowany jakiś tekst, karty mają swój nagłówek. Można by oczekiwać, że takie kontrolki będą miały jakąś właściwość typu s tri ng pozwalającą określić tytuł, jeśli jednak przyjrzymy się właściwości Content kontrolki B ut ton lub właściwości Header kontrolki Tab l tem, przekonamy się, że obie są właściwościami typu obj ect . Można w nich umieścić tekst, jednak wcale nie trzeba. Alterna­ tywne rozwiązanie przedstawiliśmy na listingu 20 .16.

Listing 20.16. Przycisk, którego zawartością jest elipsa

W rzeczywistości wcale nie musimy stosować elementu właściwości - w kla­ sie bazowej ContentCon trol został bowiem użyty atrybut [Con tent Property ( " Content " ) ] , który nakazuje kompilatorowi XAML traktować wszystkie elementy umieszczone wewnątrz elementu ContentControl tak, jak gdyby zostały podane jako wartość właściwości Conten t . A zatem poniż­ szy fragment kodu jest dokładnym odpowiednikiem kodu z listingu 20 .16 .

Elementy i kontrolki

I

759



W obu przypadkach zostanie utworzony przycisk, którego zawartością będzie zielona elipsa . Możemy także stworzyć nieco bardziej ambitne rozwiązanie i umieścić na przycisku panel:

Efekty generowane przez ten kod XAML zostały przedstawione na rysunku 20 . 1 1 . Kontrolki z zawartością pozwalają nam na dowolne szaleństwa - nic nie stoi na przeszkodzie, by umiesz­ czać przyciski w przyciskach, te w etykietach kart, a te z kolei na listach umieszczonych w kolej­ nych przyciskach. Jednak sama możliwość stworzenia czegoś takiego wcale nie oznacza, że powinniśmy to robić - bez wątpienia taki interfejs użytkownika byłby czymś koszmarnym. Chodzi nam tylko o to, by pokazać, że w kontrolkach z zawartością można umieszczać prak­ tycznie wszystko.

K l i k n ij

m n ie !

Rysunek 20.1 1 . Przycisk o różnorodnej zawartości Niektóre kontrolki mogą posiadać więcej elementów zawartości. Na przykład kontrolka Tab l tem udostępnia właściwość Content, w której jest umieszczana główna zawartość karty, oraz właści­ wość Header służącą do określenia tytułu. W obu tych właściwościach można umieścić dowolną zawartość. Oprócz kontrolek z treścią istnieją także kontrolki z elementami, które przenoszą nasze możliwości na kolejny, jeszcze wyższy poziom.

Kontrolki z wieloma elementami I temCon trol to klasa bazowa dla kontrolek, wewnątrz których może być wyświetlanych wiele elementów. Przykładami takich kontrolek mogą być Li st Box, ComboBox czy też TreeV i ew. W przy­ padku dodawania do nich elementów podrzędnych każdy z nich może być czymś całkowicie dowolnym - przypomina to nieco dowolną zawartość, którą można umieszczać na przycisku, z tą różnicą, że takich zawartości może być wiele. Listing 20 .17 przedstawia przykład kontrolki L i s t Box o bardzo różnorodnej zawartości .

Listing 20.17. Kontrolka ListBox o różnorodnej zawartości < L i stBox>

760

I

Rozdział 20. WPF i Silverlight

< / Li s tBox>

Efekty zastosowania takiego kodu przedstawiliśmy na rysunku 20 .12. Oprócz wyświetlania zawartości dostarczonej przez nas kontrolka L i s t Box w standardowy sposób reaguje na czyn­ ności wykonywane przy użyciu myszki - element, na którym jest umieszczony jej wskaźnik, ma nieco ciemniejsze tło od pozostałych, co informuje użytkownika o możliwości jego zaznacze­ nia. Element widoczny na samym dole ma jeszcze ciemniejsze tło, gdyż aktualnie jest zazna­ czony. Ta logika wyróżniania jest obsługiwana przez kontener elementów - wszystkie kon­ trolki opisywane w tym punkcie rozdziału generują kontener dla każdego prezentowanego elementu. Kontrolka L i st Box będzie generować kontenery typu L i st Box I tem, kontrolka TreeV i ew kontenery typu TreeVi ewl t em i tak dalej .

li

Pol e tekstowe

I

Te kst i tł !g ra fi ka

Rysunek 20.12 . Kontrolka ListBox z różnorodną zawartością Czasami warto utworzyć swoje własne kontenery, gdyż może się zdarzyć, że będą one musiały robić coś więcej, niż jedynie przechowywać pojedynczy element zawartości . Na przykład podczas tworzenia kontrolki drzewa nie wystarczy określić tytuł węzła - zapewne oprócz tego trzeba będzie także dodać jakieś węzły podrzędne. Kod przedstawiony na listingu 20 .18 jawnie tworzy kontenery TreeVi ewl t em i używa ich podczas określania struktury drzewa.

Listing 20.18. Jawne tworzenie kontenerów TreeViewltem < / Stac kPanel > < / ctl : TreeV i ewi tem . Header> < / ctl : TreeV i ew i tem> < / ctl : TreeV i ewi tem . Header>

Elementy i kontrolki

761



Warto zwrócić uwagę na niezwykły prefiks ctl : - jego znaczenie wyjaśniliśmy w ramce zamiesz­ czonej na następnej stronie. Jak widać na rysunku 20 .13, wartość każdej właściwości Header została użyta jako etykieta pojedynczego węzła w drzewie . Relacja rodzic-dziecko pomiędzy poszczególnymi węzłami jest w tym przypadku określana na podstawie zagnieżdżenia poszczególnych elementów T reeVi ewl tem w kodzie XAML.

e l em e nt A E l em e1





IPrzycisk I

B

e l em e nt 1 E l em e nt 2

I

I

E l em e nt

31

Pol e te ksto we

I

Rysunek 20. 13. Kontrolka TreeView z zawartością Choć zawartość kontrolek opisywanych w tym punkcie rozdziału można dodawać bezpo­ średnio, to jednak bardzo często znacznie wygodniejszym i elastyczniejszym rozwiązaniem będzie skorzystanie z mechanizmów wiązania danych, dlatego też wrócimy jeszcze do tych kontrolek w dalszej części rozdziału. Niniejszy rozdział stanowi jedynie wstęp do zagadnień tworzenia aplikacji Silverlight i WPF, dlatego też nie będziemy w nim prezentować szczegółowo wszystkich dostępnych kontrolek. Można wyróżnić proste kontrolki takie jak Text Box, AutoComp l eteBox, Sl i der oraz Dat e P i c k er. Dostępne są także bardziej wszechstronne kontrolki operujące na danych, takie jak DataGri d lub Data Pager, oraz kontrolki pomocnicze, takie jak zapewniające możliwość przeciągania kon­ trolki T h umb oraz G r i dSp l i t ter. Oprócz tego istnieje jeszcze jeden rodzaj kontrolek, któremu musimy się przyjrzeć - kontrolki użytkownika .

762

I

Rozdział 20. WPF i Silverlight

Bibl ioteki kontrolek i XAML W przykładzie z listingu 20.16 zastosowaliśmy kontrolkę TreeVi ew oraz towarzyszący jej kontener TreeVi ewl tem. Jednak klasy odpowiadające tym elementom nie są dostępne w głównej wtyczce Silver­ light. Należą one do Silverlight SDK i są dostarczane w osobnej bibliotece DLL o nazwie System . 4Wi ndows . Control s , która w efekcie jest wbudowywana w tworzone aplikacje Silverlight. W odróż­ nieniu od zwyczajnych aplikacji platformy .NET, aplikacje Silverlight są tworzone w formie archi­ wów ZIP (i zazwyczaj posiadają rozszerzenie .xap, co wymawiamy jako „zap"), dzięki czemu każda z nich może zawierać wiele komponentów i zasobów. Plik archiwum musi także zawierać wszelkie biblioteki kontrolek - te dostarczane przez firmę Microsoft, oferowane przez inne firmy, a także te napisane przez nas. Aby dodać bibliotekę DLL do pakietu aplikacji Silverlight, wystarczy w Visual Studio w zwyczajny sposób dodać odwołanie do niej do projektu. W przypadku stosowania kontrolek z bibliotek musimy poinformować kompilator XAML o tym, gdzie powinien ich szukać. A zatem by kod z listingu 20.18 mógł zadziałać, konieczne byłoby uzupełnienie go o pewne dodatkowe informacje. Główny element kodu musiałby zawierać dekla­ rację przestrzeni nazw XML o następującej postaci: xml ns : ctl = " cl r-namespace : System . W i ndows . Control s ; assembly=System . W i ndows . Control s " (Cały ten tekst powinien być zapisany w jednym wierszu i bez żadnych znaków odstępu - tutaj podzieliliśmy go, by pasował do wymiarów strony) . Oznacza to, że za każdym razem, gdy używamy elementu, którego nazwa zaczyna się od prefiksu ctl : , używamy typu zdefiniowanego w przestrzeni nazw System . W i ndows . Control s i umieszczonego w bibliotece (czy też w podzespole, gdyż w .NET biblioteki DLL są nazywane tak samo jak wyko­ nywalne pliki EXE) System . Wi ndows . Control s . Choć w technologii WPF jest stosowany dokładnie ten sam mechanizm określania przestrzeni nazw XML, to jednak w jej przypadku typ TreeVi ew należy do głównej części platformy .NET. Dlatego też w aplikacjach WPF kontrolek TreeVi ew można używać tak samo jak wszystkich innych elementów, nie przejmując się dodawaniem bibliotek DLL ani prefiksów przestrzeni nazw XML. Firma Microsoft dostarcza zestaw dodatkowych kontrolek w formie pakietu Silverlight Toolkit, który można pobrać ze strony http://www.codeplex.com/Silverlight. Z kolei na stronie http://www.codeplex.com/wpf można znaleźć podobny zestaw kontrolek dla aplikacji WPF.

Kontrolki użytkown ika Kontrolki użytkownika (ang. user controls) to, zgodnie z tym, co sugeruje ich nazwa, takie kon­ trolki, które sami tworzymy. W aplikacjach Silverlight zawsze będziemy korzystali przy­ najmniej z jednej kontrolki tego rodzaju - cały interfejs użytkownika naszej aplikacji jest jedną wielką kontrolką użytkownika, o czym możemy się przekonać na podstawie elementu będącego głównym elementem naszego pliku XAML. Nic jednak nie stoi na przeszkodzie, by tworzyć i używać więcej takich kontrolek. Stanowią one bowiem bardzo dobry sposób radzenia sobie ze złożonością kodu. Problemem, który bardzo często występuje w dużych projektach WPF oraz Silverlight zwłaszcza w pierwszym projekcie tworzonym przez dany zespół - jest plik XAML o wiel­ kości przekraczającej 10 tysięcy wierszy kodu. Visual Studio tworzy jeden plik XAML dla całego interfejsu użytkownika aplikacji i najwygodniej jest umieszczać wszystko właśnie w nim, idąc po linii najmniejszego oporu. Jednak wraz z dodawaniem do interfejsu użytkownika

Elementy i kontrolki

I

763

nowych zasobów graficznych, szablonów, źródeł danych, animacji, stylów oraz wszystkich innych elementów, które można umieszczać w kodzie XAML, wielkość tego pliku może rosnąć zaskakująco szybko. Oprócz tego pojawia się znaczący problem z umieszczeniem całej funk­ cjonalności aplikacji w jednym pliku kodu ukrytego. Utrzymanie i rozwijanie takich aplikacji jest kłopotliwe, dlatego też konieczne staje się podzielenie ich w jakiś sposób. Zamiast tworzyć jeden duży plik XAML, warto spróbować umieszczać na głównej stronie aplikacji jak najmniej elementów. Zazwyczaj jej działanie powinno się ograniczać do zdefi­ niowania ogólnego układu określającego, gdzie znajdują się poszczególne fragmenty interfejsu. Każdy z takich fragmentów można następnie umieścić w niezależnej kontrolce użytkownika . Taka kontrolka jest zwyczajnym plikiem XAML, któremu towarzyszy plik kodu ukrytego, a ponieważ pliki XAML wraz z kodem ukrytym są kompilowane do postaci klasy, można ich używać w innych plikach XAML - trzeba pamiętać, że kod XAML jest jedynie innym spo­ sobem definiowania obiektów. Listing 20 .19 przedstawia kod XAML głównego interfejsu użytkownika aplikacji, która została utworzona z wykorzystaniem tej metody.

Listing 20.19. Główny interfejs aplikacji składający się wyłącznie z kontrolek użytkownika < /Gri d . RowDef i n i t i ons> < /Gri d . Col umnDefi n i t i ons>

< / Gri d> < / UserControl >

Należy zwrócić uwagę, że w powyższym kodzie definiujemy przestrzeń nazw XML o nazwie app i informujemy kompilator XAML, że odnosi się ona do przestrzeni nazw S l U c Examp l e, czyli domyślnej przestrzeni nazw tej przykładowej aplikacji. W tym przypadku nie potrzebujemy fragmentu as sembl y=, gdyż kontrolki użytkownika są zdefiniowane jako elementy tego samego projektu, a nie w osobnej bibliotece DLL. Następnie prefiks app został użyty w trzech kontrol­ kach użytkownika, które zapewne zostały zdefiniowane w innej części projektu. Definiowanie kontrolek użytkownika jest całkiem proste. Można je dodawać w Visual Studio jako nowe elementy projektu, co powoduje utworzenie nowego pliku XAML oraz towarzyszącego mu pliku kodu ukrytego. Oba te pliki są edytowane dokładnie tak samo jak główny plik XAML aplikacji i towarzyszący mu plik z kodem ukrytym.

764

I

Rozdział 20. WPF i Silverlight

'

. .

,

.___�.·

Jak widać, w przykładzie z listingu 20.19 wszystkim kontrolkom użytkownika nadali­ śmy nazwy kończące się słowem View. Nie jest to oczywiście konieczne, jednak pomaga odróżnić klasy kontrolek użytkownika, definiujące wygląd oraz obsługę interaktywności, od innych typów definiujących inne aspekty działania aplikacji. Takie rozróżnienie nie będzie przydatne w sytuacjach, gdy zdecydujemy się obsługiwać wszystkie aspekty działania programu w pliku kodu ukrytego, zakładamy jednak, że Czytelnik ma bardziej wyrobioną wrażliwość na zagadnienia projektowania oprogramowania i będzie się starał, by wszystkie klasy tworzące aplikację miały ściśle określone przeznaczenie.

Kontrolki użytkownika mogą zawierać dowolne inne kontrolki i elementy, można więc uży­ wać w nich zarówno wbudowanych elementów technologii Silverlight, jak również innych bibliotek kontrolek, które udało się nam zdobyć. A zatem kontrolki użytkownika zapewniają bardzo dużą elastyczność. Niemniej jednak wcale nie trzeba korzystać z nich za każdym razem, gdy chcemy utworzyć jakiś własny interfejs użytkownika . Możliwości modyfikacji wbudo­ wanych kontrolek są znacznie większe, niż można by przypuszczać, a wszystko to dzięki szablonom kontrolek.

Szablony kontrolek Jak mieliśmy okazję się przekonać, kontrolki są elementami posiadającymi jakieś interaktywne zachowania - przyciski można klikać, w polach tekstowych można coś wpisywać, a zawartość list można przewijać i wybierać. Jednak zapewne nie do końca zdajemy sobie sprawę z tego, że kontrolki dostarczają jedynie tego zachowania . Nie określają natomiast swojego wyglądu. Można pomyśleć, że takie stwierdzenie jest absurdalne . W końcu jeśli dodamy do interfejsu użytkownika element Button, to go zobaczymy. Jednak w rzeczywistości wygląd kontrolki jest określany nie przez nią samą, lecz przez tak zwany szablon (ang. template) . Kontrolki posia­ dają swoje domyślne szablony i to właśnie dlatego coś się pojawia na ekranie, kiedy je two­ rzymy. To rozdzielenie wyglądu od działania jest bardzo ważne, gdyż zapewnia możliwość zmiany domyślnego szablonu na jakiś inny. Dzięki temu możemy całkowicie zmienić wygląd kontrolek bez zmieniania sposobu, w jaki działają. Działanie kontrolek jest niejednokrotnie bardzo subtelne i złożone. Można by przypuszczać, że przycisk jest czymś całkiem prostym oraz że da się utworzyć jego odpowiednik, obsługując zdarzenie Mouseleft B uttonDown jakiegoś kształtu. I choć faktycznie pozwoliłoby to nam na stwo­ rzenie kształtu, który można by było kliknąć, to jednak dużo by mu brakowało do przycisku. Przyciski są na przykład w wyraźny sposób wciskane i zwalniane. Powinny reagować na czyn­ ności wykonywane nie tylko przy użyciu myszki, lecz także klawiatury. Powinny prawidłowo współpracować z narzędziami ułatwień dostępu, tak by z aplikacji mogły korzystać także osoby mające problemy ze wzrokiem bądź koordynacją ruchów. Jeśli kiedykolwiek zdarzyło się nam korzystać z aplikacji Flash, w której, dajmy na to, pasek przewijania zdawał się działać niezupełnie tak, jak powinien, to będziemy już znali zagrożenia, z jakimi wiążą się próby odtworzenia od podstaw nawet prostych kontrolek. Na szczęście dzięki istnieniu szablonów kontrolek nie będziemy musieli uciekać się do takich rozwiązań. Klasa bazowa Control definiuje właściwość Templ ate. Aby zmienić wygląd kontrolki, wystarczy zmienić jej wartość. Jak pokazuje przykład przedstawiony na listingu 20 .20, we właściwości T empl a t e można zapisywać obiekty typu Con t rol Templ a t e, natomiast wewnątrz nich można umieszczać już zupełnie dowolne elementy. (Oczywiście gdybyśmy chcieli utworzyć element o bardziej złożonym wyglądzie, to moglibyśmy to zrobić, używając panelu) . Szablony kontrolek

I

765

'

. .

Szablony s ą dostępne wyłącznie dla kontrolek. A zatem choć typy takie jak Button i TextBox dysponują odpowiednimi szablonami, to jednak prostsze elementy, takie jak kształty lub elementy TextBl o c k - czyli te części interfejsu użytkownika, które nie posiadają żadnych własnych zachowań - nie mają swoich szablonów. Nie powinno to jednak stanowić żadnego zaskoczenia. W końcu jedynym przeznaczeniem elementu El l i pse jest to, by wyglądał jak elipsa, do czego zatem miałby mu być potrzebny szablon? (A poza tym, jakiego innego elementu moglibyśmy użyć wewnątrz szablonu, by zdefiniować wygląd elementu? Kolejnego elementu El l i pse? A w jaki sposób byłby określany jego wygląd?)

Listing 20.20. Przycisk wykorzystujący niestandardowy szablon

Efekty, jakie daje użycie powyższego kodu, przedstawiliśmy na rysunku 20 .14. Jak widać, są one raczej statyczne - na razie nie zapewniają jeszcze wizualnych odpowiedzi na czynności wykonywane przez użytkownika, jednak tym zajmiemy się już niebawem. Jeśli klikniemy ten przycisk, to będzie on generował zdarzenie C l i c k, a zatem choć jest on raczej nudny, jest funkcjonalny. Warto zwrócić uwagę, że określiliśmy wartość właściwości Con tent przycisku umieściliśmy w niej słowo „Tak" i zgodnie z oczekiwaniami zostało ono wyświetlone. Jednak to wyświetlenie zawartości nie następuje automatycznie. Szablon musi określić, w jakim miejscu powinna się ona pojawić, i właśnie do tego celu służy element Content Pre senter widoczny w kodzie na listingu 20.20 . Szablony kontrolek z zawartością wymagają użycia jednego takiego elementu, który zapewni wyświetlenie zawartości właściwości Con t en t . Jeśli natomiast defi­ niujemy szablon dla kontrolki mogącej mieć większą liczbę elementów zawartości - takich jak Tab l tem posiadających właściwości Header oraz Content - to dla każdego z nich trzeba będzie użyć jednego elementu Con t en t Presenter.

T'a k Rysunek 20.14. Przycisk, którego wygląd określono przy użyciu własnego szablonu 766

I

Rozdział 20. WPF i Silverlight

A skąd technologia Silverlight (lub WPF) wie, który element Content Presenter odpowiada której właściwości? Przyjrzyjmy się właściwości Con t en t elementu Con t en t Presenter zastosowanego w przykładzie z listingu 20.20 - jej wartość została podana w dosyć niezwykły sposób. War­ tość atrybutu została zapisana wewnątrz pary nawiasów klamrowych, co oznacza, że nie chodzi nam o zapisanie we właściwości literału łańcuchowego. Użyty w przykładzie tekst T empl ateB i n d i ng oznacza, że chcemy połączyć konkretną właściwość tego elementu szablonu z odpowiednią właściwością kontrolki, w której szablon zostanie użyty. A zatem wyrażenie { Templ ateBi ndi ng Content } połączy dany element Content Presenter z właściwością Content kontrolki B utton . Z kolei wyrażenie { Templ ateB i n d i ng Header} połączy element Content Presenter, w którym je umieszczono, z właściwością Header kontrolki, w której zostanie użyty szablon. W rzeczywistości w szablonach bardzo często stosuje się znacznie więcej powiązań tego typu. W przykładzie przedstawionym na listingu 20 .20 wiele cech wyglądu kontrolki określanych przez szablon zostało podanych na sztywno. Istnieje jednak możliwość wielokrotnego używa­ nia tego samego szablonu w wielu różnych kontrolkach, a w takim przypadku warto zachować elastyczność i możliwość modyfikowania takich aspektów wyglądu jak kolor tła, grubość krawędzi itd., dzięki czemu można uniknąć każdorazowego definiowania nowego szablonu. Szablon przedstawiony na listingu 20.21 także generuje kontrolkę z rysunku 20 . 14, jednak zamiast podawać wszelkie wartości na stałe w swoim kodzie, pobiera je z właściwości kontrolki, używając do tego powiązań.

Listing 20.2 1 . Szablon, w którym prawie nie ma wartości podanych na stałe

Teraz nasz szablon wydaje się już nadawać do wielokrotnego stosowania - z powodzeniem będziemy mogli wykorzystać go w wielu różnych przyciskach. Standardowym sposobem, by to zrobić, jest umieszczenie go w stylu.

Style Styl jest obiektem definiującym wartości dla grupy właściwości obiektu określonego typu. Ponieważ wygląd elementu jest w całości określany przez jego właściwości - pamiętajmy, że Templ ate jest właściwością - styl może definiować tak wiele aspektów wyglądu kontrolki, jak tylko będziemy chcieli. Może on być bardzo prosty - ograniczać się do określenia kilku

Szablony kontrolek

I

767

właściwości takich jak Fon t Fam i l y oraz Bac kground - jak również bardzo złożony - może określać zarówno szablon, jak i wartości wszystkich właściwości kontrolki mających jakikol­ wiek związek z jej wyglądem. Przykład przedstawiony na listingu 20 .22 znajduje się gdzieś w połowie drogi pomiędzy tymi dwoma punktami ekstremalnymi - umieszcza w stylu szablon przedstawiony na listingu 20 .21 i określa wartości kilku dodatkowych właściwości .

Listing 20.22 . Styl przycisku

Warto zwrócić uwagę, że styl został zdefiniowany wewnątrz sekcji Resources . Trzeba pamiętać, że wszystkie elementy dysponują właściwością Resources będącą słownikiem, w którym można przechowywać użyteczne obiekty takie jak style. Stylu można użyć w konkretnym elemencie w następujący sposób:

W ten sposób zostaną pobrane wszystkie właściwości określone w stylu. Trzeba zwrócić uwagę, że także w tym przypadku w wartości atrybutu zostały umieszczone nawiasy klamrowe oznacza to, że używamy rozszerzenia kodu, czyli mechanizmu, który dopiero w trakcie działania aplikacji określa, jak należy ustawić wartości właściwości . Poznaliśmy już wcześniej rozsze­ rzenie kodu Temp l a t eB i n d i ng, a teraz korzystamy z kolejnego, Stat i c Resource, które odnajduje konkretny element w słowniku zasobów . ••



.•

'-"�,·

.

,

L-------1:� '

768

I

W odróżnieniu od właściwości Templ ate, która jest dostępna wyłącznie w kontrolkach, właściwość Styl e została zdefiniowana w klasie FrameworkEl ement, co oznacza, że jest dostępna we wszystkich rodzajach elementów.

Rozdział 20. WPF i Silverlight

Warto też wspomnieć, że w elemencie używającym stylu można przesłonić dowolną z właści­ wości, których wartości określa styl, tak jak to pokazaliśmy na listingu 20 .23.

Listing 20.23. Przesłanianie właściwości określonej w stylu

Właściwości podane bezpośrednio w elemencie przesłaniają wszelkie właściwości określone w stylu. To właśnie z tego powodu ważne jest stosowanie w szablonach wyrażeń Temp l ateB i ndi ng. Styl przedstawiony na listingu 20 .22 przypisuje właściwości Bac kground kolor Li g h t B l ue, nato­ miast szablon pobiera wartość właściwości wskazanej w Temp l ateB i n d i ng. Oznacza to, że gdy kod z listingu 20.23 użyje jako koloru tła wartości Yel l ow, to kontrolka wykorzysta ten nowy kolor. Nie stałoby się tak, gdyby kolor tła został podany bezpośrednio w kodzie szablonu. A zatem połączenie stylów, szablonów oraz wiązania szablonów zapewnia możliwość pełnego określenia wyglądu kontrolki, dając jednocześnie elastyczność, dzięki której będzie można zmieniać wybrane aspekty wyglądu w konkretnych kontrolkach. W naszym stylu określającym wygląd przycisku występuje jednak pewien drobny problem: jest on bardzo statyczny. Nie zapewnia żadnej wizualnej reakcji na czynności wykonywane przez użytkownika przy użyciu myszki. Większość kontrolek, które mogą reagować na takie czynności, jest w jakiś sposób wyróżniana po umieszczeniu wskaźnika myszy w ich obszarze. Fakt, że nasz przycisk zachowuje się inaczej, może sprawić, że użytkownik uzna, iż aplikacja uległa awarii bądź też że przycisk służy jedynie do celów dekoracyjnych. Postarajmy się zatem rozwiązać ten problem.

Menedżer stanu wizualnego Szablon kontrolki może zawierać zestaw instrukcji opisujących, w jaki sposób powinien się zmieniać jej wygląd w przypadku zmiany jej stanu. Jest on dodawany przy użyciu właściwości dołączanej o nazwie V i s u a l StateGroups zdefiniowanej w klasie V i s u a l StateManager6 • Przykład z listingu 20 .24 przedstawia zmodyfikowaną wersję szablonu, w którym wykorzystano wła­ ściwość V i s ual StateG roups .

Listing 20.24. Szablon kontrolki wzbogacony o przekształcenia wyglqdu



Właściwość V i s ual StateGroups zawiera jeden lub kilka elementów V i s ual Stat eGroup - to, jakie grupy można tu dodawać, zależy od konkretnej kontrolki. Elementy Button definiują dwie takie grupy: CommonStates oraz Foc usStates. Każda z nich określa pewne aspekty wyglądu kontrolki, które mogą się zmieniać niezależnie od innych grup. Na przykład grupa Foc usStates definiuje stany Focused oraz U n focused na podstawie tego, czy przycisk ma ognisko wprowadzania (ang. input focus) . Grupa CommonStates definiuje stany Norma l , MouseOver, Pressed oraz Di sab l ed. W danej chwili kontrolka może się znajdować tylko w jednym z nich, jednak to, czy jest ona wybrana, czy nie, nie ma żadnego związku z tym, czy w jej obszarze jest umieszczony wskaźnik myszy, dlatego też te stany znalazły się w odrębnych grupach. (Grupy stanów nie są od siebie całkowi­ cie niezależne - na przykład jeśli przycisk będzie nieaktywny, nie będzie można go wybrać. Niemniej jednak grupy można zobaczyć zawsze, gdy tylko pojawia się jakikolwiek stopień niezależności różnych stanów) . Przykład przedstawiony na listingu 20 .24 definiuje zachowanie przycisku dla przypadków, gdy znajdzie się on w stanie Mou s eOver oraz Norma l , przy czym każdy z nich jest definiowany przy użyciu odrębnego elementu V i s u a l State. Definiują one animację, którą należy uruchomić w momencie, gdy element znajdzie się w danym stanie. W naszym przykładzie obie animacje będą modyfikowały wartość właściwości Bac kground elementu Border. Pierwsza animacja sprawi, że w momencie umieszczenia wskaźnika myszy w obszarze kontrolki kolor jej krawędzi stopniowo zmieni się na czerwony, natomiast druga przywróci oryginalny kolor krawędzi, gdy wskaźnik myszy zostanie usunięty z kontrolki. (Brak właściwości To w drugiej animacji spra­ wia, że zostaje przywrócona jej wartość początkowa) . ... „-

770

Kod związany z przekształceniami wyglądu jest zazwyczaj bardzo rozbudowany . Animacje są jedynym sposobem modyfikowania właściwości, i to nawet w przypadku, gdy chcemy, by zmiany zachodziły jednocześnie, dlatego też nawet najprostsza zmiana wymaga użycia obszernego kodu. Co więcej, zazwyczaj będziemy się starali definio­ wać przekształcenia dla wszystkich dostępnych stanów. W praktyce będą one two­ rzone interaktywnie w programie Expression Blend, który generuje cały niezbędny kod XAML.

Rozdział 20. WPF i Silverlight

Jak na razie wszystko, czego się dowiedzieliśmy, było ściśle związane z wizualnymi aspek­ tami aplikacji, jednak w prawdziwych aplikacjach konieczne jest łączenie ich interfejsu użyt­ kownika z danymi . Aby ułatwić to zadanie, technologie Silverlight oraz WPF udostępniają mechanizm wiązania danych.

Wiązan ie danych Mechanizm wiązania danych pozwala nam skojarzyć właściwości obiektów .NET z właści­ wościami elementów interfejsu użytkownika . Składnia, jaka jest przy tym używana, bardzo przypomina tę stosowaną w wiązaniu szablonów. Listing 20 .25 przedstawia prosty formularz składający się z dwóch pól tekstowych, z których każde zostało powiązane z obiektem źró­ dłowym.

Listing 20.25. Wprowadzenie danych z wykorzystaniem mechanizmu wiązania





Podobnie jak wiązanie szablonów pozwala nam odwołać się do właściwości obiektu docelo­ wego, tak mechanizm wiązania danych pozwala nam odwoływać się do właściwości pewnego obiektu źródłowego. Ź ródła danych mogą być zupełnie normalnymi obiektami - przykład z listingu 20 .26 pokazuje wyjątkowo prostą klasę, której jednak z powodzeniem można użyć jako źródła danych dla formularza z listingu 20 .25 .

Listing 20.26. Bardzo proste źródło danych publ i c cl ass Person { publ i c s t r i ng N ame { get ; set ; } publ i c doubl e Age { get ; set ; }

W kodzie ukrytym można utworzyć obiekt tej klasy, a następnie udostępnić go mechanizmowi wiązania danych. W tym celu należy posłużyć się właściwością DataCon text w sposób przed­ stawiony na listingu 20 .27.

Wiązanie danych

771

Listing 20.27. Przygotowanie źródła danych publ i c part i al cl a s s Ma i n Page : UserControl { pri vate Person source = new Person { Name = " Janek" , Age = 36 } ;

publ i c Mai nPage () { I n i t i al i zeComponent () ; thi s . DataContext = source ;

Jak widać na rysunku 20 .15, dzięki powiązaniu danych interfejs użytkownika wyświetlił w polach wartości odpowiednich właściwości obiektu źródłowego. Można odnieść wrażenie, że takie rozwiązanie nie jest wygodniejsze od bezpośredniego określania w kodzie wartości właściwości Text obu obiektów Text Box, jednak możliwości mechanizmu wiązania danych są znacznie większe. Kiedy użytkownik wpisuje nowe wartości w polach tekstowych, zostają one jednocześnie zapisane w odpowiednich właściwościach obiektu Person . Gdybyśmy zmodyfi­ kowali klasę Person i zaimplementowali w niej interfejs I Not i fy PropertyC h an ged - co stanowi często stosowany sposób generowania powiadomień w momencie zmiany wartości właści­ wości obiektu - to mechanizm wiązania mógłby wykrywać zmiany obiektu źródłowego i auto­ matycznie aktualizować interfejs użytkownika .

I



I mi ę : J a ne k

! I

_

W i ek : 3 6

� �

Rysunek 20.15. Pola tekstowe powiązane z danymi

I

Bez wątpienia najważniejszą korzyścią, jaką daje mechanizm wiązania danych, jest możliwość zapewnienia separacji logiki aplikacji od kodu obsługującego interfejs użytkownika . Warto zwrócić uwagę, że klasa Person nie musi dysponować żadnymi informacjami na temat inter­ fejsu aplikacji, a pomimo to dane przechowywane w obiekcie tej klasy są z nim powiązane . Pisanie testów jednostkowych dla takich klas, które do działania nie wymagają żadnego inter­ fejsu użytkownika, jest znacznie łatwiejsze . Klasycznym błędem popełnianym przez początkujących programistów Silverlight oraz WPF jest pisanie kodu, który w zbyt dużym stopniu jest uzależniony od elementów interfejsu. Przykładem takiego błędnego rozwiązania byłoby wykorzystanie elementów Text Box do prze­ chowywania danych aplikacji. Można by przypuszczać, że ułatwiłoby to jej tworzenie - po co dodawać klasę przechowującą imię i wiek, skoro z powodzeniem można do tego celu użyć interfejsu użytkownika? Jednak oznaczałoby to, że każdy kod, który chciałby skorzystać z tych danych, musiałby odczytywać je z elementów interfejsu. Takie rozwiązanie przysparza dwóch problemów. Przede wszystkim sprawia, że trudno jest wprowadzić jakiekolwiek zmiany w interfejsie użytkownika bez wprowadzania zamieszania w całym kodzie programu, a po drugie, tracimy przez nie możliwość testowania różnych fragmentów programu niezależnie od pozostałych . A zatem pomimo tego, że separacja zaprezentowana w ostatnich trzech przykładach może się wydawać rozwiązaniem nieco nadmiarowym i niepotrzebnym, to jed­ nak w przypadku każdej nieco bardziej złożonej aplikacji okazuje się, że rozdzielenie danych od interfejsu użytkownika i skojarzenie ich wyłącznie przy użyciu mechanizmu wiązania danych

772

I

Rozdział 20. WPF i Silverlight

jest niezwykle przydatne i użyteczne . W porównaniu z kodem operującym bezpośrednio na elementach interfejsu pozwala to bowiem tworzyć kod, który będzie znacznie łatwiej utrzy­ mywać i rozwijać. W przykładzie przedstawionym na listingu 20 .25 w interfejsie użytkownika zastosowaliśmy jedynie kilka wyrażeń wiążących stworzonych na doraźne potrzeby. Istnieje jednak jeszcze inny sposób wiązania danych, nieco bardziej strukturalny i zapewniający znacznie większe możliwości, którego można używać w kontrolkach zawierających wiele elementów. Są nim szablony danych.

Szablony danych Analogicznie do szablonów kontrolek, które określają ich wygląd, można także tworzyć szablony danych określające sposób prezentacji danych konkretnego typu. Przyjrzyjmy się interfejsowi użytkownika przedstawionemu na rysunku 20 .16. Tworzą go dwie listy stanowiące klasyczne rozwiązanie typu dane ogólne - dane szczegółowe.

AvWorksBrowser

X

M o unta i n Bi kes Road Bi kes Touri ng Bikes H a nd leba rs

Bottom B raokets

Bra kes

Cha i n s

Tou ri n g - 2 0 0 0 Bl u e , 6 0 ...

� � 100%

....

"'

Rysunek 20.16. Listy wykorzystujące szablony danych Kontrolka L i st Box umieszczona z lewej strony wygląda raczej zwyczajnie - zawiera kategorie produktów i prezentuje ich nazwy w formie tekstu. Można by pomyśleć, że zawartość ta jest wyświetlana poprzez pobranie listy kategorii, a następnie utworzenie w pętli odpowiednich obiektów L i s t Box I t em i dodanie ich do listy. Zastosowane rozwiązanie jest jednak w rzeczy­ wistości znacznie prostsze . Na listingu 20 .28 przedstawiliśmy kod XAML definiujący tę listę.

Listing 20.28. Kontrolka ListBox prezentująca zwyczajne teksty < L i stBox x : Name= " categoryli s t " D i spl ayMemberPath= " D i spl ayName " Sel ect i onChanged= " categoryli s t_Sel ecti onChanged " >

'

. .

Ta aplikacja korzysta z przykładowej bazy danych Adventure Works przedstawionej w rozdziale 14., którą obsługująca ją aplikacja internetowa udostępnia klientowi Silver­ light przy użyciu kombinacji mechanizmu WCF Data Services (opisanego w tym samym rozdziale) oraz niektórych możliwości sieciowych opisanych w rozdziale 13. Szcze­ gółowe informacje dotyczące kodu działającego po stronie serwera nie mają znaczenia dla omawianych tu zagadnień, jednak można go znaleźć w przykładach dołączonych do niniejszej książki.

Listing 20 .29 przedstawia kod służący do wyświetlenia kategorii na liście.

Wiązanie danych

I

773

Listing 20.29. Dostarczanie elementów do wyświetlenia na liście categoryli s t . I temsSource = categoryV i ewModel s ;

Oczywiście pominęliśmy tu pewien fragment kodu - w końcu gdzieś trzeba utworzyć tę zmienną categoryVi ewModel s zawierającą listę obiektów reprezentujących poszczególne kate­ gorie - jednak teraz koncentrujemy się na skojarzeniu danych z interfejsem użytkownika, a nie na sposobie ich tworzenia. Nie chcąc odwracać uwagi Czytelnika od zagadnień bezpośrednio związanych z tematyką tego rozdziału, pokazujemy tu tylko te fragmenty kodu, które dotyczą samego interfejsu. Jak widać, wszystko to wygląda na naprawdę bardzo łatwe. Klasa L i s t Box dziedziczy po I temsControl , od której uzyskuje właściwość I temsSource, a w tej możemy zapi­ sać dowolną kolekcję. Kontrolka przeglądnie całą tę kolekcję, tworząc odpowiedni kontener (w tym przypadku będzie to L i s t Box I t em) dla każdego jej elementu. W kodzie XAML przypisaliśmy atrybutowi Di s p l ayMemberPath wartość Di s p l ayN ame - określa on nazwę właściwości obiektu źródłowego, którą obiekt L i stBoxi tem ma odczytać w celu wyświe­ tlenia nazwy elementu prezentowanego na liście. I to właśnie dlatego na liście z lewej strony są wyświetlane nazwy kategorii . Nie ma jednak żadnych wątpliwości, że lista widoczna na rysunku 20 .16 po prawej stronie jest znacznie bardziej interesująca . Prezentuje ona wszystkie produkty z aktualnie wybranej kategorii, jednak dla każdego z nich wyświetlany jest nie tylko tekst, lecz także jego zdjęcie. Ta lista produktów jest aktualizowana w momencie wyboru innej kategorii; kod obsługujący zdarzenie S e l ect i on Changed został przedstawiony na listingu 20 .30, a sposób jego zastosowania w kontrolce listy pokazaliśmy na listingu 20 .28 .

Listing 20.30. Wczytywanie produktów z wybranej kategorii pri vate voi d categoryli s t_Sel ect i onChanged (obj ect sende r , Sel ect i onChangedEventArgs e ) CategoryV i ewModel currentCategory = categoryli s t . Sel ected l tem as CategoryVi ewModel ; i f (currentCategory == nul l ) { product li s t . I temsSource = nu l l ; el se { product li s t . I temsSource

=

currentCategory . Products ;

Nasz kod musi sobie także poradzić z przypadkami, gdy zostanie wygenerowane zdarzenie Se l ect i onChanged informujące nas o tym, że na liście nie została zaznaczona żadna opcja. Jednak najbardziej interesujące fragmenty kodu wyglądają bardzo podobnie do tych z poprzedniego przykładu - także tutaj zapisujemy we właściwości I temsSource kontrolki L i st Box (tylko tym razem chodzi o listę z prawej strony) kolekcję obiektów, a konkretnie produktów należących do wybranej kategorii . Kod z listingu 20 .30 określa więc właściwość I temsSource w taki sam sposób jak kod z listingu 20 .29, a mimo to obie listy - ta z lewej oraz ta z prawej strony rysunku 20 .16 - wyglądają całkowicie inaczej . Dzieje się tak dlatego, że inny jest kod XAML definiujący tę drugą listę: < L i stBox x : Name= " product li s t " Gri d . Col umn= " l " >

774

I

Rozdział 20. WPF i Silverlight

Modele i szczegóły widoków Choć nie chcemy w zbyt dużym stopniu odciągać uwagi Czytelnika od tematyki wiązania danych, to jednak warto wskazać kilka zagadnień związanych ze źródłami danych zastosowanymi w kodzie z listingu 20.30, gdyż dobrze jest o nich wiedzieć. Przede wszystkim Czytelnik na pewno zwrócił uwagę na słowa View Model (model widoku) pojawiające się w nazwie klasy. Często spotyka się je w nazwach klas, które nie są częściami widoku - nie zawierają żadnego kodu związanego z inter­ fejsem użytkownika - lecz służą jako źródła danych dla tych widoków. Bardzo rzadko zdarza się, by elementy interfejsu użytkownika były kojarzone bezpośrednio z obiektami modelu dziedziny, gdyż obsługa interfejsu wymaga zazwyczaj wprowadzenia kilku dodatkowych danych opisujących stan widoku, które nie należą do tego modelu. Chcielibyśmy mieć możliwość łatwego przetesto­ wania działania logiki dziedziny, zatem wolimy nie łączyć jej z kodem obsługi interfejsu użyt­ kownika. Właśnie dlatego pomiędzy widokiem i modelem dodawana jest kolejna warstwa nazy­ wana warstwą modelu widoku (ang. view model) . Czasami jest ona także określana jako prezentacja oddzielona (ang. separated presentation) . Po drugie, można by się zastanawiać, dlaczego kontrolka Li stBox nie jest w stanie samodzielnie obsłu­ giwać powiązania danych głównych i szczegółowych, a zamiast tego zmusza nas do samodziel­ nego pisania procedur obsługi zdarzeń. Okazuje się, że może ona to robić, lecz w tym konkretnym przykładzie nie dysponujemy z góry wszystkimi niezbędnymi informacjami - możemy zdecy­ dować się na pobieranie listy produktów kategorii na żądanie, zamiast zmuszać użytkownika do czekania na pobranie wszystkich produktów, zanim jakakolwiek ich część zostanie wyświetlona. W takich sytuacjach testowanie jest zazwyczaj nieco łatwiejsze, jeśli dodamy procedurę obsługi zda­ rzenia, dzięki której będziemy dokładnie wiedzieć, kiedy będą pobierane dane podrzędne. Z doświad­ czeń autorów wynika, że bardzo chytry kod, który stosuje wiązanie danych niejawnie, bazując na niejasnych sztuczkach, jest w stanie wyrządzić więcej szkody niż pożytku. < L i s tBox . I temTempl ate>

W tym przypadku nie korzystamy z właściwości Di s p l ayMemberPath, której w lewej liście uży­ liśmy do wyświetlania tekstu w elementach, lecz użyliśmy elementu I temTempl ate. Ma on dla elementów danych prezentowanych w kontrolkach mniej więcej takie samo znaczenie co właściwość Templ ate dla samych kontrolek - definiuje ich wygląd. Dla każdego obiektu znaj­ dującego się w kolekcji I temsSource zostanie utworzony jeden obiekt DataTempl ate, a w jego właściwości DataContext zostanie zapisany dany obiekt źródłowy. Oznacza to, że użyte w powyż­ szym kodzie dwa wyrażenia wiążące pobiorą odpowiednio: wartość właściwości Text obiektu źródłowego, która zostanie użyta w elemencie TextBl ock, oraz wartość właściwości T h umbna i l , która zostanie wykorzystana w elemencie I mage.

Wiązanie danych

775

'

..

Fakt, że nasz obiekt źródłowy posiada właściwość Thumbnai l , jest dobrym przykładem demonstrującym, dlaczego jest nam potrzebny model widoku niezależny od modelu . Otóż model używany w aplikacji może operować na obrazie rastrowym. I faktycz­ nie, w naszej przykładowej aplikacji jest stosowany obiekt modelu (nie pokazaliśmy go tutaj, lecz można go znaleźć w kodach źródłowych dołączonych do książki), którego właściwość zawiera nieprzetworzone dane binarne obrazka rastrowego . Chociaż technologia WPF jest w stanie automatycznie przekonwertować tablicę bajtów na obiekt ImageSource wymagany przez element I mage, to jednak technologia Silverlight tego nie potrafi i dlatego zadanie konwersji danych na odpowiedni typ będzie musiało zostać wykonane przez model widoku. A zatem choć model widoku nie jest w żaden sposób uzależniony od kodu widoku, to jednak dostarcza on danych dostosowanych do jego wymagań, i to nawet w wymiarze obejmującym wykorzystanie specyficznych typów używanych w technologiach WPF lub Silverlight.

Istnieje pewne połączenie pomiędzy szablonami danych oraz kontrolkami zawierającymi elementy: każda taka kontrolka potrafi wczytać szablon danych. (Rzeczywistym sercem tego mechanizmu jest element Content Presenter stosowany w każdym szablonie kontrolki z elemen­ tami, o czym mieliśmy okazję się przekonać na przykładzie kodu przedstawionego na lis­ tingu 20.20. To właśnie on wie, jak należy wczytywać szablony danych) . Kontrolki z elementami są w stanie tworzyć szablony danych dla wszystkich wyświetlanych w nich elementów, gdyż kontenery tych elementów (np . L i st Box I t em lub TreeVi ewl tem) są kontrolkami z zawartością. Dzięki temu szablony danych można stosować w bardzo wielu miejscach - na przykład przy prezentowaniu zawartości przycisków, w nagłówkach i zawartości kart, w etykietach elementów drzew i tak dalej . Tak jak kontrolki z elementami udostępniają właściwość I t ern '"+Templ a t e, tak w kontrolkach z zawartością można znaleźć właściwości Con t entTempl ate oraz HeaderTempl ate, w których można zapisywać szablony danych.

Podsu mowan ie W tym rozdziale pokazaliśmy, jak można tworzyć strukturę interfejsu użytkownika w języku XAML oraz jak skojarzony z nim kod ukryty może obsługiwać zdarzenia i dostarczać elementom interfejsu potrzebnych im informacji . Przedstawiliśmy także kilka najważniejszych typów kontrolek, a przede wszystkim kontrolki z zawartością, które mogą prezentować dowolne elementy. Pokazaliśmy także, w jaki sposób można skojarzyć dane aplikacji z elementami inter­ fejsu użytkownika przy użyciu mechanizmu wiązania danych.

776

Rozdział 20. WPF i Silverlight

ROZDZIAŁ 21.

Tworzenie apl ikacji w ASP.NET

Programiści coraz więcej swoich aplikacji piszą w taki sposób, by mogły one działać w inter­ necie, a korzystanie z nich odbywało się za pośrednictwem przeglądarek WWW Jak dowiedzie­ liśmy się w rozdziale 20 ., technologia Silverlight pozwala pisać kod C#, który będzie wyko­ nywany w przeglądarce WWW po stronie klienta. Jeśli natomiast chodzi o obsługę aplikacji internetowych po stronie serwera, to .NET Framework udostępnia technologię ASP.NET. .

W tym rozdziale skoncentrujemy się na przedstawieniu punktu, w którym spotykają się ASP.NET oraz język C# - technologii Web Forms. ASP.NET jest zagadnieniem bardzo obszer­ nym i jeśli Czytelnik chciałby znaleźć jego obszerną i wyczerpującą prezentację, radzimy się­ gnąć po książkę Programming ASP.NET 3.5, Fourth Edition napisaną przez Jessego Liberty, Dana Maharry'ego i Dana Hurwitza lub Learning ASP.NET 3.5, Second Edition napisaną przez Jessego Liberty, Dana Hurwitza i Dana MacDonalda (obie zostały wydane przez wydawnictwo O'Reilly) .

Podstawy tech nologi i Web Forms Technologia Web Forms przenosi ideę szybkiego programowania aplikacji (RAD - Rapid Application Development) do świata programowania aplikacji internetowych. Z poziomu Visual Studio lub programu Visual Web Developer, korzystając z techniki „przeciągnij i upuść", można umieszczać elementy sterujące na formularzach i pisać specjalny „kod ukryty" (ang. code-behind) wspomagający ich działanie . Aplikacje tego typu są zazwyczaj wdrażane na ser­ werze WWW (przeważnie jest to serwer IIS, dostępny niemal we wszystkich wersjach systemu Windows, lub Cassini, wbudowany w Visual Studio w celu testowania pisanych aplikacji), a użytkownicy prowadzą z nimi interakcję, korzystając z przeglądarek WWW . • ""

• .·

.._,..�;

.

.___� ,

Technologia ASP.NET obsługuje także inne modele niż Web Forms - można na przykład operować bezpośrednio na poziomie żądań HTTP. Co więcej, platforma .NET 4 udostępnia nowy model MVC (skrót od angielskich słów model, view, controlZer - model, widok, kontroler). Model ten jest znacznie bardziej skomplikowany, lecz jednocześnie zapewnia znacznie większe możliwości oraz elastyczność, przez co sta­ nowi doskonałe rozwiązanie w przypadku tworzenia złożonych aplikacji interneto­ wych. Jako że niniejsza książka nie jest poświęcona wyłącznie technologii ASP.NET, przedstawimy tu wyłącznie proste przykłady aplikacji tworzonych przy użyciu tech­ nologii Web Forms.

777

Technologia Web Forms udostępnia model, w którym strony WWW są generowane dyna­ micznie na serwerze i dostarczane do przeglądarki za pośrednictwem internetu. Zapewnia ona możliwość tworzenia stron ASPX zawierających kod HTML oraz kontrolki sieciowe (ang. web controls), a także pisania kodu C# implementującego reakcje na czynności wykonywane przez użytkownika na stronie i dodającego do niej dynamiczne treści. Kod C# jest wykonywany na serwerze, a dane przez niego wytworzone są integrowane z kontrolkami umieszczonymi na stronie i wspólnie generują kod HTML, który następnie zostaje przesłany do przeglądarki użytkownika . Koniecznie należy zwrócić uwagę na trzy kluczowe informacje podane w poprzednim akapicie i pamiętać o nich podczas lektury tego rozdziału: •

Strony WWW mogą zawierać zarówno kod HTML, jak i kontrolki sieciowe (opisane w dal­ szej części rozdziału) .



W technologii ASP.NET kod jest wykonywany na serwerze w środowisku zarządzanym. (Oczywiście technologii tej można używać w połączeniu z technologią AJAX bądź Silverlight, jeśli chcemy także skorzystać z kodu działającego po stronie klienta) .



Kontrolki ASP.NET generują standardowy kod HTML wyświetlany w przeglądarce.

W przypadku formularzy sieciowych tworzonych w technologii Web Forms interfejs użytkow­ nika jest dzielony na dwa elementy: część wizualną (nazywaną także interfejsem użytkownika Ul) oraz logikę jej obsługi. Rozwiązanie to określane jest jako separacja kodu (ang. code separation) i jest czymś bardzo pożytecznym. Interfejs użytkownika stron ASP .NET jest umieszczany w plikach z rozszerzeniem aspx . W przypadku żądania wykonania formularza serwer generuje kod HTML, a następnie przesyła go do przeglądarki użytkownika . W kodzie stron ASP.NET umieszczane są specjalne kon­ trolki Web Forms zdefiniowane w przestrzeniach nazw System . Web oraz System . Web . U l biblioteki klas .NET. Pisanie stron korzystających z Web Forms przy użyciu Visual Studio nie mogło być prostsze . Wystarczy, że otworzymy formularz, przeciągniemy na niego kilka kontrolek i napiszemy kod, który będzie je obsługiwał . I gotowe! Właśnie napisaliśmy aplikację internetową. Z drugiej strony, nawet w przypadku korzystania z Visual Studio napisanie solidnej i kom­ pletnej aplikacji internetowej może być onieśmielającym zadaniem. Technologia Web Forms udostępnia niezwykle bogaty interfejs użytkownika - liczba i stopień złożoności oferowanych przez nią kontrolek znacząco wzrosły w ciągu kilku ostatnich lat, podobnie zresztą jak oczeki­ wania użytkowników odnośnie do ich wyglądu i sposobu działania . Co więcej, aplikacje internetowe są z założenia aplikacjami rozproszonymi. Zazwyczaj klient takich aplikacji nie znajduje się w tym samym budynku co serwer. W przypadku większości z nich podczas tworzenia interfejsu użytkownika należy uwzględniać czasy opóźnień trans­ misji sieciowych, przepustowość oraz wydajność serwera, gdyż całkowity czas obsługi żądania może wynieść nawet kilka sekund.

!i.--..JJM'

778

I

:

Aby uprościć opisywane tu zagadnienia i umożliwić skoncentrowanie się na aspektach związanych z językiem C#, całkowicie pominiemy tu sprawy dotyczące przetwarzania wykonywanego po stronie klienta i zajmiemy się wyłącznie kontrolkami ASP.NET obsługiwanymi po stronie serwera.

Rozdział 21. Tworzenie aplikacji w ASP.N ET

Zdarzen ia form ularzy sieciowych Formularze sieciowe są sterowane zdarzeniami. Zdarzenie reprezentuje „coś, co się zdarzyło" (więcej informacji na ten temat można znaleźć w rozdziale 5 .) . Zdarzenie zostaje zgłoszone, gdy użytkownik kliknie przycisk, wybierze opcję z listy bądź wejdzie w jakąkolwiek inną interakcję z interfejsem użytkownika . Oczywiście w przypadku aplikacji internetowych interakcje te mają miejsce w przeglądarce WWW działającej na kompu­ terze użytkownika, niemniej jednak zdarzenia ASP.NET są obsługiwane na serwerze. Aby takie rozwiązanie mogło działać, konieczne jest wykonanie pełnego cyklu komunikacji z serwerem. Przeglądarka musi przesłać do serwera żądanie, a ten z kolei musi na nie odpowiedzieć dopiero wtedy zdarzenie zostanie całkowicie obsłużone . To może jednak trochę potrwać, więc w porównaniu z obsługą zdarzeń w klasycznych aplikacjach dla systemu Windows jeste­ śmy nieco ograniczeni - choć obsługiwanie po stronie serwera niektórych zdarzeń takich jak przesuwanie wskaźnika myszy po ekranie po prostu byłoby praktyczne, to jednak ASP.NET udostępnia jedynie ograniczoną liczbę zdarzeń, na przykład kliknięcie przycisku lub zmianę zawartości pola tekstowego. Są to zdarzenia, które mogą skutkować poważnymi zmianami i których obsługa może być warta wysłania żądania na serwer.

Zdarzenia przesyłane i nieprzesyłane Zdarzenia przesyłane (ang. postback events) to takie, których zgłoszenie powoduje natychmia­ stowe przesłanie formularza na serwer. Zaliczają się do nich na przykład zdarzenia związane z obsługą kliknięć takie jak zdarzenie C l i c k przycisków. Istnieje także liczna grupa zdarzeń nieprzesyłanych, czyli takich, których zgłoszenie nie powoduje natychmiastowego przesłania for­ mularza na serwer. '

..

Można zmusić kontrolki generujące zdarzenia nieprzesyłane, by ich zdarzenia powo­ dowały przesłanie formularza. W tym celu wystarczy przypisać właściwości AutoPost 4Back wartość true.

Zdarzenia nieprzesyłane są zgłaszane w momencie, gdy ASP.NET odkryje, że należy je zgłosić, co może jednak nastąpić ze znacznym opóźnieniem względem momentu, w którym użytkownik wykonał czynność prowadzącą do ich wygenerowania. Na przykład kontrolka TextBox udostępnia zdarzenie TextChanged. Raczej nie będziemy oczekiwać, że formularz zostanie automatycznie prze­ słany na serwer w momencie, gdy użytkownik zacznie coś wpisywać w polu tekstowym, dla­ tego też nie jest to zdarzenie przesyłane. Jeśli użytkownik wypełni kilka pól formularza, serwer nie będzie o tym nic wiedział - ta zmiana stanu następuje po stronie klienta. ASP.NET odkryje ją dopiero wtedy, gdy użytkownik kliknie przycisk, by przesłać formularz na serwer. Dopiero wówczas zostaną zgłoszone zdarzenia TextChanged dla wszystkich wypełnionych pól tekstowych. Oznacza to, że w ramach obsługi jednego żądania może zostać obsłużonych wiele różnych zdarzeń.

Stan widoku Użytkownicy oczekują, że kontrolki tworzące interfejs użytkownika aplikacji będą pamiętały swój stan - zniknięcie wartości wpisanej w polu tekstowym lub opcji zaznaczonej na liście 1 może być bardzo mylące . Niestety WWW jest z natury środowiskiem „bezstanowym" . 1 Choć jest to uzasadnione przez architekturę sieci, nie wpływa to dobrze na łatwość korzystania z niej .

Podstawy technologii Web Forms

I

779

Oznacza to, że każde przesłanie żądania na serwer powoduje utratę stanu z poprzedniego żąda­ nia, chyba że programista włoży wiele pracy i starań, by zachować posiadaną wiedzę o sesji. WWW obejmuje całe mnóstwo witryn zawierających formularze, których zawartość jest całko­ wicie tracona w przypadku, gdy przesłane na serwer dane zawierają jakikolwiek błąd. Progra­ miści muszą wykonać całkiem sporo pracy, by zapobiec takim sytuacjom. Jednak ASP.NET udostępnia mechanizmy pozwalające na automatyczną obsługę pewnych aspektów stanu. Za każdym razem, gdy formularz zostaje przesłany na serwer, ten przed odesłaniem odpowie­ dzi do przeglądarki go odtwarza. ASP.NET oferuje mechanizm, który automatycznie zacho­ wuje stan kontrolek obsługiwanych po stronie serwera (V i ewSt ate) . A zatem jeśli formularz zawiera listę, a użytkownik wybrał jedną z dostępnych na niej opcji, to opcja ta pozostanie zaznaczona także po przesłaniu formularza na serwer i ponownym wyświetleniu go w prze­ glądarce.

Cykl życia stron w technologi i Web Forms Każde żądanie dotyczące strony docierające na serwer powoduje wygenerowanie na nim ciągu zdarzeń. Zdarzenia te składają się na całkowity cykl życia strony oraz wszystkich jej komponen­ tów. Cykl ten zaczyna się od żądania zwrócenia strony, które sprawia, że serwer ją wczytuje. Obsługa żądania kończy się natomiast usunięciem strony z pamięci serwera. Ostatecznym celem obsługi żądania jest wygenerowanie i przesłanie do przeglądarki wynikowego kodu HTML. • .·

.„

'-"�;

_. . .___� ,

'

Jako że ASP.NET jest technologią serwerową, dla niej cykl życia strony wygląda zupełnie inaczej niż z punktu widzenia użytkownika. W chwili gdy użytkownik zobaczy stronę, serwer już dawno skończył ją obsługiwać. Kiedy kod HTML dotrze do przeglądarki, równie dobrze można by wyłączyć serwer i odłączyć go od internetu, a użytkownik w ogóle by tego nie zauważył aż do momentu przesłania kolejnego żądania.

Cykl życia strony jest wyznaczany poniższymi zdarzeniami. Na każdym z etapów jej prze­ twarzania ASP.NET wykonuje konkretne operacje, jednak z każdym z tych zdarzeń można skojarzyć procedurę obsługi, by wykonać w niej jakieś dodatkowe czynności .

Inicjalizacja Inicjalizacja jest pierwszym etapem cyklu życia strony lub kontrolki. To właśnie podczas niej są inicjowane wszelkie ustawienia, które będą potrzebne podczas obsługi żądania. Wczytanie Vi ewState Na tym etapie zostaje określona wartość właściwości V i ewState. Wartość ta jest przecho­ wywana w ukrytym polu formularza HTML. Kiedy ASP.NET po raz pierwszy generuje kod strony, umieszcza w nim to ukryte pole, a następnie korzysta z niego, by zachować stan strony pomiędzy kolejnymi żądaniami przesyłanymi na serwer. Ciąg znaków zapisany w tym polu jest przetwarzany przez platformę i na jego podstawie określana jest wartość właściwości V i ewSt ate. Zapewnia to możliwość zarządzania stanem poszczególnych kon­ trolek pomiędzy kolejnymi wyświetleniami strony, dzięki czemu ich zawartość nie jest za każdym razem przywracana do wartości domyślnej . Przetworzenie danych zwrotnych Podczas tego etapu zostają przetworzone dane przesłane na serwer - tak zwane dane zwrotne (ang. postback data) .

780

I

Rozdział 21. Tworzenie aplikacji w ASP.N ET

Wczytanie Na tym etapie następuje wywołanie metody CreateC h i l dControl s, które powoduje utwo­ rzenie i zainicjowanie kontrolek serwerowych w drzewie sterowania . Stan formularzy zostaje odtworzony, a ich kontrolki zawierają dane przesłane z przeglądarki w żądaniu. Wysłanie modyfikacji danych zwrotnych Jeśli pojawiły się jakiekolwiek różnice pomiędzy poprzednim i bieżącym stanem, to zostaje wywołana metoda Ra i s e PostDataChanged Event, która powoduje zgłoszenie odpowiednich zdarzeń. Obsługa zdarze1i Na tym etapie zostaje obsłużone zdarzenie (zgłoszone po stronie klienta), które spowodo­ wało przesłanie żądania na serwer . Generowanie wstępne To ostatni moment, by zmienić właściwości kontrolek, nim zostaną one wygenerowane. (W przypadku formularzy sieciowych „wygenerowanie" oznacza utworzenie odpowied­ niego kodu HTML, który zostanie następnie przesłany do przeglądarki) . Zapisanie stanu Na początku cyklu życia strony jej zachowany stan został odczytany i odtworzony z ukry­ tego pola formularza . Na tym etapie jest on ponownie zapisywany w polu jako łańcuch znaków, co kończy pełny cykl przesyłania stanu pomiędzy klientem i serwerem. Generowanie Podczas tego etapu jest generowany kod wynikowy, który zostanie przesłany do prze­ glądarki. Zwolnienie To ostatni etap cyklu życia strony. Zapewnia on programiście możliwość wykonania ostatecznych porządków i zwolnienia referencji do wszelkich kosztownych zasobów takich jak połączenia z bazami danych.

Tworzenie apl ikacj i internetowych Visual Studio udostępnia dwa sposoby tworzenia aplikacji internetowych. Nie jest to jedynie kwestia wyboru jednego z dwóch przycisków w menu zapewniających dostęp do tej samej możliwości - obie opcje działają całkowicie odmiennie, a Visual Studio nie oferuje wystarcza­ jących informacji o różnicach pomiędzy nimi w momencie, gdy trzeba dokonać wyboru. Jednym z tych dwóch sposobów jest skorzystanie z opcji New Project, która udostępnia różne szablony projektów ASP.NET umieszczone w sekcji Visual C#/Web, pozwalające na generowanie róż­ nych rodzajów projektów aplikacji internetowych. Są to pełnoprawne projekty aplikacji Visual Studio generowane i budowane dokładnie tak samo jak wszelkie inne projekty (biblioteki, aplikacje konsolowe czy też aplikacje WPF) . Projekty tego typu są nieco „lżejsze" - nie ma w nich pliku .csproj reprezentującego projekt. Nie istnieje też konieczność ich budowania, gdyż taki projekt składa się wyłącznie z plików źródłowych i to one później będą kopiowane na serwer WWW. W przykładach zamieszczonych w tym rozdziale wykorzystamy projekt apli­ kacji internetowej, gdyż jest on najbardziej podobny do innych typów projektów, które mieli­ śmy okazję poznać we wcześniejszej części książki .

Tworzenie aplikacji internetowych

I

781

Aby utworzyć prosty formularz internetowy, który wykorzystamy w następnym przykła­ dzie, należy uruchomić Visual Studio .NET i wybrać z menu głównego opcję File/New/Project. W oknie dialogowym New Project należy następnie zaznaczyć opcję Visual C#/Web i wybrać szablon ASP.NE T E mpty Web Application. Zgodnie z tym, co sugeruje nazwa szablonu, Visual Studio utworzy pustą aplikację internetową. W jej skład będzie początkowo wchodził wyłącznie plik Web.config zawierający ustawienia konfiguracyjne witryny. Aby dodać do aplikacji nowy formularz, należy wybrać opcję Pro­ ject/Add New Item, po czym wybrać z listy szablonów wyświetlonej po lewej stronie okna opcję Visual C#/Web. Następnie należy wybrać szablon Web Form i nadać mu nazwę HelloWeb.aspx. W rezultacie Visual Studio utworzy także plik kodu ukrytego o nazwie HelloWeb.aspx.cs, który będzie można zobaczyć w panelu Solution Explorer po rozwinięciu opcji HelloWeb.aspx. (Dodat­ kowo pojawi się także plik HelloWeb.aspx.designer.cs, w którym Visual Studio będzie umiesz­ czać wszelki kod, który musi wygenerować automatycznie. Nie należy w nim umieszczać żad­ nego własnego kodu, gdyż Visual Studio usuwa ten plik i odtwarza go za każdym razem, gdy musi wprowadzić do wygenerowanego kodu jakieś zmiany) .

Pliki kod u ukrytego Przyjrzyjmy się nieco dokładniej plikom aspx oraz plikom kodu ukrytego utworzonym przez Visual Studio. Najpierw zobaczmy kod HTML zapisany w pliku HelloWeb.aspx. Podczas edycji plików aspx Visual Studio może je wyświetlać w trzech różnych widokach. Domyślnym jest widok źródła (Source) prezentujący kod HTML. U dołu okna edytora znajdują się trzy przyciski, które pozwalają przełączać się pomiędzy trzema dostępnymi widokami: widokiem projektu (Design) prezentującym zawartość strony w taki sposób, w jaki będzie ona wyświetlana w przeglądarce, widokiem źródła (Source) prezentującym nieprzetworzony kod HTML strony oraz widokiem podzielonym (Split) prezentującym stronę jednocześnie na dwa opisane wcześniej sposoby. Jak można zauważyć, formularz został utworzony na stronie przy użyciu standardowego znacznika form języka HTML:

Formularze internetowe ASP.NET wymagają, by na stronie znajdował się przynajmniej jeden formularz HTML umożliwiający zarządzanie interakcją z użytkownikiem, dlatego też Visual Studio utworzyło go podczas dodawania nowej strony aspx. Atrybut runat = " server" jest kluczem do całej magii, która później będzie wykonywana na serwerze. Każdy znacznik zawierający ten atrybut jest traktowany jako kontrolka serwerowa, która powinna zostać wykonana przez ASP.NET Framework na serwerze . •

,



" '"'

t..•„,; . , ....,'.___�_

Choć form jest standardowym znacznikiem HTML, to atrybut run at do tego standardu nie należy. Jednak ASP.NET usuwa ten atrybut z kodu HTML przed jego przesłaniem do przeglądarki. Ma on jakiekolwiek znaczenie wyłącznie na serwerze .

Wewnątrz formularza Visual Studio umieszcza otwierający i zamykający znacznik d i v, pomiędzy którymi można wstawiać własne kontrolki oraz tekst. Po utworzeniu pustego formularza pierwszą czynnością, jaką Czytelnik zapewne będzie chciał wykonać, będzie umieszczenie na nim jakiegoś tekstu. Po przełączeniu się do widoku źródła można dodawać kod bezpośrednio do pliku HTML. W ten sposób możemy na przy­ kład dodać jakąś zawartość do elementu d i v umieszczonego w sekcji body strony aspx, tak jak to pokazano na listingu 21 .1 (dodany fragment został wyróżniony pogrubioną czcionką) . 782

I

Rozdział 21. Tworzenie aplikacji w ASP.N ET

Listing 2 1 . 1 . Dodawanie zawartości HTML < ! DOCTY PE html PUB L I C " - //W3C // DTD XHTM L 1 . 0 Trans i t i onal // EN " " http : //www . w3 . org / TR/xhtml l / DTD / xhtml l - trans i t i onal . dt d " > < / t i t l e> < / head> Wi taj , świ eci e ! Teraz j est :

< / d i v> < / form> < / body> < / html >

W ten sposób na stronie zostanie wyświetlone pawi tanie oraz aktualny lokalny czas: Wi taj , świ eci e ! Teraz j es t 20 1 1 - 12 - 18 23 : 40 : 42

Znaczniki działają dokładnie tak samo jak we wcześniejszej wersji technologii ASP oznaczają kod, który został pomiędzy nimi umieszczony (w naszym przypadku jest to kod C#) . Znak równości ( ) znajdujący się bezpośrednio za znacznikiem otwierającym sprawia, że ASP.NET przetworzy umieszczone za nim wyrażenie i wyświetli jego wartość. Wykonajmy zatem tę stronę, naciskając klawisz F5. =

Dodawanie kontrolek Kontrolki serwerowe można umieszczać na formularzu na trzy sposoby: samodzielnie pisząc w pliku aspx odpowiedni kod, przeciągając je z panelu Toolbox (przy czym w tym przypadku można je umieszczać na stronie zarówno w widoku źródła, jak i w widoku projektu) lub ewentualnie pisząc kod, który doda kontrolki w trakcie działania programu. W ramach przy­ kładu załóżmy, że chcemy wyświetlić na stronie trzy przyciski opcji zapewniające użytkow­ nikowi składającemu zamówienie możliwość wyboru jednej z trzech firm kurierskich. W tym celu w kodzie strony, wewnątrz znacznika , można by umieścić następujący kod HTML: < / asp : Rad i oButton> < / asp : Rad i oButton> < / asp : Rad i oButton>

Znacznik asp deklaruje serwerową kontrolkę ASP.NET, która podczas przetwarzania strony zostaje zastąpiona zwyczajnym kodem HTML. Po uruchomieniu aplikacji przeglądarka wyświetli zbiór trzech przycisków opcji - wybór jednego z nich spowoduje usunięcie zaznaczenia pozostałych. Dokładnie ten sam efekt można uzyskać znacznie łatwiej, przeciągając trzy przyciski opcji z panelu Toolbox Visual Studio i upuszczając je na formularzu, a jeszcze łatwiejszym rozwiąza­ niem będzie przeciągnięcie i upuszczenie na formularzu listy przycisków opcji, która będzie Tworzenie aplikacji internetowych

I

783

zarządzać wszystkimi przyciskami jako jedną grupą. Kiedy zaznaczymy kontrolkę przycisku opcji w widoku projektu, pojawi się inteligentny znacznik (ang. smart tag), prosząc nas o podanie źródła danych (zapewnia to nam możliwość powiązania przycisku z kolekcją, na przykład pobraną z bazy danych) lub wpisanie ich samemu. Kliknięcie przycisku Edit items powoduje wyświetlenie okna Listltem Collection Editor, w którym możemy dodać potrzebne nam trzy przyciski opcji. Każdy z przycisków będzie miał domyślną nazwę L i st I t em, jednak zarówno ich opisy, jak i war­ tości będzie można samemu podać w odpowiednich właściwościach. Można tam także określić, które z trzech pól będzie początkowo zaznaczone (patrz rysunek 21 .1) . listlte m C ol l ection E dit o r

_Q Listltem Mem b er�:

G :?:••••••I G 1 Li stltem

Li stltem prop erti es:

� �'. 1 "

M isc

_J

Ena b·l ed

Trn e

Jj@M Text Value

Ad d

11

Remove OK

11

C a n c el

Rysunek 2 1 . 1 . Kolekcja elementów Listltem Wygląd listy przycisków opcji można poprawić, modyfikując wartości właściwości wyświe­ tlonych w panelu Properties, które określają takie aspekty wyglądu jak: używana czcionka, kolor, liczba kolumn, kierunek powtarzania (domyślnie wybrany jest pionowy) itd. Można to również zrobić, korzystając z rozbudowanego wsparcia, jakie Visual Studio zapewnia w zakre­ sie tworzenia arkuszy stylów CSS (przedstawionego na rysunku 21 .2) . Rysunek 21 .2 pokazuje, że można przełączać wyświetlane w prawym dolnym rogu Visual Studio panele Properties oraz Apply Styles. W przedstawionym przykładzie użyliśmy panelu Properties, by określić tekst etykiety ekranowej, a następnie panelu Styles, by utworzyć styl L i st Box, który wyświetla wokół listy przycisków obramowanie oraz określa ich czcionkę i kolor. Skorzystaliśmy także z widoku podzielonego, by móc jednocześnie oglądać stronę w widoku źródła oraz projektu. Sekwencja znaczników wyświetlana automatycznie u dołu okna Visual Studio pokazuje nasze aktualne położenie w strukturze dokumentu. Jak widać, znajdujemy się w elemencie L i s t I t em w kontrolce L i s t Box, która jest umieszczona w elemencie d i v wewnątrz formularza form l . To naprawdę świetne narzędzie.

784

I

Rozdział 21. Tworzenie aplikacji w ASP.N ET



;

! I.Li;tBox (Current Pag• ·I •V

1 1(No n e)

-1 1 c!ĘI I I(Defau lt Fo nt)

I I(Defa u lt ·1 1

Y:' � � u -

·

·

;

B

I

!!

,J;o., ,.f,

I g I ;:: ·

!

�! �

- � I o "ii' � []I I '"" Ob'-� -& E:..; (N E t ) :;;,: nt...:. j= c;,;:_: .:.:.;: � :..; v; e:..; n; ts'-; i...Cl' ....: 'e.;;.: -L..-''--'0_;:..; v; en_; ; ;:. ; ...,.. ----------,-,-1 1* r--i on ' P rog ra m m1„ n g C Sh a r pW• \�ita j , :ś w i e cie ! Teraz j e s t d= l>ateTime . Now . ToS tri n g ( ) %> .... b ' (1 p ro -Ji ect) I QO 5o l ut1.'LEJ � • • � 'ł Pr09rammi119CSharpWeb

I

El

< a sp : Ra d ioButto n list I D = "RadioButton l istl "

runat= " s erve r "

CssC l a s s = "Li s t B ox " >

S p e e dy E xpr e ss

United Package F e d e r eil S h i p ping< / a s p : Listitem> < / asp : R a d ioB utto nlist >



< / html>

Q G

t>-

� Pro p erties.

t>-

� D ispl aySh ipp ers. .:i spx � HelloWeb.aspx � H el l oWeb . a sp:x. c i; � H el l oWeb. aspx. d e;ign er. c s







Referen c es

� Web. config

Ili aso:Radio Butto n li st: RadioButtonListll

r

r

Speedy Express

lJ

United Package l

r F ederal

Shipping



App ly Styl e;

'A �

CSS styles:

IOt

p ions



I

Oear Stytes

El Current Page

@_ListBox

Rysunek 2 1 .2 . Stosowanie właściwości i stylów

Kontrolki serwerowe Technologia Web Forms udostępnia dwa rodzaje kontrolek serwerowych. Pierwszym z nich są serwerowe kontrolki HTML . Wyglądają one niemal identycznie jak zwyczajne znaczniki HTML, jednak posiadają dodatkowy atrybut runat= 11 server 11 • Alternatywą dla serwerowych kontrolek HTML są kontrolki serwerowe ASP.NET (ang. ASP.NET server controls; nazywane także czasami kontrolkami internetowymi, ang. web controls) . Stworzono je po to, by udostępnić nieco wygodniejszy interfejs API do pracy ze standardo­ wymi kontrolkami HTML. Kontrolki internetowe udostępniają nieco bardziej spójny model obiektowy oraz spójne nazewnictwo właściwości . W przypadku kontrolek HTML istnieje na przykład wiele sposobów obsługi wprowadzania danych: < i nput type= " rad i o " > < i nput type= " checkbox "> < i nput type= " button " > < i nput type= " text " >

Każda z tych kontrolek działa inaczej i wymaga zastosowania innych atrybutów. To dosyć przykry efekt nieco przypadkowego rozwoju języka HTML we wczesnych latach istnienia WWW. Kontrolki internetowe mają za zadanie znormalizować zbiór dostępnych elementów

Tworzenie aplikacji internetowych

I

1as

sterujących oraz zapewnić spójne wykorzystanie atrybutów w całym ich modelu obiektowym. Poniżej przedstawione zostały kontrolki internetowe odpowiadające kontrolkom HTML widocz­ nym w poprzednim przykładzie .

Kod HTML przekazywany do przeglądarki użytkownika nie zawiera znaczników zaczynających się od a s p : (i bardzo dobrze, gdyż żadna przeglądarka nie wiedziałaby, co z nimi zrobić) . ASP.NET konwertuje je na standardowy kod HTML, a zatem z punktu widzenia klienta nie ma żadnej różnicy pomiędzy internetowymi kontrolkami ASP.NET a standardowymi kon­ trolkami HTML. Wszystko sprowadza się więc do pytania, z jakiego API będziemy chcieli korzystać na serwerze: czy chcemy, by kod na serwerze operował na tych samych elementach i właściwościach, które są używane po stronie klienta, czy też wolimy, by kontrolki były zgodne z konwencjami używanymi we wszystkich innych klasach biblioteki .NET. W dalszej części rozdziału skoncentrujemy się na kontrolkach internetowych.

Wiązan ie danych Choć niektóre treści prezentowane przez aplikacje internetowe mogą być stałe, to jednak każda interesująca witryna WWW będzie się zmieniać wraz z upływem czasu. Dlatego też jest wysoce prawdopodobne, że będziemy chcieli, by niektóre kontrolki na stronie wyświetlały dane, które od czasu do czasu mogą podlegać zmianom i które najprawdopodobniej będą prze­ chowywane w bazie danych. Wiele kontrolek ASP .NET można powiązać z danymi, co znacznie ułatwia prezentowanie tych danych oraz ich modyfikację . W poprzednim podrozdziale na stałe umieściliśmy na formularzu trzy przyciski opcji, po jednym dla każdej z trzech firm kurierskich, które może wybrać użytkownik. Jednak to nie jest najlepsze rozwiązanie - dostawcy się zmieniają, a poza tym istnieje duże prawdopodo­ bieństwo, że w przyszłości będziemy chcieli nawiązać współpracę także z innymi firmami kurierskimi . Nie chcemy, by każda zmiana relacji biznesowych zmuszała nas do ręcznego modyfikowania tych kontrolek. Znacznie bardziej rozsądnym rozwiązaniem będzie zapisanie listy firm kurierskich w bazie danych i powiązanie ich z przyciskami opcji wyświetlanymi na formularzu. W tym podrozdziale dowiemy się, w jaki sposób można utworzyć te kontrolki dynamicznie i powiązać je z informacjami przechowywanymi w bazie danych, korzystając w tym celu z możliwości wiązania z bazą danych zapewnianej przez kontrolkę Rad i oButtonL i s t . Dodajmy zatem do naszej aplikacji nowy formularz o nazwie DisplayShippers.aspx i wyświetlmy go w widoku podzielonym. Teraz musimy przeciągnąć kontrolkę Rad i oButtonL i st z panelu Toolbox, umieszczając ją na naszym nowym formularzu - bądź to w panelu widoku projektu, bądź to w kodzie źródłowym wewnątrz znacznika . '

..

' ...._'' __...z.r_

786

I

Jeśli z lewej strony okna Visual Studio nie jest widoczny panel Toolbox z kontrolką przycisków opcji, to można go wyświetlić, wybierając z menu głównego opcję View/Toolbox, a następnie rozwijając zakładkę Standard. By uporządkować kontrolki, można kliknąć w panelu Toolbox prawym przyciskiem myszy i z wyświetlonego menu kontekstowego wybrać opcję Sort Items Alphabetically.

Rozdział 21. Tworzenie aplikacji w ASP.N ET

Następnie w widoku projektu należy kliknąć na „inteligentnej" ikonie nowej kontrolki - nie­ wielkiej strzałce wyświetlonej w jej prawym górnym rogu. Z wyświetlonego menu należy wybrać opcję Choose Data Source, a na ekranie pojawi się okno dialogowe Data Source Configuration Wizard przedstawione na rysunku 21 .3. Data So u rce Confi gu rabio n Wi za rd

Select a data so·u rce:

I (None) Select a data field to display in th e Rad i oButto·nlist:

Select a data field for the value o.f th e Ra di oButto·n list:

OK

11

Ca n cel

Rysunek 2 1 .3 . Okno dialogowe Data Source Configuration Wizard W oknie tym należy rozwinąć listę Select a data source i wybrać z niej opcję . Następnie zostaniemy poproszeni o wybór źródła danych spośród typów danych dostępnych na komputerze. Powinniśmy wybrać opcję Database, podać identyfikator i kliknąć przycisk OK. Na ekranie zostanie wyświetlone okno dialogowe Configure Data Source przedstawione na rysunku 21 .4. Możemy albo wybrać istniejące połączenie z bazą danych, albo utworzyć nowe . W naszym przypadku skorzystamy z tej drugiej możliwości - kliknijmy zatem przycisk New Connection, by je skonfigurować. Na ekranie zostanie wyświetlone okno dialogowe Add Connection . Kolejnym krokiem będzie wypełnienie pól formularza: należy wybrać nazwę serwera, podać informacje o sposobie logowania się do niego (w razie wątpliwości należy wybrać opcję Windows Authentication) oraz podać nazwę bazy danych (w naszym przykładzie będzie to Northwind) . Koniecznie powinniśmy także pamiętać o kliknięciu przycisku Test Connection, by przetestować połączenie z bazą danych. Kiedy wszystko będzie działać prawidłowo, można kliknąć przy­ cisk OK, tak jak to pokazano na rysunku 21 .5 . Po kliknięciu przycisku OK właściwości połączenia zostaną wpisane do okna dialogowego Configure Data Source. Warto jeszcze raz się im przyjrzeć, a jeśli wszystko będzie w porządku, Wiązanie danych

I

787

Co rifigure Data So u rce

-S

I

aS ou r

1

Choose Your Data Connectio n

Whidh d�ta ,oo milecłiolil shou'ld yo1u ap,p lication use to ,OO lillilect to the d�tabase?

c· o_ d_ rth_ t r_ i n_ i o_ i n_ ct_ n_ nS_ I N_o_ w_ n e_ g. __________________ 1 1 N ew C on n� G Con nectio n string ------___, '"

__

I

<

Previ ous

11

Next >

I I Fi n i s h 1 1

Cancel

Rysunek 2 1 .4. Wybieranie połączenia ze źródłem danych można kliknąć przycisk Next. Jeśli chcemy zapisać połączenie w pliku konfiguracyjnym web.config, to na kolejnej stronie kreatora powinniśmy podać jego nazwę (np . Nort hWi ndConnec "+t i on Stri ng ) .

Po kliknięciu kolejnego przycisku Next będziemy mieli możliwość określenia tabel i kolumn, które chcemy pobierać, bądź też podania własnego zapytania SQL lub nazwy procedury skła­ dowanej, które zostaną użyte do pobrania danych. W naszym przykładzie powinniśmy rozwinąć listę Tables i przewinąć jej zawartość tak, by była widoczna opcja S h i ppers . Następnie powinniśmy zaznaczyć pola S h i pper I D oraz CompanyName, jak pokazano to na rysunku 21 .6 . W dalszej kolejności należy ponownie kliknąć przycisk Next i przetestować połączenie, by spraw­ dzić, czy z bazy danych są pobierane oczekiwane wartości (co pokazano na rysunku 21 .7) . W końcu nadszedł czas, by połączyć utworzone przed chwilą źródło danych z kontrolką Rad i o "+But ton l i s t . Kontrolka ta (podobnie jak większość innych list) rozróżnia wartość wyświetla­ ną (w naszym przypadku nazwę firmy kurierskiej) oraz wartość wyboru (w naszym przykła­ dzie jest nią identyfikator firmy kurierskiej) . Pola te należy wskazać w kreatorze, wybierając je z list rozwijalnych, tak jak to pokazano na rysunku 21 .8.

788

I

Rozdział 21. Tworzenie aplikacji w ASP.N ET

11...iJ. l....a.I.

Ad d G on nectio n

Enter i nformation to c onnect to the selected data so urce o·r di c k " C hang e" to c h o ose a d ifferent data so.urce a n d/or prnvider. Data source:

Mi cro.soft SQL Server [5q1Client)

Ch ange„ .

.,. I Refresh

Server n am e: .\�q l expres.s Log on to the server

@ Use Wi ndows Authentic atio·n O Use 5QL 5erver Authenticatio n User name: P a ssword:

I I

I I

'------'

D Save my pa ssword





C onnect to a data ba se

I11m;1,µ;ii1.HI

@ Select or enter a d ata base na m e:

„J

O Arta ch a d ata base fi le: Browse„, L ogical name:

I Advanced„ .

I

Test Con n ecti on

I

___,1, 1

.____ o_ K

C anc el

Rysunek 2 1 .5. Okno dialogowe Add Connection

Sprawdzan ie kod u Zanim przejdziemy dalej, należy zwrócić uwagę na kilka zagadnień. Kiedy naciśniemy kla­ wisz F5, by uruchomić aplikację, zostanie ona wyświetlona w przeglądarce, a strona, zgodnie z oczekiwaniami, będzie prezentowała grupę przycisków opcji. Możemy teraz wybrać opcję Choose View/Source, a przekonamy się, że do przeglądarki został przesłany prosty kod HTML przedstawiony na listingu 21 .2 .

Listing 2 1 .2 . Kod źródłowy wygenerowanej strony < ! DOCTY PE html PUB L I C " - //W3C // DTD XHTM L 1 . 0 Trans i t i onal // EN " "http : //www . w3 . org /T R/ xhtml l / DTD/ xhtml l - t rans i t i onal . dt d " > < / t i t l e>< / head>

Wiązanie danych

I

789

C a rnfig ure Data So u rce

-

Sq l DataSourcel

Configur,e the Select State ment

How wo111 kll ycm li ke to retrieve cllata from ycm r cl latabase?

O Spec: ify a custorn SQL statement or stored p ro c: edure @

Spec: ify co l umns from a tah le or view Na m e:

I Ship,p ers

... 1

IO *

IO Return o nly u ni q u e rows

Col umns;:

� C ompanyName IO Pho ne � Shipperld

\1VH ERE„. ORDER BY„ .

Adva n ced„ .

SELECT statement:

SELECT [Ship-p erldL [Cornp anyNam e] FRO M [Shipp ers]

I



Previ ol!Js

11

Next >

I l�_Fin_is_h_�I I

Rysunek 2 1 .6. Konfiguracja polecenia Select < i nput type= " h i dden " name= "_V I EWSTATE " i d= "_V I EWSTATE " val ue= " /wEPDwUJMj MzNj Y 1MzU402QWAg I DD2QWAg I BDxAP Fg ieC18hRG FOYUJvdW5 kZ2QQ FQMOU3Bl ZWR5 I EV4cHJ l c3MOVW5pdGV kI FBhY2thZ2UQRmV kZXJ hbCBTaGl wcGl uZxUDATEBMgEz FCs DA2dnZ2 RkZCO ksd8 I I Lj pH40AdN kxGqj SaORYAA3N2 F8zJz4 l yxsv " /> < i nput type= " h i dden " name= " EVENTVA L I DATION " i d= " EVENTVA L I DAT I ON " val ue= " /wEWBQK02+CfAg L444 i 9AQ L544 i 9AQ L644 i 9AQ L3j KLTDWyl FX ks lYMe8G5o7AkyHj JysQ kO Cl i wu8U/2yTrYA/Y " /> < i nput i d= " Radi oButton l i s t l O " type= " radi o " name= " Radi oButton l i s t l " val ue= " l " /> Speedy Expres s < i nput i d= " Radi oButton l i s t l 1 " type= " radi o " name= " Radi oButton l i s t l " val ue= " 2 " /> Uni ted Pac kage < i nput i d= " Radi oButton l i s t 1_2 " type= " radi o " name= " Radi oButton L i s t l " v a l ue= " 3 " /> Federa l S h i ppi ng

790

Rozdział 21. Tworzenie aplikacji w ASP.N ET

Cancel

C a rnfig ure Data So u rce

l��

-

11� 1�1

Sq l DataSourcel

T,est Query

„=

To p·review the data retu med hy this data so urce, di ck Test Query. To m m pl ete th is wizard, di ck Finish .

5hipperld

Com p a nyN ame

B 2

United Pac ka g e

3

Fed eral :Sh i pp·in g

:Speedy Exp ress

I

5ELECT statement: SELECT [Shipp erldL [Cornp·anyNam e]

I

Test Query



FRO M [Sh i p p ers]



I



P revi ol!Js

11

N ext „

II

Fi n ish

II

Can cel

I

"'

Rysunek 2 1 .7. Testowanie zapytania

Zwróćmy uwagę, że w powyższym kodzie HTML nie ma elementu Rad i oButton L i st; zawiera on tabelę z komórkami, wewnątrz których są umieszczone standardowe kontrolki HTML oraz etykiety. A zatem ASP .NET przekształcił swoje kontrolki na kod HTML zrozumiały dla każdej przeglądarki WWW. Wrogo nastawiony użytkownik może utworzyć żądanie wyglądające dokładnie tak jak prawidłowe dane przesłane z naszego formularza, lecz zawierające zupełnie inne, nieoczekiwane przez nas wartości. Może mu to zapewnić możliwość wyboru opcji, która nie powinna być dla niego dostępna (na przykład opcji dostępnej wyłącznie dla uprzywilejowanych użytkowników), bądź nawet umożliwić przeprowadzenie ataku SQL injection (wstrzyknięcia kodu SQL). A zatem w przypadkach udostępniania w formularzach HTML istotnych informacji takich jak wartości kluczy głównych należy zachować szczególną ostrożność i pamiętać o tym, że dane pochodzące z formularza wcale nie muszą być tymi, które zostały do niego przesłane. Więcej informacji na temat pisania bezpiecznych aplikacji .NET można znaleźć na stronie http://msdn.microsoft.

com/security/. Wiązanie danych

791

llr-@- I

Data So u rce Confi gu ratio n WiLard

l-=�

,Oh,oose

,a

=

I

Data Souroe

... =

Sel ect a data >nu rc e:

I SqlDataSourcel

... 1

Select a data field to display in th e Radi oButto,nlist: C ompanyNa me

...

Select a data field for the value of th e Radi oButto,nlist: ...

Sh i pperld

Refresh Sc h ema

I

OK

II

Ca n cel

I

Rysunek 2 1 .8. Wiązanie przycisków opcji ze źródłem danych

Dodawanie kontrolek i form ularzy Wystarczy dodać do naszej przykładowej strony kilka kolejnych kontrolek, by utworzyć kom­ pletny formularz zapewniający możliwość prowadzenia interakcji z użytkownikiem. W tym celu dodamy bardziej odpowiednie powitanie („Witamy w NorthWind"), pole tekstowe pozwa­ lające na podanie imienia, dwa nowe przyciski (Zamów oraz Anuluj) i tekst, który umożliwi nam wyświetlanie komunikatów dla użytkownika. Wygląd strony po wprowadzeniu tych mody­ fikacji przedstawiliśmy na rysunku 21 .9. Ten formularz nie wygrałby żadnej nagrody dla najlepszego projektu, niemniej jednak pozwoli nam przedstawić kilka kluczowych zagadnień związanych z formularzami Web Forms. ••

, L-------11.J"' '

.._,..�;

792

I

Nigdy nie spotkaliśmy programisty, który by nie uważał, że potrafi zaprojektować idealny interfejs użytkownika. Jednocześnie nigdy nie udało nam się spotkać takiego, który by to rzeczywiście potrafił. Projektowanie interfejsów użytkownika jest jedną z tych umiejętności (tak jak nauczanie), które każdy w jego własnej opinii posiada, jednak które bardzo niewiele naprawdę utalentowanych osób rozwinęło w wystarczającym stopniu. Jako programiści doskonale znamy swoje ograniczenia: piszemy kod, a ktoś inny roz­ mieszcza kontrolki na stronie, zapewniając ich odpowiednią użyteczność. Czytelnikom zainteresowanym tymi zagadnieniami gorąco polecamy książkę Don't Make me Think: A Common Sense Approach to Web Usibility napisaną przez Steve'a Kruga i wydaną przez wydawnictwo New Riders Press oraz Why Software Sucks„. and What You Can Do About It napisaną przez Davida Platta i wydaną przez wydawnictwo Addison-Wesley.

Rozdział 21. Tworzenie aplikacji w ASP.N ET

l o c a l h o Wi tamy w NorthWi nd Twoj e i m i ę : Kuri er :

Wiązanie danych

I

793



Kiedy użytkownik kliknie przycisk Zamów, odczytamy wartość wpisaną przez niego w polu tekstowym oraz wyświetlimy komunikat potwierdzający wybór jednej z firm kurierskich. Trzeba pamiętać, że w momencie projektowania formularza nie jest ona znana, gdyż nazwy wszystkich dostępnych firm są pobierane z bazy danych w trakcie działania aplikacji, niemniej jednak możemy pobrać z kontrolki Rad i oButtonL i st wybraną nazwę lub identyfikator. Aby to zrobić, należy wyświetlić formularz w widoku projektu i dwukrotnie kliknąć przycisk Zamów. W rezultacie Visual Studio wyświetli plik kodu ukrytego oraz utworzy procedurę obsługi zdarzeń Cl i c k przycisku . ••



. •

t..tt.�;

.

L---...iJ"' '

Aby uprościć kod naszej przykładowej aplikacji, nie będziemy sprawdzać, czy użytkownik wpisał swoje imię w polu tekstowym. Informacje na temat kontrolek, które znacznie upraszczają taką weryfikację poprawności danych, można znaleźć w książce

ASP.NET. Programowanie.

W kodzie obsługującym zdarzenie określamy treść wyświetlanego komunikatu, umieszczając w niej imię odczytane z pola tekstowego oraz tekst i wartość kontrolki Rad i oButtonL i s t : protected vo i d btnOrder_Cl i ck (obj ect sende r , EventArgs e ) { l bl Msg . Text = "Wi taj , " + txtName . Text . Tr i m ( ) + " . Dz i ę kuj emy z a zamówi en i e . " + "Wybrałeś fi rmę kuri ers ką " + rbl S h i ppers . Sel ected i tem . Text + " o i dentyfi katorze " + rbl S h i ppers . Sel ectedVal ue + " . " ;

Po uruchomieniu naszej aplikacji można zauważyć, że początkowo żaden z przycisków opcji nie jest zaznaczony. Podczas wiązania listy ze źródłem danych nie określiliśmy, która z warto­ ści powinna być traktowana jako domyślna . Można to zrobić na kilka różnych sposobów, jednak najprostszym z nich jest dodanie jednego wiersza kodu do metody Page_ Load automa­ tycznie tworzonej przez Visual Studio: protected vo i d Page- Load (obj ect sende r , EventArgs e) { rbl S h i ppers . Se l ected i ndex = O ;

Zastosowanie powyższego kodu sprawi, że zostanie zaznaczony pierwszy przycisk opcji w kontrolce Rad i oBut ton L i s t . W powyższym rozwiązaniu występuje jeden dosyć subtelny pro­ blem. Otóż kiedy uruchomimy aplikację, zostanie zaznaczony pierwszy przycisk, a jeśli następ­ nie wybierzemy drugi lub trzeci i klikniemy Wyślij, to okaże się, że po ponownym wyświe­ tleniu strony znowu będzie zaznaczony przycisk pierwszy. Może się zatem wydawać, że nie da się wybrać żadnej innej opcji poza pierwszą. Dzieje się tak dlatego, że za każdym razem, gdy strona jest wyświetlana, zostaje wykonane zdarzenie Onload, a procedura jego obsługi zawsze zaznacza pierwszy z przycisków opcji w kontrolce Rad i oButtonL i s t . Jednak nam chodzi o to, by ten pierwszy przycisk został zaznaczony wyłącznie podczas pierw­ szego wyświetlenia strony, lecz nie podczas kolejnych wyświetleń następujących po kliknięciu przycisku Wyślij.

794

I

Rozdział 21. Tworzenie aplikacji w ASP.N ET

Aby rozwiązać ten problem, wystarczy umieścić wiersz kodu zaznaczający pierwszy przycisk kontrolki Rad i oButton L i st wewnątrz instrukcji warunkowej sprawdzającej, czy formularz został przesłany na serwer: protected vo i d Page- Load (obj ect sende r , EventArgs e) { i f ( ! I s Pos tBack) { rbl S h i ppers . Sel ectedi ndex = O ;

W momencie wykonywania strony sprawdzana jest wartość właściwości I s PostBack. Za pierw­ szym razem przyjmie ona wartość fal s e, zatem pierwsze pole kontrolki zostanie zaznaczone . Kiedy zaznaczymy jakiś przycisk opcji i klikniemy przycisk Wyślij, strona zostanie przesłana na serwer w celu jej przetworzenia (co spowoduje wykonanie procedury obsługi btnOrder_C l i c k ) , a następnie zostanie odesłana do klienta . W tym przypadku właściwość I s PostBack przyjmie wartość t rue, co sprawi, że kod umieszczony wewnątrz instrukcji i f nie zostanie wykonany. Dzięki temu, jak pokazano to na rysunku 21 .10, po ponownym wyświetleniu strony będzie na niej zaznaczony przycisk wybrany wcześniej przez użytkownika . � http ://localh ost:24840/D i � p l ayShipper� P

...

§l C X

X

Twoj e im ię : Jla n ek Kow al s ki

ei Spe edy Express

Kurier:

ei F e dernl Shipping @ United Package

I Zamów 1 1 Anu l uj I

Witaj , f . an ek Kowal ski. Dziękuj emy za zamówi enie . 1Nybrałeś firmę kurierską United Package o identyfikatorze 2 .

� 100%

...

.

Rysunek 2 1 . 1 0. Wybór użytkownika zachowany po ponownym wyświetleniu strony Listing 21 .4 przedstawia kompletny kod ukryty obsługujący nasz przykładowy formularz.

Listing 2 1 .4. Kod ukryty umieszczony w pliku DisplayShippers.aspx.cs us i ng Sys tem ; namespace Programm i ngCSharpWeb { publ i c part i al cl ass D i spl ayS h i ppers : System . Web . U l . Page { protected v o i d Page- Load (obj ect sender, EventArgs e) { i f ( ! I s PostBack)

Wiązanie danych

I

795

rbl Sh i ppers . Sel ected i ndex = O ;

protected v o i d btnOrder-Cl i c k (obj ect sende r , EventArg s e) { l bl Msg . Text = "Wi taj , " + txtName . Text . Tr i m () + " . Dzi ę kuj emy za zamówi en i e . " + "Wybrał'es fi rmę kuri ers ką " + rbl S h i ppers . Sel ectedi tem . Text + " o i dentyfi katorze " + rbl S h i ppers . Sel ectedVal ue + " . " ;

Podsu mowan ie W tym rozdziale pokazaliśmy, jak można utworzyć prostą aplikację ASP.NET, korzystając z technologii Web Forms. Powiązaliśmy listę przycisków opcji z tabelą bazy danych i dodaliśmy kod obsługujący zdarzenia po stronie serwera zapewniający nam możliwość reagowania na czynności wykonywane na stronie przez użytkownika .

796

I

Rozdział 21. Tworzenie aplikacji w ASP.N ET

ROZDZIAŁ 22.

Windows Forms

Technologia Windows Forms zapewnia możliwość tworzenia aplikacji biurowych przy wyko­ rzystaniu platformy .NET. Jeśli Czytelnik czyta tę książkę po kolei, rozdział po rozdziale, to powyższe stwierdzenie na pewno wyda mu się znajome - w końcu dokładnie te same moż­ liwości zapewnia technologia WPF. Obie te technologie mają pewne punkty wspólne, jednak działają całkowicie odmiennie. Windows Forms jest w zasadzie opakowaniem dla klas inter­ fejsu użytkownika systemu Windows: tworząc pole tekstowe przy użyciu Windows Forms, w rzeczywistości uzyskujemy zwyczajne systemowe pole tekstowe oraz towarzyszącą mu klasę .NET. W przypadku WPF sytuacja wygląda zupełnie inaczej . By ominąć ograniczenia inter­ fejsu użytkownika systemu Windows, kontrolki WPF stworzono w całości do podstaw. Choć w znacznej mierze wyglądają i działają one jak zwyczajne kontrolki systemowe, to jednak z nich nie korzystają. (Technologia Silverlight może działać także w systemie Mac OS X, a zatem nie ma wątpliwości co do tego, że nie korzysta ona z kontrolek systemu Windows) . Jako że w technologii WPF tak znaczna część interfejsu użytkownika została odtworzona od podstaw, jej utworzenie zajęło trochę czasu - wprowadzono ją w platformie .NET 3.0, czyli niemal dekadę po pojawieniu się .NET 1 .0 . Tymczasem technologia Windows Forms była dostępna już od samego początku, co prawdopodobnie zawdzięczamy jej znacznie mniej­ szemu zakresowi - jej kluczowe możliwości zapewnia sam system operacyjny, zatem stwo­ rzenie jej wymagało znacznie mniejszych nakładów pracy niż stworzenie WPF. Choć to tło historyczne wyjaśnia, dlaczego aktualnie dysponujemy dwiema technologiami do tworzenia aplikacji biurowych w języku C#, to jednak nie daje odpowiedzi na pytanie, dlaczego mielibyśmy się obecnie decydować na korzystanie z Windows Forms. Technologia WPF została opracowana, by można było uniknąć niektórych ograniczeń interfejsu użytkownika Win32, więc ma znacznie większe możliwości niż Windows Forms, jednak także ta ostatnia ma pewne zalety. Przede wszystkim powstała ona na długo przed WPF i jest doskonale obsługiwana przez narzę­ dzia zarówno firmy Microsoft, jak i innych firm. W Visual Studio narzędzia do projektowania interfejsu przy użyciu Windows Forms są znacznie bardziej dojrzałe niż ich odpowiedniki korzystające z WPF, w których przypadku często może się okazać, że wiele zmian musimy wykonywać sami w kodzie C# lub XAML. A jeśli zechcemy wykorzystać już istniejące kon­ trolki, to najprawdopodobniej będziemy w stanie znaleźć kontrolki Windows Forms zapew­ niające znacznie większe możliwości niż te, które daje ich najlepszy odpowiednik wśród kontrolek WPF. (W jednej aplikacji można używać kontrolek z obu tych technologii, może się

797

zatem zdarzyć, że tworząc aplikacje WPF, będziemy korzystali z kontrolek Windows Forms, choć stosowanie dwóch różnych platform obsługi interfejsu użytkownika w jednej aplikacji może ją znacznie skomplikować) . Kolejną zaletą technologii Windows Forms jest to, że zdaje się ona być nieco bardziej oszczędna. Zazwyczaj aplikacje WPF zajmują w pamięci nieco więcej miejsca niż odpowiadające im apli­ kacje napisane w Windows Forms. Oczywiście stworzenie dokładnego odpowiednika aplikacji z użyciem tej technologii czasami nie jest możliwe, niemniej jednak jeśli nie korzystamy z żad­ nych specyficznych zalet technologii WPF, jej stosowanie może oznaczać nieuzasadnione nara­ żanie się na niepotrzebne koszty. W sytuacjach, gdy nasza aplikacja musi działać na starych komputerach o kiepskiej specyfikacji, może to być kluczowym czynnikiem decydującym o wyborze konkretnej technologii. Jeśli żadna z powyższych zalet nie ma dla nas większego znaczenia, to lepszym rozwiązaniem będzie zapewne wybór technologii WPF. Windows Forms nie ma tak potężnego, bazującego na koncepcji kompozycji modelu, jakim dysponuje WPF i jakiego przykładem jest model zawar­ tości i potężny system szablonów. Interfejsy użytkownika tworzone przy użyciu Windows Forms są mniej atrakcyjne graficznie i nie zapewniają możliwości tworzenia animacji. Nie można w nich stosować stylów, mają znacznie prostszy system wiązania danych i nie udostępniają niczego, co byłoby odpowiednikiem języka XAML. (Choć z technicznego punktu widzenia można utworzyć interfejs użytkownika, korzystając z XAML, to jednak Visual Studio nie zapew­ nia takiej możliwości i jest to rozwiązanie raczej kłopotliwe, gdyż technologia Windows Forms nie była tworzona z myślą o współpracy z tym językiem) . Co więcej, firma Microsoft wyraźnie dała do zrozumienia, że technologia ta nie będzie już dłużej rozwijana - owszem, będzie wspie­ rana jeszcze przez wiele lat, lecz nie będą już do niej dodawane żadne nowe możliwości . Jeśli Czytelnik dotarł do tego akapitu, można sądzić, że zainteresował się bardziej zaletami technologii Windows Forms. W niniejszym rozdziale przedstawimy więc sposób tworzenia prostej aplikacji Windows Forms, by pokazać, w jaki sposób jest ona wspierana przez Visual Studio oraz jakie są podstawowe cechy jej modelu programowego.

Tworzenie apl i kacj i Napiszemy prostą aplikację do prezentowania i edycji listy rzeczy do zrobienia. Aby utworzyć nową aplikację Windows Forms, należy wyświetlić okno dialogowe New Project (naciskając kombinację klawiszy Ctrl+Shift+N), po czym z listy Installed Templates widocznej z jego lewej strony wybrać opcję Visual C#/Windows. Następnie z listy szablonów pośrodku okna dialo­ gowego należy wybrać opcję Windows Forms Application. Naszemu projektowi nadamy nazwę ToDoL i s t . W rezultacie Visual Studio utworzy nowy projekt zawierający jeden formularz o nazwie Forml, przy czym będzie on dziedziczył po klasie bazowej Form . Nasz formularz jest zwyczaj­ nym oknem - nazwa klasy odzwierciedla fakt, że jednym z celów, z realizacją których tech­ nologia Windows Forms radzi sobie wyjątkowo dobrze, jest tworzenie aplikacji biznesowych w głównej mierze bazujących na wypełnianiu formularzy. Visual Studio wyświetli nowy formularz w widoku projektu, dzięki czemu będzie można umieszczać na nim nowe kontrolki, posługując się techniką „przeciągnij i upuść" . Jednak zanim zaczniemy tworzyć interfejs użytkownika na naszym formularzu, zdefiniujemy klasę, która w naszej aplikacji będzie reprezentowała poszczególne zadania do wykonania . W tym celu dodamy do projektu klasę o nazwie ToDo Entry, której kod przedstawiliśmy na listingu 22 . 1 .

798

I

Rozdział 22. Windows Forms

Listing 22 . 1 . Klasa reprezentująca zadania do wykonania publ i c cl ass ToDoEntry { publ i c s t r i ng T i t l e { get ; set ; } publ i c s t r i ng Descri pt i on { get ; set ; publ i c DateT i me DueDate { get ; set ; }

••

• .·

'-"�;

. ......___� ·

Jeśli Czytelnik od razu podczas lektury wpisuje kody przykładów w Visual Studio, to po dodaniu powyższej klasy powinien zbudować projekt. Będziemy bowiem korzystać z pewnych narzędzi projektowych udostępnianych przez Visual Studio, które muszą dysponować informacjami o naszych klasach, a to wymaga wcześniejszego zbudowania projektu.

Teraz musimy się upewnić, że Windows Forms wie, iż będziemy używali tej klasy jako źródła danych. W tym celu utworzymy tak zwane źródło wiązania (ang. binding source) .

Dodawanie źródła wiązania Klasa B i n d i ngSource nadzoruje sposób, w jaki interfejs użytkownika Windows Forms używa konkretnego źródła danych. W przypadku gdy dysponujemy kolekcją elementów takich jak wpisy na naszej liście rzeczy do zrobienia, klasa Bi ndi ngSource śledzi, który z jej elementów jest w danej chwili wybrany, i jest w stanie koordynować operacje ich dodawania, usuwania oraz modyfikacji. Korzystanie z klasy Bi ndi ngSource może także ułatwić prace w projektancie formula­ rzy Visual Studio, gdyż dostarcza ona informacji dotyczących używanych danych, a to może pomóc w powiązaniu ich z kontrolkami. Aby dodać obiekt B i n d i ngSource do naszego projektu, należy przejść do widoku projektu for­ mularza, upewnić się, że jest widoczny panel Toolbox (a w razie potrzeby wyświetlić go, wybie­ rając odpowiednią opcję z menu View), i rozwinąć zawartość sekcji Data. Znajdziemy w niej między innymi kontrolkę B i n d i ngSource, którą należy przeciągnąć na formularz . ••

• .·

'-"�;

. .___� ,

Komponenty narzędziowe, które nie mają wizualnej reprezentacji widocznej na formularzu podczas działania programu, nie pojawiają się na nim także w widoku projektu. Wszystkie takie niewizualne kontrolki są umieszczane w panelu wyświetlanym w widoku projektu poniżej formularza.

Visual Studio nada naszej nowej kontrolce niezbyt opisową nazwę b i n d i ngSourcel . Możemy ją zmienić, wybierając kontrolkę i wyświetlając panel Properties - należy w nim odszukać wła­ ściwość (Name) i przypisać jej nazwę entri esSource. Kolejną czynnością jest określenie, czego chcemy używać jako źródła danych. W panelu Properties można znaleźć właściwość DataSource, a kiedy rozwiniemy dostępną w niej listę, pojawi się okienko dialogowe prezentujące wszystkie źródła danych dostępne w projekcie (przedstawione na rysunku 22.1) . Aktualnie nie będzie dostępnych żadnych źródeł, dlatego też musimy kliknąć łącze Add Project Data Source umiesz­ czone u dołu okna . Kliknięcie tego łącza spowoduje wyświetlenie okna dialogowego kreatora Data Source Confi­ guration Wizard. Obsługuje ono kilka rodzajów źródeł danych. Jego postać może się nieco różnić w zależności od tego, z jakiej wersji Visual Studio korzystamy, niemniej jednak powinny w nim być dostępne takie opcje jak: Database, Service, Object oraz SharePoint. W naszym projekcie

Tworzenie aplikacji

I

799

entriesSouce System.Windows. Fo. rm s. B i n d' i n g S(

P ro perti es

�= u t:>

'"

-}

cJ

T

(App licatio nSetti ng s) (N a me) entriesSmuce AllowN ew True DataMember DataSource

(no rn�)

N one

'u Ad d Pwject Data



ice.„____� ·

Sel ect a data smm:e u n • er 'Oth er Data So·u rc es' to connect to data.

Rysunek 22 . 1 . Konfiguracja źródła wiązania będziemy korzystali z obiektów - w końcu właśnie w tym celu dodaliśmy do niego klasę ToDo Entry - dlatego powinniśmy zaznaczyć opcję Object i kliknąć przycisk Next. Następne okno dialogowe kreatora, przedstawione na rysunku 22.2, pozwala określić typ obiektów, z któ­ rymi chcemy związać kontrolki . W naszym przypadku będzie to klasa ToDo Entry . Po kliknięciu przycisku Finish kontrolka B i ndi ngSource będzie już wiedzieć, na jakich obiektach ma operować. Ostatnią czynnością będzie przekazanie jej konkretnych obiektów. Gdybyśmy dysponowali połączeniem z bazą danych, Visual Studio mogłoby przygotować wszystko, co niezbędne, by automatycznie pobrać dane z bazy, jednak ponieważ będziemy używali obiek­ tów, musimy je przygotować i dostarczyć samodzielnie. Należy to zrobić w kodzie ukrytym aplikacji . Domyślnie Visual Studio pokazuje formularz w widoku projektu, jeśli jednak klik­ niemy go prawym przyciskiem myszy, to w wyświetlonym menu kontekstowym ujrzymy opcję View Code pozwalającą wyświetlić ukryty kod obsługujący dany formularz (można to także zrobić, naciskając klawisz F7) . Podobnie jak w przypadku technologii WPF, Silverlight oraz ASP.NET, także w aplikacjach Windows Forms kod ukryty jest klasą częściową, w której jest umieszczany kod służący do obsługi zdarzeń oraz interfejsu użytkownika. Pozostałe części tej klasy są generowane automatycznie przez Visual Studio na podstawie czynności wykony­ wanych w projektancie formularzy. Zmodyfikujemy ten kod ukryty, dodając do niego frag­ menty zaznaczone na listingu 22.2 pogrubieniem.

Listing 22 .2 . Dostarczanie obiektów na potrzeby źródła danych publ i c part i al cl a s s Forml : Form { pri vate Bi ndi ngli st entri es = new Bi ndi ngli st () ;

800

I

Rozdział 22. Windows Forms

Data So u rce Confi gu ratio n

i



Select the Data Objects

E:tpa n d th e referenced assemb·lies and namespaces to select your objects. If an object is mi ssing from a referenced assembly, can cel th e wi!Zard a n d rehu i ld th e p roject that contain s the object.

What objects do yo11 want to biool to? "' lil f.:6:1 ToDolistj "' li] 0 ToDo List IO
Ian Griffiths, Matthew Adams, Jesse Liberty - C#. Programowanie. Wydanie VI (2012) [SHARP]

Related documents

204 Pages • 41,894 Words • PDF • 2.3 MB

6 Pages • 1,532 Words • PDF • 55.5 KB

350 Pages • 137,548 Words • PDF • 3.1 MB

526 Pages • 184,357 Words • PDF • 11.1 MB

1 Pages • 51 Words • PDF • 1.5 MB

120 Pages • 35,342 Words • PDF • 11 MB

947 Pages • 401,407 Words • PDF • 14.1 MB

1 Pages • 294 Words • PDF • 128.5 KB

27 Pages • PDF • 46.2 MB

1,164 Pages • 459,264 Words • PDF • 15.1 MB