W
dzisiejszym artykule będziemy kontynuować temat klas w języku C# 2.0. Na łamach
portalu CentrumXP zostało już o nich bardzo wiele napisane i każdy z nas
potrafi prawidłowo zdefiniować pojęcie klasy jak i bez żadnego problemu ją
zaimplementować w swoim programie.
Dzisiaj
powiemy sobie jeszcze o klasach zamkniętych, klasie Object oraz samym
mechanizmie zagnieżdżania klas.
Tydzień
temu poznaliśmy sposób definiowania oraz stosowania klas abstrakcyjnych. Jak
pamiętamy, są to klasy, które stanowią w pewnym sensie kontrakt dla klas
pochodnych, które dziedziczą właśnie klasę abstrakcyjną. Innymi
słowy, klasa abstrakcyjna opisuje publiczne metody klas pochodnych. Nie jest
przypadkiem, że o tych klasach w tym miejscu wspominamy, ponieważ ich
przeciwieństwem są tzw. klasy zamknięte. Klasy te charakteryzują się tym, że od
nich nie można w ogóle tworzyć klas pochodnych (w przeciwieństwie do klas
abstrakcyjnych). Napiszmy na początek prosty programik, w którym użyjemy klasy
abstrakcyjnej:
abstract public class Figura
{
protected double e,
f;
public Figura(double
e, double f)
{
this.e = e;
this.f = f;
}
abstract public void Komunikat();
}
class Romb : Figura
{
public Romb(double e,
double f) : base(e,
f)
{ }
public override void Komunikat()
{
System.Console.WriteLine("Program
obliczający pole rombu.");
}
public double
ObliczPole()
{
return (e * f) / 2;
}
}
class Glowna
{
static void Main(string[] args)
{
Romb r = new Romb(6, 8);
double wynik = r.ObliczPole();
r.Komunikat();
System.Console.WriteLine("Pole naszego rombu wynosi: {0}", wynik +".");
}
}
W powyższym przykładzie abstrakcyjna klasa Figura definiuje publiczną metodę Komunikat(), która z kolei jest
przesłonięta w klasie Romb wg
poznanych już przez nas zasad. A więc mechanizm dziedziczenia i polimorfizmu
jest w jak najlepszym stopniu prawidłowo zastosowany, dlatego też po
skompilowaniu i uruchomieniu powyższego kodu otrzymamy następujące wyniki:

Jak już wiemy, przeciwieństwem klasy abstrakcyjnej jest tzw.
klasa zamknięta, którą spróbujmy wprowadzić do powyższego przykładu w
nastepujący sposób:
sealed public class Figura
{
protected double e,
f;
public Figura(double
e, double f)
{
this.e = e;
this.f = f;
}
abstract public void Komunikat();
}
class Romb : Figura
{
public Romb(double e,
double f) : base(e,
f)
{ }
public override void Komunikat()
{
System.Console.WriteLine("Program
obliczający pole rombu.");
}
public double
ObliczPole()
{
return (e * f) / 2;
}
}
class Glowna
{
static void Main(string[] args)
{
Romb r = new Romb(6, 8);
double wynik = r.ObliczPole();
r.Komunikat();
System.Console.WriteLine("Pole naszego rombu wynosi: {0}", wynik +".");
}
}
Jak widzimy, klasy zamknięte definiujemy poprzez użycie słowa
kluczowego: sealed. Słówko to
umieszczone przed deklaracją klasy zapobiega tworzeniu klas od niej pochodnej.
Dlatego też w trakcie kompilacji powyższego kodu otrzymamy następujący
komunikat:

Pierwszy błąd informuje nas, że klasa Figura jest klasą
zamkniętą, a więc nie możemy już jej dziedziczyć. Natomiast dwa kolejne
ostrzeżenia mówią nam, że w klasie zamkniętej nie jest możliwe tworzenie
składowych chronionych (składowe: e oraz f). Oczywiście pojawiłoby się jeszcze
wiele kolejnych błędów, np. klasa zamknięta nie może definiować metod
abstrakcyjnych idt. Warto również wywnioskować w tym miejscu fakt, że
niedozwolone jest jednoczesne zadeklarowanie klasy jako abstrakcyjnej i
zamkniętej. Dlaczego? Odpowiedź jest prosta: ponieważ klasa abstrakcyjna nie
jest kompletna i dopiero w podklasach implementujemy jej zawartości
(abstrakcyjne metody).
Pewnie część z nas zastanawia się czy warto w praktyce
stosować klasy zamknięte (a co za tym idzie również metody zamknięte –
zadeklarowanie klasy jako sealed
jawnie deklaruje także wszystkie jej metody jako sealed)? Są przede wszsytkim 2 takie sytuacje, w których odpowiedź
na powyższe pytanie jest twierdząca. Po pierwsze, gdy chcemy zapobiec
przesłanianiu, to stosujemy klasy zamknięte:
public class Pierwsza
{
sealed public virtual void
MojaMetoda()
{
Console.WriteLine("Moja metoda zamknięta.");
}
}
class Druga : Pierwsza
{
public override void
MojaMetoda()
{
base.MojaMetoda();
Console.WriteLine("Niedozwolone!");
}
}
Po drugie,
używamy klas zamkniętych, aby zapobiec dziedziczeniu:
sealed public class Pierwsza
{
public void MojaMetoda()
{
Console.WriteLine("Moja klasa zamknieta.");
}
}
class Druga : Pierwsza
{
Console.WriteLine("Niedozwolone dziedziczenie!");
}
Podsumowując,
klasy zamknięte z punktu widzenia dziedziczenia oraz polimorfizmu odgrywają
istotną rolę. Oczywiście tworzenie obiektów klas zamkniętych jest czynnością
jak najbardziej dozwoloną:
sealed public class Program
{
public int a, b;
public Program (int
a, int b)
{
this.a = a;
this.b = b;
}
public int Suma()
{
return a + b;
}
}
class glowna
{
static void Main(string[] args)
{
Program
pr = new Program(6,
4);
System.Console.Write("Wynik dodawania wynosi: {0}", pr.Suma()+
"\n");
}
}
Powyższy
program bez żadnych kłopotów się skompiluje i otrzymamy wynik dodawania dwóch
liczb całkowitych:

Deklaracja
klasy zamkniętej jawnie deklaruje wszystkie metody tej klasy jako metody
zamknięte (bardziej elegancko mówi się na nie metody ostateczne). Są to metody,
których dalsze przesłanianie w klasach pochodnych nie jest możliwe. Prześledźmy
poniższy kod:
class A
{
public virtual void
PierwszaMetoda()
{
System.Console.WriteLine("Mamy tutaj wywołanie: A.PierwszaMetoda();");
}
public virtual void
DrugaMetoda()
{
System.Console.WriteLine("Mamy tutaj wywołanie: A.DrugaMetoda();");
}
}
class B : A
{
public override void PierwszaMetoda()
{
System.Console.WriteLine("Mamy tutaj wywołanie: B.PierwszaMetoda();");
}
public sealed override void
DrugaMetoda()
{
System.Console.WriteLine("Mamy tutaj wywołanie: B.DrugaMetoda();");
}
}
class C : B
{
public override void PierwszaMetoda()
{
System.Console.WriteLine("Mamy tutaj wywołanie: C.PierwszaMetoda();");
}
}
W klasie B
zdefiniowaliśmy dwie metody przesłaniające. Jedna z nich jest również metodą
ostateczną (metodaDrugaMetoda())
co sprawia, że metody tej nie można już przesłonić w klasie C.
Drugim
punktem naszego dzisiejszego tematu jest klasa Object. Jest to klasa główna (ang. root) w języku C#, od której zaczyna się hierarchia dziedziczenia.
Innymi słowy, wszystkie klasy są traktowane jako klasy pochodne od właśnie
klasy Object. Klasa ta udostępnia nam wiele wirtualnych metod, które z kolei
możemy i często przesłaniamy w klasach pochodnych.
W poniższej
tabelce przedstawiamy wybrane metody klasy Object
Equals() - sprawdza, czy dwa obiekty są sobie równe;
GetType() - sprawdza typ danego obiektu;
ToString() - zwraca łańcuch znaków, który reprezentuje dany obiekt;
Ważną
informacją jest również to, że podstawowe typy danych (takie
jak np. liczby całkowite) są pochodne od klasy Object. Prezentuje to
poniższy przykład, jak również ukazuje sposób stosowania metody
ToString(), która jest również
tworem klasy Object:
public class MojaKlasa
{
protected int a;
public MojaKlasa(int
a)
{
this.a = a;
}
public override string ToString()
{
return a.ToString();
}
}
class Glowna
{
static void Wyswietl(Object o)
{
System.Console.WriteLine("Wartość obiektu przekazanego do metody Wyswietl()
wynosi: {0}", o.ToString() +".");
}
static void Main(string[] args)
{
int
liczba = 8;
System.Console.WriteLine("Wartość zmiennej: liczba wynosi: {0}",
liczba.ToString());
Wyswietl(liczba);
MojaKlasa
mKlasa = new MojaKlasa(23);
System.Console.WriteLine("Wartość zmiennej w obiekcie mKlasa wynosi: {0}",
mKlasa.ToString());
Wyswietl(mKlasa);
}
}
W klasie Object
jest zainicjalizowana wirtualna metoda ToString(),
która zwraca łańcuch znaków i nie przyjmuje żadnego parametru. Wszsytkie typy
wbudowane, takie jak liczby całkowite, mogą korzystać z tej metody, którą
wówczas dziedziczą od klasy głównej. W powyższym przykładzie w klasie MojaKlasa, metoda ToString() została przesłonięta tak, aby zwracała odpowiednią
wartość. Jeśli usuniemy tę nową wersję metody ToString(), to automatycznie zostanie wywołana metoda z klasy
bazowej, a więc z klasy Object.
Jak łatwo wywnioskować z powyższego przykładu, nie trzeba
jawnie dziedziczyć po klasie Object. Dzieje się to w sposób automatyczny i
oczywiście tylko w przypadku klasy głównej. Po skompilowaniu powyższego
programu otrzymamy następujące wyniki:

Ostatnim
punktem dzisiejszego artykułu, który chcemy poruszyć jest mechanizm
zagnieżdżania klas. Często spotykamy się z sytuacją, w której chcemy zbudować
klasę pomocniczą wewnątrz jakiejś klasy. Tę klasę pomocniczą nazywamy klasą
zagnieżdżoną, a klasę zewnętrzną - która będzie mieć w swojej definicji klasę
pomocniczą – klasą zawierającą. Klasy zagnieżdżone mają dostęp do wszystkich składowych jakie zostały zadeklarowane w klasie zewnętrznej.
Aby uzyskać
dostęp do publicznej klasy zagnieżdżonej należy użyć kwalifikatora w postaci
nazwy klasy zawierającej. W poniższym fragmencie kodu:
public class
KlasaZawierajaca
{
public
class KlasaZagniezdzona
{
}
}
dostęp do
klasy KlasaZagniezdzona jest
następujący:
KlasaZawierajaca.KlasaZagniezdzona
Na koniec
napiszmy przykład, w którym zastosujemy mechanizm zagnieżdżania klas:
public class Kolory
{
private string kolor1, kolor2;
public Kolory(string
kolor1, string kolor2)
{
this.kolor1 = kolor1;
this.kolor2 = kolor2;
}
public override string ToString()
{
return String.Format("Mamy następujące kolory: {0} i {1}",
kolor1, kolor2 +".");
}
public class KlasaZagniezdzona
{
public void Wyswietl(Kolory
k)
{
Console.WriteLine("Pierwszy
kolor to: {0}", k.kolor1.ToString());
Console.WriteLine("Drugi
kolor to: {0}", k.kolor2.ToString());
}
}
}
class Glowna
{
static void Main(string[] args)
{
Kolory
k = new Kolory("biały", "czarny");
System.Console.WriteLine("{0}",
k.ToString());
Kolory.KlasaZagniezdzona kol = new
Kolory.KlasaZagniezdzona();
kol.Wyswietl(k);
}
}
Zagnieżdżona klasa to: KlasaZagniezdzona,
która udostępnia metodę Wyswietl().
Co jest warte odnotowania, to fakt, że klasa ta ma dostęp do prywatnych
składowych klasy Kolory (k.kolor1
oraz k.kolor2), do których to nie mają dostępu inne klasy.
Aby zadeklarować egzemplarz klasy zagnieżdżonej, należy
najpierw podać nazwę klasy zewnętrznej:
Kolory.KlasaZagniezdzona
kol = new Kolory.KlasaZagniezdzona();
W wyniku
uruchomienia powyższego programu otrzymamy następujące wyniki:

W
dzisiejszym artykule opowiedzieliśmy sobie o klasach zamkniętych, o głównej
klasie Object oraz mechanizmie zagnieżdżania klas. Miejmy nadzieję, że proste,
przytoczone przez nas przykłady spowodowały, że od dziś powyższe pojęcia nie
będą dla nas obce.
Za tydzień
na łamach portalu Centrum.XP wprowadzimy sobie kolejne nowe pojęcie. Mam tutaj
na myśli zdefiniowanie struktur w języku C# 2.0.