Kontakt  |  PRS 170  |  ALK 420  |  PZR 420  |  SIK 420  |  JPR 222  |  SOP 121


Języki programowania - ćwiczenia 11


Temat zajęć: Programowanie w środowisku .NET.

Literatura:
  • [1] J. Liberty, "Programowanie C#" (wyd. O'Reilly, druk w Polsce: Helion)
  • [2] A. Troelsen, "Pro C# 2005 and the .NET 2.0 Platform, 3rd edition" (wyd. APress)

Materiały dodatkowe:

 Platforma .NET - wprowadzenie (na podst. [1])

Platforma .NET to środowisko udostępniające nowy interfejs tworzenia aplikacji (API), ułatwiający korzystanie z serwisów i API systemów operacyjnych z rodziny Windows (choć istnieją już implementacje także na innych platformach, np. pakiety Mono czy GNU Portable .NET). Platformę .NET można zdefiniować w skrócie jako zestaw pakietów obejmujący: grupę języków programowania, w skład której wchodzą m. in. C#, C++ i Visual Basic, zestaw narzędzi programistycznych (z Visual Studio .NET na czele), bibliotekę klas umożliwiająca łatwe tworzenie aplikacji internetowych, okienkowych i bazodanowych, a także środowisko uruchomieniowe CLR (Common Language Runtime), jednakowe dla wszystkich języków programowania przystosowanych do współpracy z .NET.
Ponadto .NET zawiera wspólną specyfikację języka (CLS - Common Language Specification), udostępniającą zestaw reguł niezbędnych do integracji różnych języków programowania. CLS określa minimalne wymagania, jakie musi spełniać każdy język z rodziny .NET. Kompilatory zgodne z CLS tworzą obiekty, które mogą ze sobą współpracować mimo, że ich kod źródłowy został napisany w różnych językach programowania. Każdy język zgodny z CLS może używać dowolnych elementów całej biblioteki klas platformy .NET (Framework Class Library - FCL). Biblioteka ta zawiera obecnie ponad 4000 klas.
Programy na platformę .NET nie są kompilowane do plików wykonywalnych (mimo, że na platformie Windows znajdują się one w wykonywalnych plikach .exe). Są one kompilowane do podzespołów, które składają się z instrukcji standardowego języka pośredniego (Microsoft Intermediate Language - MSIL lub w skrócie :-) IL). Kompilacja kodu w języku C# (lub innym z rodziny .NET) do IL ma miejsce w momencie budowania (kompilacji) projektu. W momencie uruchomienia programu zachodzi ponowna kompilacja: języka IL do kodu maszynowego. Jest to kompilacja typu JIT (just-in-time), podobnie jak w maszynie wirtualnej Javy. Specyfikacja CLS wymusza podobieństwo IL niezależnie od języka programowania, stąd komponenty implementowane w różnych językach mogą współpracować ze sobą i nie jest konieczne tworzenie odrębnego JIT dla każdego języka (istnieje tylko jeden JIT - dla IL).

 Przykład 1

Na początek oczywiście program typu minimum (nie mam na myśli funkcji minimum). Wykorzystamy go do poznania procesu tworzenia, kompilacji i uruchamiania programów dla .NET.

Plik cw11p1.cs pobierz 
using System;

