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.