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


Języki programowania - ćwiczenia 12


Temat zajęć: Programowanie w C# na platformie .NET (c.d.).

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:

 Właściwości (properties)

Właściwości (pojawiły się już w przykładzie 2 na poprzednich zajęciach) są elementem wspomagającym hermetyzację danych w obiektach. Dzięki nim w kodzie klienckim możliwy jest dostęp do stanu obiektów w sposób przypominający bezpośredni dostęp do pól składowych, jednak w rzeczywistości dostęp ten odbywa się poprzez specjalne metody klasy. W ten sposób autor klasy może np. udostępniać pewne elementy stanu obiektu tylko do odczytu lub umożliwiać zmianę stanu tylko wtedy, gdy jest to uzasadnione. Można również wprowadzać atrybuty wyliczalne, które nie odpowiadają bezpośrednio żadnemu z pól obiektu.

 Przykład 1

Na dzisiejszych zajęciach zaimplementujemy klasę, której obiekty reprezentować będą ułamki zwykłe. W kolejnych przykładach będziemy ją rozbudowywać, dodając nowe elementy funkcjonalne.
Zaczniemy od samej definicji klasy Ulamek, określimy jej stan (licznik i mianownik), zdefiniujemy konstruktory i właściwości oraz przeciążymy metodę ToString, która umożliwi nam wyświetlanie ułamków na konsoli.

Zaczniemy od utworzenia nowego projektu w Visual Studio .NET. Podobnie jak na poprzednich zajęciach, wybieramy jako typ projectu C# Console Application. Jako nazwę projektu podajemy cw14p1. Zostanie wygenerowany szkielet programu zawierający jeden moduł źródłowy o nazwie Program.cs. W Solution Explorer (po prawej) proszę kliknąć na Program.cs prawym przyciskiem myszy i z menu kontekstowego wybrać Rename. Następnie proszę zmienić nazwę Program.cs na Ulamek.cs. Visual Studio zapyta, czy zmienić w kodzie programu wszystkie odwołania do Program na odwołania do Ulamek. Proszę potwierdzić zamianę. Klasa Program w naszym module źródłowym zostanie zamieniona na Ulamek.

Przystąpimy teraz do definicji stanu obiektów klasy Ulamek. Stan ten definiować będą dwa pola:
private int mLicznik;
private int mMianownik;
Pola są prywatne, zatem nie dopuszczamy bezpośredniego dostępu do stanu obiektu dla kodu spoza klasy Ulamek. Dostęp do stanu odbywać się będzie za pomocą właściwości:
public int Licznik
{
get
{
return mLicznik;
}
set
{
mLicznik = value;
}
}

public int Mianownik
{
get
{
return mMianownik;
}
set
{
if (value <= 0) throw new Exception("Nieprawidlowy mianownik");
else mMianownik = value;
}
}
Definicja właściwości składa się z nagłówka określającego typ właściwości, zakres widoczności oraz nazwę. W tym sensie definicja właściwości przypomina definicję zwykłego pola. Następnie należy podać blok zawierający sekcję get oraz (opcjonalnie) sekcję set (to z kolei sprawia, że definicja właściwości przypomina również definicję metody). Pierwsza z nich wykonywana jest gdy kod kliencki czyta wartość właściwości (powinna ona zwrócić wyrażenie zgodne co do typu z typem właściwości), natomiast druga wykonywana jest przy próbie zmiany wartości właściwości. W sekcji set można korzystać z predefiniowanego identyfikatora value, który oznacza wartość przypisywaną w danym momencie właściwości.
Gdyby w kodzie programu została wykonana instrukcja:
Ulamek u = new Ulamek();
u.Licznik = 10;
to zostanie uruchomiona sekcja set właściwości Licznik z parametrem value równym 10.
W przypadku klasy Ulamek zakładamy, że mianownik musi być zawsze dodatni, stąd kontrola przypisywanej wartości i wyrzucenie wyjątku w przypadku, gdy przypisywany mianownik nie mieści się w dopuszczalnym zakresie.
Możliwe jest tworzenie właściwości tylko do odczytu. Pomijana jest wówczas sekcja set właściwości. W naszym przykładzie zdefiniujemy właściwość tylko do odczytu o nazwie Dziesietny, której wartością będzie reprezentacja naszego ułamka zwykłego w postaci liczby zmiennopozycyjnej.
public double Dziesietny
{
get
{
return ((double)mLicznik / (double)mMianownik);
}
}
Przystąpimy teraz do definicji konstruktorów. Klasa Ulamek posiadać będzie trzy konstruktory: bezparametrowy, z parametrami typu int (licznik i mianownik) oraz kopiujący (inicjujący obiekt klasy Ulamek na podstawie innego obiektu klasy Ulamek).
public Ulamek()
:
base()
{
Licznik = 0;
Mianownik = 1;
}

