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ść.