Rozdział 5. Przegląd .NET Framework

Adam Boduch

W rozdziale 2. omówiłem podstawowe aspekty platformy .NET. Czytelnik powinien już znać podstawowe pojęcia związane z tą technologią, a także umieć już pisać proste aplikacje w Delphi. Jak dotąd jednak przykłady prezentowane przeze mnie w rozdziale 3. były oparte na programowaniu dla Win32.

W tym rozdziale zajmiemy się tylko i wyłącznie programowaniem dla platformy .NET oraz dokładniej omówimy technologie związane z tą platformą.

Środowisko .NET Framework obejmuje swym zakresem wszystkie warstwy tworzenia oprogramowania: od systemu operacyjnego po bibliotekę klas (jak np. Windows Forms).

     1 Środowisko CLR
          1.1 Kod pośredni IL
          1.2 Kod zarządzany i niezarządzany
          1.3 Moduł zarządzany
          1.4 Podzespoły
          1.5 Działanie CLR
               1.5.1 Class Loader
               1.5.2 Weryfikacja
               1.5.3 Kompilator JIT
     2 System CTS
     3 Specyfikacja CLS
     4 Biblioteka klas
     5 Moduły i przestrzenie nazw
          5.6 Wieloznaczność
          5.7 Główne przestrzenie nazw
          5.8 Tworzenie przestrzeni nazw
     6 Test
     7 FAQ
     8 Podsumowanie

W tym rozdziale:
*przedstawię szczegóły działania środowiska CLR,
*omówię nowe pojęcia: kod zarządzany i niezarządzany,
*opiszę, czym jest CLS i CTS.

Środowisko CLR

Wspólne środowisko uruchomieniowe (CLR) stanowi podstawowy element platformy .NET Framework. W momencie uruchomienia aplikacji .NET, CLR odpowiada za jej sprawne wykonanie (załadowanie do pamięci), przydział pamięci, obsługę błędów itp. W standardowym modelu programowania — Win32, za tego typu czynności odpowiadał zwyczajnie system operacyjny (Windows). Po zainstalowaniu na komputerze środowiska .NET Framework, odpowiednie biblioteki systemu pozwalają na rozpoznanie, czy dany program jest aplikacją Win32, czy też .NET. Jeżeli jest to aplikacja .NET, uruchomione zostaje środowisko CLR, pod kontrolą którego działa program. To, co dzieje się „w tle” nas nie interesuje, nie trzeba się tym przejmować.

W jaki jednak sposób system operacyjny rozpoznaje, która aplikacja jest aplikacją .NET? Dzieje się tak dlatego, że aplikacja wykonywalna .NET (plik .exe) jest inaczej zbudowana niż standardowe programy Win32. Tę kwestię postaram się wyjaśnić w paru kolejnych podrozdziałach.

Kod pośredni IL

Kompilatory działające pod kontrolą systemu Windows kompilują kody źródłowe do postaci 32-bitowego kodu maszynowego. W efekcie otrzymujemy aplikacje .exe czy biblioteki .dll. Taki sposób uniemożliwia przeniesienie aplikacji na urządzenia czy systemy, które działają pod kontrolą innych procesorów. Może także stwarzać problemy z innymi wersjami systemu operacyjnego (w tym wypadku Windows). Wszystko dlatego, że aplikacje wykonywalne komunikują się z API, które może różnić się w poszczególnych wersjach systemu.

Rozwiązaniem tego problemu jest kompilacja programu do kodu pośredniego, nazywanego Common Language Infrastructure (z ang. architektura wspólnego języka, CLI).

CLI na platformie .NET jest często nazywany MSIL (Microsoft Intermediate Language) lub po prostu — IL.

Kod pośredni IL przypomina kod języka Asembler:

.method family hidebysig virtual instance void 
        Dispose(bool Disposing) cil managed
{
  // Code size       30 (0x1e)
  .maxstack  2
  IL_0000:  ldarg.1
  IL_0001:  brfalse.s  IL_0016
  IL_0003:  ldarg.0
  IL_0004:  ldfld      class [System]System.ComponentModel.Container WinForm.TWinForm1::Components
  IL_0009:  brfalse.s  IL_0016
  IL_000b:  ldarg.0
  IL_000c:  ldfld      class [System]System.ComponentModel.Container WinForm.TWinForm1::Components
  IL_0011:  callvirt   instance void [System]System.ComponentModel.Container::Dispose()
  IL_0016:  ldarg.0
  IL_0017:  ldarg.1
  IL_0018:  call       instance void [System.Windows.Forms]System.Windows.Forms.Form::Dispose(bool)
  IL_001d:  ret
} // end of method TWinForm1::Dispose

W takiej postaci kod IL jest kompilowany do kodu maszynowego, który może już zostać uruchomiony. Tak właśnie działa język Java, z którego pomysł zaczerpnął Microsoft projektując platformę .NET.

Wszystko to jest możliwe dzięki tzw. maszynom wirtualnym, czyli aplikacjom przystosowanym do konkretnej wersji systemu/procesora. Środowisko CLR odpowiada za kompilację kodu pośredniego na maszynowy, w trakcie uruchamiania aplikacji. Dzieje się to jednak na tyle szybko, że dla użytkownika jest to niezauważalne.

Możliwe jest również jednorazowe skompilowanie danego programu od razu na kod maszynowy, dzięki temu przy każdym uruchamianiem programu są oszczędzane zasoby systemowe, potrzebne do uruchomienia kompilatora JIT (ang. Just-In-Time).

Kod zarządzany i niezarządzany

Platforma .NET definiuje dwa nowe pojęcia: kod zarządzany (managed code) oraz niezarządzany (ang. unmanaged code), które są istotne z punktu widzenia CLR. Kod niezarządzany jest zwykłym kodem, wykonywanym poza środowiskiem .NET, zatem określenie to oznacza stare aplikacje kompilowane dla środowiska Win32. Natomiast — jak nietrudno się domyśleć — kod zarządzany jest kodem wykonywanym pod kontrolą CLR.