public Ulamek(int nowyLicznik, int nowyMianownik)
:
base()
{
Licznik = nowyLicznik;
Mianownik = nowyMianownik;
}

public Ulamek(Ulamek u)
:
base()
{
Licznik = u.Licznik;
Mianownik = u.Mianownik;
}
W każdym z konstruktorów wywołujemy jawnie (za pomocą base()) konstruktor bezparametrowy klasy bazowej (w naszym przypadku System.Object).  Jest to zbyteczne, ponieważ konstruktor ten i tak zostałby wywołany automatycznie (podobnie jak w Javie i C++) - wywołanie to znajduje się w przykładzie tylko po to, aby zilustrować składnię wywołań konstruktorów klasy bazowej (jest ona identyczna ze składnią listy inicjującej w C++). Jawne wywołanie konstruktora klasy bazowej jest konieczne w przypadku, gdy chcemy wywołać inny konstruktor niż bezparametrowy.  Proszę zauważyć, że inicjując stan obiektu posługujemy się wcześniej zdefiniowanymi właściwościami Licznik i Mianownik. Dzięki temu unikamy ponownej implementacji kontroli poprawności danych (np. badania czy mianownik jest dodatni).
Aby przetestować klasę Ulamek przeciążymy jeszcze metodę wirtualną ToString, która jest wywoływana automatycznie gdy zachodzi potrzeba uzyskania reprezentacji obiektu w postaci łańcucha tekstowego (np. wtedy, gdy obiekt "wyświetlany" jest na konsoli za pomocą System.Console.WriteLine).
public override string ToString()
{
return Licznik.ToString() + " / " + Mianownik.ToString();
}
Na koniec dodajemy fragment kodu manipulujący obiektami klasy Ulamek do metody Main.
static void Main(string[] args)
{
Ulamek u1, u2, u3;
u1 = new Ulamek();
u2 = new Ulamek(3, 4);
u3 = new Ulamek(u2);
System.Console.WriteLine("u1 = {0}, dziesietnie = {1}", u1, u1.Dziesietny);
System.Console.WriteLine("u2 = {0}, dziesietnie = {1}", u2, u2.Dziesietny);
System.Console.WriteLine("u3 = {0}, dziesietnie = {1}", u3, u3.Dziesietny);
}
Po skompilowaniu i uruchomieniu programu na wyjściu powinniśmy otrzymać:
u1 = 0 / 1, dziesietnie = 0
u2 = 3 / 4, dziesietnie = 0,75
u3 = 3 / 4, dziesietnie = 0,75
Proszę zauważyć, że w miejsce {0} w Console.Writeline wstawiany obiekt klasy Ulamek. Jest to możliwe, ponieważ przykryliśmy ToString w klasie Ulamek. Gdyby metoda ta nie została przykryta, wyświetlenie obiektu klasy Ulamek również nie zostałoby zgłoszone jako błąd, jednak na wyjściu dostaniemy
u1 = cw14p1.Ulamek, dziesietnie = 0
u2 = cw14p1.Ulamek, dziesietnie = 0,75
u3 = cw14p1.Ulamek, dziesietnie = 0,75
ponieważ zostanie uruchomiona implementacja ToString odziedziczona z klasy Object.

