Programowanie w języku C# » Wstęp do programowania w języku C#

Rozdział 4. Przegląd .NET Framework.

W rozdziale 2. omówiłem podstawowe aspekty platformy .NET. Powinieneś już znać podstawowe pojęcia związane z tą technologią, a także umieć pisać proste aplikacje w C#.

W tym rozdziale skupię się przede wszystkim na środowisku .NET Framework. Ponieważ środowisko .NET Framework obejmuje wiele pojęć oraz technologii, należy je opisać na tyle szczegółowo, abyś wiedział, jak działają aplikacje w środowisku .NET.

Środowisko .NET Framework obejmuje swym zakresem wszystkie warstwy tworzenia oprogramowania: od systemu operacyjnego po bibliotekę klas (jak np. Windows Forms). Co to oznacza? Otóż środowisko .NET Framework zainstalowane na komputerze czuwa nad aplikacją, odpowiada za odpowiednie zarządzanie pamięcią oraz obsługę błędów. Oczywiście Ty jako programista nie musisz się tym przejmować. Wszystko odbywa się automatycznie.

Spis treści

     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, przestrzenie nazw
          5.1 Wieloznaczność
          5.2 Główne przestrzenie nazw
     6 Podsumowanie


Ś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ł 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 którego kontrolą 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óry program 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 jednak także stwarzać problemy z innymi wersjami systemu operacyjnego (w tym wypadku Windows). Duża w tym rola API systemu, z którego korzystają aplikacje. Wiadomo, że np. w systemie Linux nie istnieją biblioteki DLL, odpowiednik WinAPI systemu Windows 1. Z tego względu takie programy nie mogą zostać uruchomione na  systemach innych niż Windows.

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 (ang. 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. Jeżeli więc kompilujesz swoje aplikacje napisane w C#, w rzeczywistości są one kompilowane do kodu pośredniego IL, a nie do kodu maszynowego.

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 jest niezauważalne dla użytkownika.

Możliwe jest również jednorazowe skompilowanie danego programu od razu na kod maszynowy. Dzięki temu przy każdym uruchamianiu 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 (ang. 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ślić — kod zarządzany jest wykonywany pod kontrolą CLR.

Zmiany 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 2 , 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ć — jest to ciekawostka dla osób, które wcześniej programowały w Win32.

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 11. 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ć Ci się 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. Kiedy instalujemy 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 — głównie 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 do ich załadowania. Następnie klasy są ładowane do pamięci.

Weryfikacja


Po załadowaniu klas do pamięci zawartość programu, czyli metadane oraz kod IL, zostaje poddana 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. Kod po weryfikacji jest sprzekazywany 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. Podstawowe typy, takie jak int, string, omówiłem w poprzednim rozdziale.

Wspólny system typów (ang. Common Type System) jest bogatym zbiorem typów opracowanych przez firmę Microsoft na potrzeby .NET. Programując w C#, korzystamy z tych typów cały czas! W rzeczywistości bowiem np. typ int odpowiada typowi System.Int32 z platformy .NET! Spójrz na poniższy przykład:

int Foo;
System.Int32 Bar;
 
Foo = 23;
Bar = Foo;


Jak widzisz, zadeklarowałem w programie dwie zmienne typu całkowitego, które są sobie równoważne.

W rzeczywistości Int32 czy String to klasy znajdujące się w przestrzeni nazw System.

Mimo że typy języka C# wskazują na odpowiedniki środowiska .NET Framework, łatwiej jest stosować skrócone wersje, takie jak int czy string. Takie kroki zostały poczynione z myślą o programistach języka C/C++, w którym obecne są takie właśnie typy.

Tabela 4.1 zawiera listę wbudowanych typów języka C# wraz z ich odpowiednikami CTS.

Tabela 4.1. Typy CTS
Typ C# Typ .NET Framework
boolSystem.Boolean
byteSystem.Byte
sbyteSystem.SByte
charSystem.Char
decimalSystem.Decimal
doubleSystem.Double
floatSystem.Single
intSystem.Int32
uintSystem.UInt32
longSystem.Int64
ulongSystem.UInt64
objectSystem.Object
shortSystem.Int16
ushortSystem.UInt16
stringSystem.String


System CTS jest składnikiem CLR, odpowiada za weryfikowanie i zarządzanie tymi typami. Wszystkie typy, również te przedstawione w tabeli 4.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 na 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 11.

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ń.

Dodatkowe informacje na temat specyfikacji CLS można także znaleźć na stronach firmy Microsoft:
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 MFC (w środowisku Visual Studio) czy VCL (w środowisku Delphi), które jeszcze bardziej ułatwiały programowanie.

Biblioteka klas .NET Framework (ang. .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).

W języku C# korzystamy jedynie z klas .NET Framework, nie używamy w ogóle funkcji WinAPI. Ponieważ są setki klas, polecam korzystanie z internetowej dokumentacji środowiska .NET Framework (dostępnej na stronie firmy Microsoft), która zawiera opis wszystkich klas, metod itp.

Powiedzieliśmy, że do programowania na platformę .NET można używać wielu różnych języków programowania. Pisząc swój program, np. w Delphi, mogę używać tych samych klas co w języku C#:

Console.WriteLine("Hello World"); // C#

Console.WriteLine('Hello World'); // Delphi


Moduły, przestrzenie nazw


Nieco wcześniej, w poprzednim rozdziale wspomniałem o programowaniu proceduralnym, które jest znacznym ułatwieniem pracy podczas pisania programów.

Podział na procedury i funkcje jednak przestał wystarczać, gdy programy stawały się coraz dłuższe. Wówczas ktoś wpadł na pomysł, aby części kodu źródłowego podzielić na mniejsze pliki. Taką możliwość wprowadzono także w jednej z wcześniejszych wersji Turbo Pascala.

Ideę dzielenia kodu na mniejsze pliki nazwano programowaniem strukturalnym.

Moduł (ang. unit) jest plikiem tekstowym zawierającym (posiadającym rozszerzenie *.cs) polecenia przetwarzane przez kompilator. Inaczej mówiąc, jest to fragment kodu źródłowego.

Jakie są zalety programowania strukturalnego (modularnego)? Otóż programista ma możliwość umieszczenia części kodu (np. kilku klas) w osobnych modułach. Wyobraźmy sobie, że tworzymy dość spory program i cały kod źródłowy jest zawarty w jednym pliku i podzielony na poszczególne klasy. W takim pliku trudno się będzie później odnaleźć — przede wszystkim ze względu na jego długość. Koncepcja programowania strukturalnego polega na podzieleniu takiego programu na kilka plików i włączaniu ich po kolei do głównego pliku źródłowego.

Dzięki temu możemy w jednym pliku, np. interfaces.cs, zawrzeć jedynie klasy związane z tworzeniem interfejsu użytkownika, a z kolei w pliku db.cs umieścić procedury związane z obsługą bazy danych.

Włączenie nowego pliku do projektu C# jest dosyć proste, jeżeli korzystamy z edytora takiego jak Visual C# Express Edition. Z menu Project wybierz Add New Item. Zaznacz ikonę Code File i naciśnij przycisk OK. Utworzona zostanie nowa zakładka z edytorem kodu. Zwróć uwagę na okno Solution Explorer, w którym pojawiła się nowa pozycja — CodeFile1.cs (rysunek 4.1). W trakcie kompilacji plik ten będzie włączany do projektu.
 

Rysunek 4.1. Okno Solution Explorer

Aby przetestować możliwość korzystania z kodu znajdującego się w innym pliku, w nowym utwórz przykładową klasę zawierającą prostą metodę:

class Foo
{
    public static void Bar()
    {
        System.Console.WriteLine("Jestem kodem znajdującym się w innym pliku!");
    }
}


W module głównym wystarczy wywołać odpowiednią metodę z klasy Foo:

using System;
 
class Program
{
    static void Main(string[] args)
    {
        Foo.Bar();
        Console.Read();
    }
}



Ani środowisko Visual C# Express Edition, ani kompilator nie mają informacji, który z plików źródłowych projektu jest plikiem „głównym”. Po prostu któryś z modułów musi zawierać „miejsce startowe” w postaci metody Main(). Jeżeli kompilator odnajdzie więcej niż jedną metodę Main(), nawet jeżeli będą one w różnych klasach i przestrzeniach nazw, zasygnalizuje błąd.

Wieloznaczność


Podział projektu na poszczególne pliki źródłowe nie ma w C# takiego znaczenia jak podział na przestrzenie nazw (ang. namespaces).

Załóżmy, że mam firmę. Piszę swoje aplikacje, a następnie udostępniam je innym. Pamiętasz, jak mówiłem, że skompilowane podzespoły mogą być w .NET wykorzystywane przez inne aplikacje? Napisałem świetną aplikację służącą do kompresji dźwięku. Mogę w niej zadeklarować klasę Console czy nawet DateTime, które potencjalnie mogą kolidować z klasami o tych samych nazwach z przestrzeni System. Jednak tak nie jest, gdyż swoje klasy umieszczam w przestrzeni nazw Boduch:

namespace Boduch
{
    class Foo
    {
    }
 
    class Bar
    {
    }
 
    class Console
    {
    }
}


Teraz zewnętrzne aplikacje odwołujące się do moich klas będą musiały poprzedzać nazwę klasy nazwą przestrzeni nazw:

Boduch.Console.Nazwa_Metody();


Chodzi o to, że dana klasa może mieć zupełnie inne znaczenie w zależności od tego, w jakiej przestrzeni nazw została zadeklarowana. W jednej aplikacji mogę mieć wiele przestrzeni nazw. Nie powinna więc wydarzyć się sytuacja, w której jakieś nazwy się dublują w obrębie jednej przestrzeni.

Każda przestrzeń nazw może zawierać kolejne, zagnieżdżone przestrzenie.

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ę, czyli wspominaną przestrzeń 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 4.2 przedstawia główne przestrzenie nazw używane w .NET.

Tabela 4.2. Główne przestrzenie nazw .NET
Przestrzeń nazwOpis
SystemZawiera klasy, które są używane w każdym programie — m.in. Int64, String, Char.
System.IOUdostępnia klasy do manipulacji strumieniami wejścia i wyjścia, a także służące do tworzenia oraz odczytu plików.
System.ThreadingPrzestrzeń udostępnia szereg klas służących do bardziej zaawansowanego procesu polegającego na projektowaniu aplikacji wielowątkowych.
System.ReflectionDostarcza klasy do inspekcji podzespołów, metod itp.
System.SecurityPrzestrzeń dostarcza klasy związane z bezpieczeństwem oraz kryptografią.
System.NetTa przestrzeń udostępnia klasy umożliwiające programowanie sieciowe oraz dostęp do wielu usług — m.in. DNS czy HTTP.
System.DataUdostępnia klasy związane z dostępem do baz danych.
System.WebOgólnie pojęte klasy oraz inne przestrzenie związane z dostępem do sieci, usługami sieciowymi oraz protokołem HTTP.
System.Windows.FormsPrzestrzeń związana z biblioteką Windows Forms oraz projektowaniem wizualnym.


Więcej informacji na temat przestrzeni nazw oraz zawartych w niej klas można znaleźć na stronie http://msdn.microsoft.com.

Włączenie danej przestrzeni nazw następuje przy pomocy słowa kluczowego using, ale powinieneś już o tym wiedzieć z lektury poprzedniego rozdziału.

Podsumowanie


W tym rozdziale zasypałem Cię sporą dawką teoretycznych informacji odnośnie środowiska .NET Framework. Wydaje mi się, że programista piszący swoje aplikacje w języku C#, a więc ściśle związanym z platformą .NET, powinien znać zasady działania na niej tych programów. Podstawowym składnikiem platformy i idei .NET jest właśnie środowisko .NET Framework, które to definiuje wiele pojęć związanych z procesem wytwarzania oprogramowania. Ja zaprezentowałem jedynie podstawowe związane z tym pojęcia. Jeżeli jesteś zainteresowany szczegółową dokumentacją technologii CLR czy CLS, odsyłam Cię do stron internetowych firmy Microsoft.


[1] Od wielu lat nieustannie trwają prace nad odpowiednikiem WinAPI na system Linux. Projekt Wine, bo o nim tutaj mowa, jest już na tak wysokim stadium zaawansowania, że umożliwia uruchamianie niektórych aplikacji systemu Windows na Linuksie.
[2] Niestety nie znalazłem dobrego polskiego odpowiednika tego słowa, które w pełni oddawałoby istotę rzeczy.

« Rozdział 3. Podstawy języka C#
Rozdział 5. Programowanie obiektowe »




© Helion 2006. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.