Zmiany w Delphi 2005 oraz w .NET w porównaniu z Win32 są na tyle duże, że problemem staje się współdziałanie obu rodzajów aplikacji (aplikacji .NET oraz Win32), jak również korzystanie z zasobów starszych aplikacji Win32 — np. bibliotek DLL. Dlatego też w .NET w tym celu wykorzystuje się mechanizm zwany marshalingiem [#]_, który jest związany z określeniem sposobu, w jaki dane mają być przekazywane z kodu niezarządzanego do zarządzanego. Tym jednak nie należy się teraz przejmować — podejmę ten temat w rozdziale 11.

Moduł zarządzany

Każdy moduł zarządzany jest przenośnym plikiem systemu Windows. Moduły zarządzane są generowane przez kompilatory zgodne z platformą .NET, a w ich skład wchodzą następujące elementy:

*Nagłówek PE (ang. PE Header) — standardowy nagłówek pliku wykonywalnego systemu Windows,
*Nagłówek CLR (ang. CLR Header) — dodatkowe informacje, charakterystyczne dla danego środowiska CLR,
*Metadane (ang. metadata) — informacje o typach modułów używanych w programie oraz ich wzajemnych powiązaniach,
*Kod zarządzany (ang. managed code) — wspólny kod generowany przez kompilatory .NET (kod IL).
Informacje o metadanych znajdują się w 8. rozdziale tej książki.

Informacje o metadanych znajdują się w 8. rozdziale tej książki.

Podzespoły

To jest bardzo ważne pojęcie, którym nieraz będę się posługiwał w tej książce. W najprostszym ujęciu podzespołem (ang. assembly) nazywamy każdą aplikację działającą pod kontrolą .NET.

Każdy moduł zarządzany wymaga podzespołu. Jeden podzespół może zawierać jeden lub więcej modułów zarządzanych. Podzespół może zawierać również inne pliki, takie jak dokumenty, grafikę itp.

Na tym etapie zagadnienie to może wydać się Czytelnikowi niezwykle skomplikowane. Podzespoły mogą zawierać jeden lub więcej modułów zarządzanych, w których z kolei znajdują się kod zarządzany oraz metadane.

Działanie CLR

Wspomniałem wcześniej, że CLR zajmuje się uruchamianiem aplikacji napisanej w .NET oraz ogólnie — zarządzaniem procesem jej działania. Prześledźmy proces uruchamiania aplikacji w .NET. Oto kilka etapów, które przechodzi aplikacja od momentu, gdy użytkownik zarządzi jej uruchomienie:

*ładowanie klasy (ang. Class Loader),
*weryfikacja,
*kompilacja.

Class Loader

Dotychczas jedynym formatem, rozpoznawanym przez systemy Windows jako aplikacja wykonywalna, był stary format PE. Stary format PE zawierał skompilowany kod maszynowy aplikacji. Instalując bibliotekę .NET w systemie, dodawane jest uaktualnienie mówiące o nowym formacie PE, dzięki czemu system jest w stanie rozpoznać również nowy format plików wykonywalnych.

Skoro teraz aplikacja .NET jest rozpoznawana przez system, ten w momencie jej uruchamiania oddaje sterowanie do CLR. CLR odczytuje zawartość pliku oraz listę klas używanych w programie. Lista klas jest odczytywana z przeróżnych miejsc — szczególnie jest to manifest oraz metadane, a także plik .config, który może być dołączany do programu. Po odczytaniu klas następuje obliczenie ilości pamięci potrzebnej, aby załadować klasy. Następnie klasy są ładowane do pamięci.

Weryfikacja

Po załadowaniu klas do pamięci zawartość programu, czyli metadane oraz kod IL, zostają poddane weryfikacji. Jest to ważny etap, gdyż w przypadku niepowodzenia kod IL nie zostanie przekazany kompilatorowi JIT.

Kompilator JIT

Kompilator JIT odgrywa znaczącą rolę w procesie uruchamiania aplikacji. Po weryfikacji kodu jest on przekazywany do kompilatora, który kompiluje go do kodu maszynowego, a następnie gotowy program zostaje załadowany do pamięci. Znajduje się on w tej pamięci do czasu zakończenia działania aplikacji.

System CTS

Wiadomo już, czym są typy języka programowania. Wspólny system typów (Common Type System) jest bogatym zbiorem typów opracowanych przez firmę Microsoft na potrzeby .NET. W rzeczywistości, podczas programowania w Delphi z użyciem VCL.NET lub WinForms i zastosowaniem typu Integer, korzystamy z typu System.Int32, który należy do specyfikacji CTS.

Otwórzmy teraz okno repozytorium i wybierzmy kategorię Delphi for .NET Projects, a następnie Console Application. Można zadeklarować dwie zmienne i spróbować skompilować program:

var
  I1 : Integer;
  I2 : System.Int32;

Kompilacja odbędzie się bezproblemowo. Typy Integer oraz System.Int32 są sobie równoważne. Typy takie jak Integer czy String są zachowane ze względu na kompatybilność projektów ze starszymi wersjami Delphi. Programista nie musi uczyć się nazw nowych typów — wykorzystuje nazwy dobrze mu znane i kompatybilne z .NET. Typy języka Delphi oraz ich odpowiedniki w CTS znajdują się w tabeli 5.1.

Tabela 5.1. Typy CTS

Typ Delphi Typ .NET Framework
String System.String
Boolean System.Boolean
Char System.Char
Double System.Double
Integer System.Int32
Smallint System.Int16
Word System.UInt16
Int64 System.Int64
Byte System.Byte

System CTS jest składnikiem CLR, odpowiada za weryfikowanie i zarządzanie tymi typami. Wszystkie typy, również te przedstawione w tabeli 5.1 wywodzą się z głównej klasy — System.Object.

Specyfikacja CLS

Do tej pory możliwości w komunikowaniu się pomiędzy aplikacjami były nieco ograniczone. Kod aplikacji w środowisku Win32 może być dzielony pomiędzy biblioteki DLL. Po umieszczeniu funkcji w bibliotece DLL, istnieje możliwość ich eksportu, co z kolei pozwala na ich wykorzystanie przez aplikacje EXE.

W środowisku .NET aplikacje mają pełną zdolność do komunikowania się. Jeden podzespół może wykorzystywać funkcje drugiego i na odwrót. Wszystko to dzięki kompilacji do kodu pośredniego IL, do którego są kompilowane wszystkie aplikacje, bez względu na to, czy są pisane w C#, czy w Delphi. Tak więc, jeżeli napisano program do szyfrowania danych, można (jeżeli tylko programista tego zechce) udostępnić jego funkcjonalność innym podzespołom. Praktyczne przykłady tego zagadnienia zaprezentuję w rozdziale 8.

Nie tylko język pośredni ma tu znaczenie, ale również specyfikacja CLS (ang. Common Language Specification, czyli wspólna specyfikacja języków). Jest to zestaw reguł określających nazewnictwo oraz inne kluczowe elementy języka programowania. Jeśli projektanci języka programowania, który docelowo ma działać dla .NET, chcą, aby był on kompatybilny z CLS oraz miał zdolność do komunikowania się z pozostałymi podzespołami, muszą dostosować swój produkt do określonych wymagań.

Na stronach firmy Microsoft można także znaleźć dodatkowe informacje na temat specyfikacji CLS: http://msdn.microsoft.com/net/ecma/.

Biblioteka klas

Czytelnik wciąż spotyka się ze słowami: klasa, obiekt, których zresztą często używam w tym rozdziale. Wspominałem wcześniej, że na platformie Win32 aplikacje korzystały z WinAPI, czyli z zestawu funkcji pomocnych przy programowaniu. Wkrótce po tym pojawiły się takie biblioteki jak VCL, które jeszcze bardziej ułatwiały programowanie (VCL korzysta z funkcji WinAPI).

Biblioteka klas .NET Framework (.NET Framework Class Library) stanowi zestaw setek klas, bibliotek, interfejsów, typów, które mają zastąpić WinAPI. W założeniu FCL ma połączyć funkcjonalność WinAPI oraz dodatkowych bibliotek, takich jak VCL czy MFC (ang. Microsoft Fundation Classes).

Dla przykładu — w rozdziale 3. korzystaliśmy z funkcji Writeln, Readln, które są funkcjami Delphi umożliwiającymi operacje na konsoli. Odpowiednikiem tych funkcji na platformie .NET jest klasa Console oraz metody WriteLine oraz ReadLine. Oto prosty przykład programu, który pobiera informacje o imieniu użytkownika. Program korzysta z funkcji konsolowych biblioteki FCL:

program Project3;

{$APPTYPE CONSOLE}

var
  S : System.String;

begin
  Console.WriteLine('Cześć! Podaj swoje imię!');
  S := Console.ReadLine;

  Console.WriteLine('Cześć ' + S + '! Ja mam na imię Adam!');
  Console.ReadLine;
end.

Moduły i przestrzenie nazw

Znamy już pojęcie modułu (mówiłem o tym w rozdziale 3.). Można powiedzieć, że moduł jest pojemnikiem na funkcje, procedury, typy, zmienne rekordy itp. W swoim programie można mieć wiele modułów, podzielonych na kategorie. Przykładowo: w jednym module można by umieścić wszelkie funkcje, które służą do zapisywania oraz odczytywania danych z plików tekstowych. Kolejny moduł może odpowiadać za analizę i odczytywanie danych z plików XML. Sztuką jest odpowiednie zaprojektowanie modułów i umiejętne ich włączenie do programu.

Klauzula uses umożliwia dołączenie modułu do programu i tym samym udostępnienie funkcji oraz typów, które zostały zadeklarowane w owym module. Naturalnie, im więcej modułów wykorzystujemy w programie, tym więcej kodu zawiera cała aplikacja, co wiąże się ze zwiększeniem czasu kompilacji oraz rozmiarów pliku wykonywalnego.

Platforma .NET wprowadza nowe pojęcie — przestrzenie nazw (ang. namespace). Przestrzenie nazw umożliwiają tworzenie hierarchicznych struktur modułów. Nie jest to trudne pojęcie, postaram się je wyjaśnić w paru poniższych akapitach.

Wieloznaczność

Załóżmy, że w program składa się z dwóch modułów — UnitA oraz UnitB. W obydwóch znajduje się procedura Hello. Moduł może wyglądać tak jak na listingu 5.1.

Listing 5.1. Przykładowy moduł UnitA

unit UnitA;

interface

  procedure Hello;

implementation


procedure Hello;
begin
  Console.WriteLine('Moduł UnitA!');
end;

end.

Taki sam kod znajduje się w module UnitB, natomiast budowa programu jest prosta:

program MyProject;

{$APPTYPE CONSOLE}


uses
  UnitA,
  UnitB;

begin
  Hello;
  Console.ReadLine;
end.

Pytanie jest proste: z którego modułu zostanie wywołana procedura Hello? Odpowiedź brzmi: z modułu UnitB, ponieważ został zadeklarowany w drugiej kolejności. Aby uniknąć wieloznaczności, można użyć operatora odwołania (.), aby jasno dać kompilatorowi do zrozumienia, z jakiego modułu ma być wywołana procedura Hello:

UnitA.Hello;

Wywołując jakąś procedurę/funkcję z danego modułu zawsze można owe wywołanie poprzedzać nazwą modułu (tak jak to zaprezentowano powyżej). Dla kompilatora nie ma znaczenia, czy projektant napisze Hello czy UnitA.Hello. Jeżeli jednak jawnie nie określi, z jakiego modułu ma być wywoływana procedura, Delphi spróbuje odszukać jej deklarację w dołączonych modułach.

Główne przestrzenie nazw

Biblioteka klas, dostępna w pakiecie .NET Framework, składa się z klas, typów i stałych. Elementy biblioteki są pogrupowane w hierarchiczną strukturę, która nosi nazwę przestrzeni nazw. Dzięki temu mogą istnieć dwie klasy o tej samej nazwie, jednak pod warunkiem, że zostały zadeklarowane w różnych przestrzeniach nazw. Nazwy przestrzeni nazw dostarczanych przez Microsoft zawsze zaczynają się od słowa System lub Microsoft. Przykładowo, przestrzeń nazw System.IO dostarcza mechanizmów umożliwiających pracę z plikami.

Dodatkowo, przestrzenie nazw wprowadzają pewną organizację, hierarchię i porządek. Przykładowo, dany producent może udostępnić bibliotekę, np. Firma.dll, która zawierać będzie przestrzenie nazw Firma.Biuro oraz Firma.Biuro.Komputer, udostępniające dodatkowe klasy. Tabela 5.1 przedstawia główne przestrzenie nazw używane w .NET.

Tabela 5.1. Główne przestrzenie nazw .NET

Przestrzeń nazw Opis
System Zawiera klasy, które są używane w każdym programie — m.in. Int64, String, Char.
System.IO Udostępnia klasy do manipulacji strumieniami wejścia i wyjścia, a także służące do tworzenia oraz odczytu plików.
System.Threading Przestrzeń udostępnia szereg klas służących do bardziej zaawansowanego procesu, polegającego na projektowaniu aplikacji wielowątkowych.
System.Reflection Dostarcza klasy do inspekcji podzespołów, metod itp.
System.Security Przestrzeń dostarcza klasy związane z bezpieczeństwem oraz kryptografią.
System.Net Ta przestrzeń udostępnia klasy umożliwiające programowanie sieciowe oraz dostęp do wielu usług — m.in. DNS czy HTTP.
System.Data Udostępnia klasy związane z dostępem do baz danych.
System.Web Ogólnie pojęte klasy oraz inne przestrzenie związane z dostępem do sieci, usługami sieciowymi oraz protokołem HTTP.
System.Windows.Forms Przestrzeń związana z biblioteką Windows Forms oraz projektowaniem wizualnym.

Więcej informacji na temat przestrzeni nazw oraz klas w niej zawartych jest dostępna na stronie http://msdn.microsoft.com.

Pełna lista przestrzeni nazw .NET znajduje się w dodatku C niniejszej książki.

Przestrzenie nazw to nic innego jak specjalne nazewnictwo modułów. Spróbujemy napisać aplikację, która skorzysta z przestrzeni nazw System.IO i której zadaniem jest wyświetlenie zawartości pliku tekstowego na konsoli. Pierwszym krokiem jest dołączenie do listy uses przestrzeni System.IO:

uses
  System.IO;

Następnie, w celu wyświetlenia zawartości pliku, musimy skorzystać z klasy StreamReader. Cały kod programu znajduje się na listingu 5.2.

Listing 5.2. Przykład odczytu zawartości pliku

program P5_2;

{$APPTYPE CONSOLE}

uses
  System.IO;

var
  S : StreamReader;
  Path : String;

begin
  Console.WriteLine('Program wyświetli zawartość pliku na ekranie');
  Console.Write('Podaj ścieżkę pliku: ');
  Path := Console.ReadLine;  // pobranie ścieżki wpisanej przez użytkownika

  S := StreamReader.Create(Path);

  while (S.ReadLine <> nil) do
  begin
  { odczytujemy plik linia po linii }
    Console.WriteLine(S.ReadLine);
  end;

  Console.WriteLine('--');
  Console.WriteLine('Naciśnij Enter aby zakończyć');
  Console.ReadLine;  
end.

Program na samym początku zgłasza żądanie podania ścieżki do pliku, którego zawartość chcemy wyświetlić. Późniejsze instrukcje wiążą się z użyciem klasy StreamReader. Ponieważ nie omawiałem jeszcze kwestii dotyczących klas, nie należy przejmować się tym zapisem, szczegółowo będę o tym mówił w rozdziale 7.

Tworzenie przestrzeni nazw

Nie trzeba tworzyć programów z użyciem przestrzeni nazw, można je nazywać po prostu UnitA, UnitB itp. bez używania znaku kropki. Aby zachować pewną hierarchię, można nazywać swoje moduły, używając znaku kropki, np. MyProject.MyUnit oraz MyProject.MyUnit.UnitA.

Test

  1. Odpowiednikiem typu Integer w CTS jest:
    a. System.Int,
    b. System.Int64,
    c. System.Int32.
  2. Metadane zawierają informacje o:
    a. typach użytych w programie,
    b. modułach, klasach użytych w programie,
    c. obydwie odpowiedzi są prawidłowe.
  3. Kompilator JIT:
    a. kompiluje kod aplikacji do kodu IL,
    b. kompiluje kod aplikacji do kodu maszynowego,
    c. weryfikuje poprawność podzespołu.
  4. Kod pośredni jest zapisywany w:
    a. manifeście,
    b. module zarządzanym,
    c. nagłówku CLR.

FAQ

Gdzie mogę znaleźć opis klas .NET?

Szczegółowy opis wszystkich klas .NET Framework, łącznie z WinForms, można znaleźć na stronie producenta, firmy Microsoft (http://msdn.microsoft.com), lub na niezależnej, dość ciekawej stronie poświęconej .NET — http://www.dotnet247.com/.

W jakim katalogu znajdują się omawiane w tym rozdziale aplikacje dołączone do .NET?

W moim przypadku są do katalogi C:\Program Files\Microsoft.NET\SDK\v1.1\Bin oraz C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322. W przypadku Czytelnika zapewne będzie to taki sam lub podobny katalog.

Podsumowanie

W tym rozdziale przedstawiłem bardziej szczegółowy opis głównego składnika platformy .NET — środowiska .NET Framework. Najważniejsze sprawy, o których trzeba pamiętać po lekturze tego rozdziału, to pojęcie podzespołu czy kodu pośredniego IL. Często w dalszej części tej książki będę wspominał pojęcia CLR, CLS czy CTS. Czytelnik powinien wiedzieć, co oznaczają te skróty i czym charakteryzują się te technologie.

.. [#] Niestety nie znalazłem dobrego, polskiego odpowiednika tego słowa, które w pełni oddawałoby istotę rzeczy.

[[Delphi/Vademecum|Spis treści]]

[[Delphi/Vademecum/Prawa autorskie|©]] Helion 2005. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

0 komentarzy