Kompletny moduł źródłowy przykładu 1: pobierz  (po zapisaniu należy przemianować plik na Ulamek.cs).

 Przeciążanie operatorów

Jednym z założeń projektowych języka C# jest to, że klasy zdefiniowane przez użytkownika powinny być równie funkcjonalne jak typy wbudowane. Definiując zatem klasę Ulamek chcielibyśmy korzystać ze standardowych operatorów arytmetycznych i relacyjnych, definiując wcześniej ich znaczenie w kontekście obiektów klasy Ulamek.
W języku C# implementuje się operatory tworząc statyczne metody, które przyjmują operandy i zwracają wartość reprezentującą wynik operacji. Kiedy tworzy się operator dla klasy, przeciąża się go podobnie jak dowolną inną metodę składową. Aby przeciążyć operator dodawania dla klasy Ulamek należy zaimplementować metodę
public static Ulamek operator+(Ulamek lewy, Ulamek prawy)
W przypadku operatorów relacyjnych istotne jest aby przeciążać pary operatorów (w C++ można było przeciążać dowolne podzbiory operatorów). Na przykład przeciążając operator == należy również przeciążyć operator !=, przeciążając < należy przeciążyć również > itd. Przeciążając operator porównania należy również przeciążyć metodę
public virtual bool Equals(object o)

 Przykład 2

Kontynuując implementację klasy Ulamek z przykładu 1, dodamy możliwość wykonywania operacji arytmetycznych i relacyjnych na ułamkach zwykłych.
Zanim jednak przystąpimy do przeciążania operatorów arytmetycznych, opracujemy procedurę normalizującą nasz ułamek, tzn. dzielącą jego licznik i mianownik przez NWD(licznik, mianownik).
private int NWD(int u, int v)
{
int k = 0;
if (u == 0) return v;
if (v == 0) return u;
u = Math.Abs(u);
v = Math.Abs(v);
while (u % 2 == 0 && v % 2 == 0)
{
u /= 2;
v /= 2;
k++;
}
do
{
if (u % 2 == 0)
u /= 2;
else if (v % 2 == 0)
v /= 2;
else if (u >= v)
u = (u - v) / 2;
else v = (v - u) / 2;
} while (u > 0);
return v << k;
}

public void Normalizuj()
{
int nwd = NWD(Licznik, Mianownik);
Licznik = Licznik / nwd;
Mianownik = Mianownik / nwd;
}
Zaczniemy od operatorów dodawania i odejmowania.
public static Ulamek operator +(Ulamek lewy, Ulamek prawy)
{
int licz, mian;
licz = lewy.Licznik * prawy.Mianownik + prawy.Licznik * lewy.Mianownik;
mian = lewy.Mianownik * prawy.Mianownik;
Ulamek wynik = new Ulamek(licz, mian);
wynik.Normalizuj();
return wynik;
}

public static Ulamek operator -(Ulamek lewy, Ulamek prawy)
{
int licz, mian;
licz = lewy.Licznik * prawy.Mianownik - prawy.Licznik * lewy.Mianownik;
mian = lewy.Mianownik * prawy.Mianownik;
Ulamek wynik = new Ulamek(licz, mian);
wynik.Normalizuj();
return wynik;
}
Możemy teraz sprawdzić działanie zdefiniowanych operatorów wstawiając następujący kod do metody Main:
static void Main(string[] args)
{
Ulamek u1, u2, u3;
u1 = new Ulamek();
u2 = new Ulamek(3, 4);
u3 = new Ulamek(2, 3);
System.Console.WriteLine("u1 = {0}, dziesietnie = {1}", u1, u1.Dziesietny);
System.Console.WriteLine("u2 = {0}, dziesietnie = {1}", u2, u2.Dziesietny);
System.Console.WriteLine("u3 = {0}, dziesietnie = {1}", u3, u3.Dziesietny);
System.Console.WriteLine("{0} + {1} = {2}", u2, u3, u2 + u3);
System.Console.WriteLine("{0} - {1} = {2}", u2, u3, u2 - u3);
System.Console.WriteLine("{0} - {1} = {2}", u2, u1, u2 - u1);
}
Po uruchomieniu programu powinniśmy zobaczyć na wyjściu
u1 = 0 / 1, dziesietnie = 0
u2 = 3 / 4, dziesietnie = 0,75
u3 = 2 / 3, dziesietnie = 0,666666666666667
3 / 4 + 2 / 3 = 17 / 12
3 / 4 - 2 / 3 = 1 / 12
3 / 4 - 0 / 1 = 3 / 4