// nowa przestrzen nazw aby uniknac
// ew. konfliktow na wypadek, gdyby w .NET
// byla juz klasa Przyklad1 ;-)
namespace cw11p1 {

// definicja klasy - implementacja metod wewnatrz
// jak w Javie; nie ma plikow naglowkowych
class Przyklad1 {

/* od tej metody zaczyna sie wykonanie programu */
static void Main() {
// namespace System, klasa Console,
// metoda statyczna WriteLine
System.Console.WriteLine("Ja dzialam!");

/* Tak tez mozna bo na poczatku modulu
mamy using System */
Console.WriteLine("To prawdziwy cud!");

// niepotrzebne w przypadku metod bezwynikowych
return;
}
}
}
Przeanalizujmy kod. Dyrektywa using jest odpowiednikiem using namespace z C++ lub import z Javy - pozwala posługiwać się nazwami klas bez konieczności kwalifikowania ich przestrzeniami nazw, do których klasy te należą (w C# mamy pojęcie przestrzeni nazw, nie pakietu - w tym sensie przypomina on bardziej C++). Wiersz
using System;
pozwala zatem korzystać z klas przestrzeni System (my korzystamy z System.Console) bez konieczności dodawania przedrostka System.
Następnie definiujemy własną przestrzeń nazw cw11p1. Mimo, że jest mało prawdopodobne aby doszło do konfliktu nazw w przypadku klasy Przyklad1 (nie ma takiej klasy wśród klas z FCL, a nie korzystamy z żadnych innych bibliotek klas), umieszczanie własnego kodu w zdefiniowanej przez siebie przestrzeni nazw jest zalecane i możemy je uznać za dobrą praktykę.
Kolejnym elementem jest definicja klasy Przyklad1. Podobnie jak w Javie, również w C# każda instrukcja musi należeć do jakiejś metody jakiejś klasy. Zatem dowolny program w C# na pewno będzie zawierał co najmniej jedną klasę.
Cały kod wykonywalny naszego programu znajduje się w statycznej metodzie Main - jak łatwo się domyślić, od tej właśnie metody zaczyna się wykonanie programu po jego uruchomieniu.
Aby skompilować przykład korzystając z kompilatora C# działającego w wierszu poleceń, należy wydać komendę
 csc cw11p1.cs
Dostaniemy w wyniku cw11p1.exe, który jest programem wykonywalnym działającym w trybie konsoli.
Należy pamiętać, że zawiera on kod w IL (mimo, że jest w pliku .exe). Kompilator csc oprócz kodu naszej klasy w IL dodaje do pliku wynikowego standardowy nagłówek plików wykonywalnych Win32, który powoduje załadowanie maszyny wirtualnej .NET i uruchomienie kompilatora JIT. Stąd nie jest konieczne jawne (jak to było np. w przypadku Javy) uruchamianie maszyny wirtualnej - robi to za nas system operacyjny (zasada ta nie musi dotyczyć implementacji .NET innych niż firmy Microsoft; np. w przypadku Mono należy uruchamiać programy przez mono nazwa_programu).

Jeśli chcemy skorzystać ze środowiska Visual Studio, to po jego uruchomieniu (wersja .NET 2005) należy z menu File wybrać New Project, a następnie odszukać C# Console Application i podać nazwę projektu (cw11p1). Zostanie wygenerowany szkielet programu konsolowego w C#, z przestrzenią nazw cw11p1 i klasą Program. Wystarczy teraz uzupełnić kod metody Main, i wykonać Build Solution (F6) z menu Build. Aby uruchomić skompilowany program należy wybrać z menu Debug albo Start debugging, albo Start without debugging.

 Typy danych, operatory, instrukcje sterujące

Wszystkie typy platformy .NET dzielą się na dwie grupy:
  • skalarne, wywodzące się z System.ValueType,
  • obiektowe, wywodzące się z System.Object.
Do typów skalarnych należą: byte, char, bool, sbyte (byte ze znakiem), short, ushort, int, uint, float, double, decimal, long, ulong, struktury (struct) i typy wyliczeniowe (enum). Wszystkie inne typy to typy obiektowe (łącznie z tablicami i łańcuchami tekstowymi - podobnie jak w Javie).

Instrukcje sterujące (pętle, instrukcja warunkowa i wyboru) mają identyczną składnię jak w C++ i w Javie. Różnica w stosunku do C++ jest taka, że C# posiada odrębny typ logiczny, zatem nie można używać dowolnych wyrażeń arytmetycznych jako warunków. Ma to swoje dobre strony. W C/C++ taki kod jest (niestety) poprawny:
int X = 0;
if (X = 0) printf("Panie X, Pan jest zerem!\n");
Powyższy warunek nigdy nie jest spełniony, ponieważ zamiast porównania == wykonujemy przypisanie = (którego wartością jest wartość przypisywana, czyli 0, co jest równoznaczne z wartością logiczną fałsz, zatem warunek nigdy nie jest spełniony).

 Zadanie 1

Proszę zaimplementować krótki program w C# i sprawdzić, jak reaguje kompilator jeśli popełnimy błąd opisany wyżej.


Operatory arytmetyczne, binarne i logiczne w C# również mają identyczną składnię jak te znane nam z C++ i Javy, zatem nie będziemy dokonywać ich ponownego przeglądu.
Aby przejść do nieco bardziej złożonych przykładów warto jeszcze poznać sposób wypisywania na konsoli wartości zmiennych i wyrażeń. Służy do tego poznana wcześniej metoda WriteLine (i pokrewna metoda Write) klasy System.Console. Jeśli w danym miejscu łańcucha wyjściowego ma zostać wstawiona wartość wyrażenia, należy umieścić w nim sekwencję sterującą {numer} (gdzie numer to numer kolejny wyrażenia do wyświetlenia; numerowane są od 0) oraz podać dodatkowy parametr będący wyrażeniem do wyświetlenia (podobnie jak w przypadku funkcji printf znanej nam z języka C). Przykład:
System.Console.WriteLine("X={0}, t[1]={1}, t={2}", x, t[1], t);
Oczywiście nie wszystkie typy danych mają sensowną reprezentację tekstową. Jeśli w powyższym fragmencie t jest typu int[], to jako ostatni człon wyjścia otrzymamy System.Int32[] (nazwa typu tablicowego), a nie całą zawartość tablicy.

 Zadanie 2

Dość częstym błędem początkujących programistów jest korzystanie z niezainicjowanych zmiennych. Język C# dba również o ten aspekt. Proszę przetestować (tworząc projekt i przeklejając poniższy fragment do metody Main) jak zachowa się kompilator dla następującego fragmentu:
 int x, y;
y = x + 1;
System.Console.WriteLine("x={0}, y={0}", x, y);
i co stanie się po zastąpieniu pierwszego wiersza przez
 int x=1, y;

 Klasy i interfejsy, dziedziczenie, przynależność (is) , rzutowanie (as)

Definicję klas, dziedziczenie, polimorfizm i badanie przynależności do klasy zbadamy analizując przykład poniżej (proszę samodzielnie założyć projekt typu C# Console Application w Visual Studio i zastąpić wygenerowany szkielet kodem z przykładu).

 Przykład 2

Plik cw11p2.cs pobierz 
using System;

namespace cw11p2
{
// definicja interfejsu
// wszystkie metody z zalozenia sa publiczne
interface Pismienny
{
string PodpiszSie();
}

// Uczony implementuje interfejs Pismienny
class Uczony : Pismienny
{
string mojeNazwisko;

// konstruktor bezparametrowy
public Uczony()
{
mojeNazwisko = "zapomnialem";
}

// przeciazony konstruktor z parametrem
public Uczony(string nazwisko)
{
mojeNazwisko = nazwisko;
}

// wlasciwosc (property)
// jesli u jest klasy Uczony, to mozna wykonac
// s = u.Nazwisko;
// oraz
// u.Nazwisko = "Jasio";
public string Nazwisko
{
get // co w przypadku pobierania wartosci
{
return mojeNazwisko;
}
set // co w przypadku ustalania wartosci
{
mojeNazwisko = value; // value to przypisywana wartosc
System.Console.WriteLine("Uczony: ktos mi zmienil nazwisko!");
}
}

// troche sztuczna metoda, ale bedzie przykryta w klasie potomnej
public virtual String JakSieNazywam()
{
return Nazwisko;
}

// implementacja metody z interfejsu Pismienny
public string PodpiszSie()
{
return JakSieNazywam();
}
}

// kolejna implementacja interfejsu Pismienny
class Analfabeta : Pismienny
{
// konstruktor domyslny bedzie wygenerowany
// automatycznie

// implementacja metody z Pismienny
public string PodpiszSie()
{
// analfabeta nie umie pisac, wiec zglasza wyjatek (to umie)
throw new Exception("Nie umiem pisac!");
}
}

// Matematyk to klasa potomna Uczonego
// implementuje rowniez Pismienny
class Matematyk : Uczony, Pismienny
{
// base() to wywolanie konstruktora klasy bazowej - Uczony
public Matematyk(): base()
{
}

// tu rowniez poslugujemy sie konstruktorem klasy bazowej
public Matematyk(String nazwisko):
base(nazwisko)
{
}

// "przykrywamy" - override - metode wirtualna JakSieNazywam()
// z Uczony
public override string JakSieNazywam()
{
// base.JakSieNazywam() == Uczony.JakSieNazywam()
return "2*PI*R " + base.JakSieNazywam();
}
}


class cw11p2
{
// formalnie nie wiemy jakiej klasy obiekt bedzie przekazany
static string Podpis(Object o)
{
// jesli jest "typu" Pismienny, to oddajemy jego
// podpis, jesli nie - informacje ze nie umie
// sie podpisac
if (o is Pismienny) return (o as Pismienny).PodpiszSie();
else return "nie umie sie podpisac";
}


static void Main(string[] args)
{
Uczony u1 = new Uczony();
Uczony u2 = new Uczony("Fizyk");
Matematyk m = new Matematyk("Matematyk");
Analfabeta a = new Analfabeta();

u1.Nazwisko = "Uczony 1"; // korzystamy z property Nazwisko

Console.WriteLine("u1: {0}", Podpis(u1));
Console.WriteLine("u2: {0}", Podpis(u2));
Console.WriteLine("m: {0}", Podpis(m));

// uwaga, nadchodzi analfabeta
// lapiemy wyjatki
try
{
Console.WriteLine("a: {0}", Podpis(a));
}
catch (Exception e)
{
Console.WriteLine("Analfabeta powiedzial: {0}", e.Message);
}

// Teraz 5 minut dla polimorfizmu
Pismienny p1 = new Uczony("Polimorficzny uczony");
Pismienny p2 = new Matematyk("Polimorficzny matematyk");
Pismienny p3 = new Analfabeta(); // pismienny analfabeta - fiu fiu...

Console.WriteLine("Teraz wersje ilustrujace polimorfizm:");

// mozemy korzystac z PodpiszSie, bo interfejs Pismienny
// deklaruje ta metode
Console.WriteLine("Uczony: {0}", p1.PodpiszSie());
Console.WriteLine("Matematyk: {0}", p2.PodpiszSie());

// UWAGA! Tutaj mamy nieprzechwycony wyjatek.
// Ponizszy wiersz spowoduje otwarcie okna debugowania
// programu w systemie Windows.
Console.WriteLine("Analfabeta: {0}", p3.PodpiszSie());

// zadnych delete, mamy garbage collector
}
}
}
Ponownie spróbujemy przeanalizować kod przykładu.

Definiujemy interfejs Pismienny, który określa jedną metodę: PodpiszSie(). Każda klasa implementująca ten interfejs (w C# implementacja interfejsu technicznie zapisywana jest jako dziedziczenie z interfejsu) musi dostarczyć implementacji metody PodpiszSie.
Pierwszą klasą implementującą Pismienny jest klasa Uczony. Posiada ona prywatne pole mojeNazwisko, które inicjowane jest w konstruktorze. Klasa posiada dwa publiczne konstruktory - bezparametrowy i przeciążony z jednym parametrem typu string. Dostęp do składowych klas jest ograniczany podobnie jak w C++ (choć składnia bardziej przypomina tą z Javy). Kwalifikator private powoduje, że składowa jest dostępna tylko w danej klasie. Kwalifikator public udostępnia składową dla dowolnego kodu dowolnej klasy, a brak kwalifikatora jest równoważny dostępowi protected - tylko dana klasa i jej klasy potomne.
Klasa Uczony definiuje również właściwość (property) o nazwie Nazwisko, typu string. Właściwości są na zewnątrz klasy widziane jak pola, jednak przy próbie dostępu do nich wykonywany jest kod zdefiniowany dla właściwości. I tak przy próbie odczytu wykonywana jest sekcja get, a przy próbie zapisu sekcja set (predefiniowany identyfikator value oznacza w niej przypisywaną wartość). Klasa Uczony definiuje jeszcze wirtualną metodę JakSieNazywam, która zwraca wartość właściwości Nazwisko (czyli pole mojeNazwisko) oraz metodę PodpiszSie, co jest wymagane gdyż klasa Uczony implementuje Pismienny.
Następną klasą przykładu jest Analfabeta, który również implementuje interfejs Pismienny (co jest sprzecznością samą w sobie). Jednak jego implementacja metody PodpiszSie() zgłasza wyjątek (throw) klasy Exception z komunikatem "Nie umiem pisac!" - zachowanie godne analfabety.
Poniżej mamy klasę Matematyk, która dziedziczy z klasy Uczony, a oprócz tego jawnie implementuje Pismienny. Konstruktory klasy Matematyk korzystają z konstruktorów bazowej klasy uczony (słowo kluczowe base oznacza odwołanie się do klasy bazowej). Metoda wirtualna JakSieNazywam(), odziedziczona z Uczony, została tutaj przykryta (override).
Na koniec przyjrzyjmy się klasie cw11p2. Posiada ona statyczną metodę Podpis, przyjmującą jako parametr pewien obiekt. Metoda ta powinna zwracać wartość metody PodpiszSie() ale tylko w przypadku, gdy przekazany obiekt implementuje Pismienny. Aby zbadać, czy dany obiekt należy do jakiejś klasy lub jej klasy potomnej należy skorzystać z operatora is:
 obiekt is KlasaX
Jeśli wartością takiego wyrażenia jest prawda, to możemy bezpiecznie zrzutować obiekt na daną klasę i wywołać na jego rzecz metodę tej klasy:
 (obiekt as KlasaX).metodaKlasyX()
W metodzie Main tworzymy obiekty zdefiniowanych wcześniej klas i próbujemy uzyskać z każdego obiektu jego podpis (właściwy dla klasy, do której należy). Wyjątkowo traktujemy obiekt klasy Analfabeta - przechwytujemy ewentualne wyjątki wyrzucane przez jego metodę PodpiszSie(). Formalnie wyjątek taki pojawia się już w metodzie statycznej Podpis(), jednak nie jest tam chwytany, w związku z czym wędruje w górę stosu wywołań i zostaje złapany dopiero w metodzie Main.
W drugim fragmencie korzystamy z polimorfizmu. Deklarujemy formalne referencje do interfejsu Pismienny (zmienne p1,p2,p3), przypisując im oczywiście utworzone obiekty konkretnych klas, implementujących interfejs Pismienny.  Następnie korzystamy z metody PodpiszSie(), zadeklarowanej w interfejsie Pismienny, wywołując ją kolejno z utworzonych obiektów (uruchomiona zostanie zawsze implementacja z tej klasy, do której faktycznie należy adresat wywoływanej metody, niezależnie od tego, że formalnie referencja wskazuje na jego rodzica - w tym przypadku interfejs).

Po uruchomieniu przykładu na wyjściu powinniśmy dostać:
Uczony: ktos mi zmienil nazwisko!
u1: Uczony 1
u2: Fizyk
m: 2*PI*R Matematyk
Analfabeta powiedzial: Nie umiem pisac!
Teraz wersje ilustrujace polimorfizm:
Uczony: Polimorficzny uczony
Matematyk: 2*PI*R Polimorficzny matematyk

***** W tym miejscu pojawi się okno debuggera spowodowane wyjątkiem *****

Wyjątek nieobsłużony: System.Exception: Nie umiem pisac!
at cw11p2.Analfabeta.PodpiszSie()
at cw11p2.cw11p2.Main(String[] args)

 Zadanie 3

Proszę sprawdzić jak zachowa się program jeśli usuniemy klauzulę try...catch z metody Main.


 Zadanie 4

Zmodyfikować przykład 2 tak, aby łapanie wyjątków odbywało się już w metodzie Podpis() i to tylko w przypadku, gdy przekazany obiekt jest analfabetą. W takim przypadku metoda Podpis() powinna zwracać treść wyjątku jako swoją wartość.




Valid HTML 4.01!