Moduł źródłowy przykładu 2: pobierz (po zapisaniu należy przemianować plik na Ulamek.cs).

 Zadanie 1

Zaimplementować operatory mnożenia i dzielenia.


 Przykład 3

Oprócz dodawania, odejmowania, mnożenia i dzielenia obiektów klasy Ulamek, chcielibyśmy mieć możliwość wykonywania tych operacji również dla kombinacji ułamków i liczb całkowitych, np.
u2 = u1 + 3;
Jedną z możliwości jest ponowne przeciążenie operatorów arytmetycznych dla par operandów, z których jeden element jest obiektem klasy Ulamek, a drugi skalarem typu int. Inną możliwością (i z niej skorzystamy) jest zdefiniowanie operatora niejawnej konwersji z typu int do typu Ulamek. Jeśli taki operator będzie dostępny, kompilator przed wykonaniem operacji arytmetycznej skonwertuje liczbę całkowitą do obiektu klasy Ulamek i uruchomi jeden z już zaimplementowanych operatorów (wtedy oba operandy będą obiektami klasy Ulamek).
Operator konwersji również implementowany jest jako metoda statyczna i dla klasy Ulamek i typu int ma postać
public static implicit operator Ulamek(int liczba)
{
return new Ulamek(liczba, 1);
}
Proszę zwrócić uwagę na słowo kluczowe implicit - oznacza ono, że operator ten wywoływany będzie w momencie wykonania niejawnych konwersji. Przy okazji otrzymaliśmy możliwość przypisywania liczb całkowitych do obiektów klasy Ulamek:
Ulamek u;
u = 10;

Kod metody Main pozwalający sprawdzić funkcjonalność naszego nowego operatora podany został poniżej.
static void Main(string[] args)
{
Ulamek u1, u2, u3;
u1 = new Ulamek();
u2 = new Ulamek(3, 4);
u3 = new Ulamek(2, 3);
System.Console.WriteLine("u1 = {0}, dziesietnie = {1}", u1, u1.Dziesietny);
System.Console.WriteLine("u2 = {0}, dziesietnie = {1}", u2, u2.Dziesietny);
System.Console.WriteLine("u3 = {0}, dziesietnie = {1}", u3, u3.Dziesietny);
System.Console.WriteLine("{0} + {1} = {2}", u2, 2, u2 + 2);
System.Console.WriteLine("{0} - {1} = {2}", u2, 1, u2 - 1);
System.Console.WriteLine("{0} + {1} = {2}", 1, u1, 1 + u1);

}

 Zadanie 2

Zaimplementować operatory:
  • public static bool operator == (Ulamek lewy, Ulamek prawy)
  • public static bool operator != (Ulamek lewy, Ulamek prawy)
  • public static bool operator > (Ulamek lewy, Ulamek prawy)
  • public static bool operator < (Ulamek lewy, Ulamek prawy)
  • public static bool operator >= (Ulamek lewy, Ulamek prawy)
  • public static bool operator <= (Ulamek lewy, Ulamek prawy)
oraz metodę
  • public override bool Equals(object o)
przy czym ta ostatnia powinna najpierw badać, czy o jest typu Ulamek (operator is - przykład z poprzednich zajęć). Jeśli nie, od razu możemy zwrócić false, natomiast jeśli tak, należy zrzutować o na obiekt klasy Ulamek (operator as - przykład z poprzednich zajęć), po czym wykonać porównanie licznika i mianownika po normalizacji.

 Zadanie 3

Sprawdzić skutek przypisania zerowego lub ujemnego mianownika do obiektu klasy Ulamek.



Valid HTML 4.01!