Tydzień temu na łamach portalu CentrumXP.pl zostało
wprowadzone nowe pojęcie, które odgrywa w świecie programistów ogromną rolę..
Interfejsy – bo o nich jest tutaj mowa – są kontraktem jaki zostaje utworzony
pomiędzy klasą a użytkownikiem. Jest to kontrakt, który musi zostać w pełni
wypełniony po stronie klasy. Oznacza to, że musi ona zaimplementować wszystkie
metody czy właściwości dziedziczonego interfejsu.
Potrafimy już definiować interfejsy i je implementować.
Nauczyliśmy się również używać naraz kilku interfejsów, a także je rozszerzać.
Dzisiaj będziemy kontynuować temat interfejsów i poznamy kilka nowych zagadnień
z nimi związanych. Między innymi opowiemy sobie o słowach kluczowych is
oraz as, o sposobach przesłaniania interfejsów czy o mechanizmie jawnej
implementacji interfejsu.
Jak już wiemy, możliwe jest rozszerzanie już istniejącego
interfejsu poprzez dodanie do niego jakiejś nowej metody lub właściwości. Łatwo
można się domyśleć, że interfejsy można łączyć ze sobą: tworzymy nowy interfejs
i łączymy go z już istniejącym oraz w razie potrzeby dodajemy nowe metody czy
też inne elementy nowego interfejsu. Na początek prześledźmy poniższy przykład:
interface IMojInterfejs
{
int Dodawanie();
int Wynik
{
get;
set;
}
}
interface IMnozenie
{
int
Mnozenie();
}
interface IOperacje : IMnozenie
{
int Dzielenie();
int
KwadratSumy();
}
public class MojaKlasa : IMojInterfejs,
IOperacje
{
int a, b;
//przechowuje wartosc
wlasciwosci
private int _wynik = 0;
//konstruktor klasy
MojaKlasa
public
MojaKlasa(int a, int
b)
{
this.a = a;
this.b = b;
}
//implementacja metody
Dodawanie() z interfejsu IMojInterfejs
public
int Dodawanie()
{
return a + b;
}
//implementacja
wlasciwosci z interfejsu IMojInterfejs
public int Wynik
{
get { return _wynik;
}
set { _wynik = value;
}
}
//implementacja metod
z interfejsu IOperacje
public int Dzielenie()
{
return a /
b;
}
//implementacja metody
z interfejsu IKwadrat, obslugiwany przez interfejs IOperacje
public
int KwadratSumy()
{
return (a + b) * (a + b);
}
public
int Mnozenie()
{
return a * b;
}
}
public class Glowna
{
static
void Main()
{
MojaKlasa mk
= new MojaKlasa(36,
6);
//rzutowanie mk na
rozne interfejsy
IMojInterfejs imMk = mk as
IMojInterfejs;
if (imMk != null)
{
imMk.Wynik =
imMk.Dodawanie();
System.Console.WriteLine("Suma liczb: 36 i 6 wynosi: {0}",
imMk.Wynik);
}
IMnozenie
imnMk = mk as IMnozenie;
if (imnMk !=
null)
{
mk.Wynik = imnMk.Mnozenie();
System.Console.WriteLine("Mnożenie liczb: 36 i 6 wynosi: {0}",
mk.Wynik);
}
IOperacje io
= mk as IOperacje;
if (io != null)
{
System.Console.WriteLine("Dzielenie liczby: 36 przez liczbę: 6 wynosi:
{0}", io.Dzielenie());
System.Console.WriteLine("Kwadrat sumy liczb: 36 i 6 wynosi: {0}",
io.KwadratSumy());
}
}
}
W
powyższym przykładzie łączymy ze sobą 2 interfejsy: IOpercje obsługuje
istniejący już interfejs IMnozenie. W ten sposób interfejs IOperacje
łączy w jednym ciele metody swoje z metodą interfejsu IMnozenie.
Nasz
programik potrzebuje jeszcze parę słów komentarza, bowiem zastosowaliśmy w nim
nowe dla nas słowo. A mianowicie chodzi o: as. W poniższym fragmencie
kodu utworzyliśmy obiekt klasy MojaKlasa:
MojaKlasa mk = new MojaKlasa(36, 6);
IMojInterfejs imMk = mk as
IMojInterfejs;
if (imMk != null)
{
imMk.Wynik =
imMk.Dodawanie();
System.Console.WriteLine("Suma
liczb: 36 i 6 wynosi: {0}", imMk.Wynik);
}
a
następnie używamy go jako egzemplarza interfejsu IMojInterfejs. Innymi
słowy, jeśli nie jesteśmy pewni, czy nasza klasa (w tym przypadku MojaKlasa)
obsługuje dany interfejs (czyli IMojInterfejs) to możemy zrzutować
obiekt tej klasy używając operatora as i w ten sposób sprawdzić, czy wynikiem
takiego rzutowania jest null (co oznacza, że po prostu nasza klasa nie
obsługuje danego interfejsu) czy też jakaś wartość (co oznacza, że nasza klasa
obsługuje żądany interfejs).
Kiedy
obiekt klasy, która obsługuje dany interfejs zostanie prawidłowo zrzutowany na
ten interfejs, wówczas obiekt ten może wywoływać wszystkie metody, właściwości
i inne zdarzenia zrzutowanego interfejsu.
Zanim
przejdziemy dalej, należy prawidłowo zdefiniować pojęcie „egzemplarza
interfejsu”. W żargonie programistycznym bardzo często tak się mówi, jednak nie
jest to prawidłowe. Precyzyjniej powinno się określać to pojęcie jako
referencja na obiekt, który implementuje dany interfejs.
W
wyniku uruchomienia powyższego przykładu otrzymaliśmy następujące wyniki:

Spróbujmy
teraz napisać dobrze już nam znany przykład w trochę inny sposób:
interface IMojInterfejs
{
int Dodawanie();
int Wynik
{
get;
set;
}
}
interface IMnozenie
{
int Mnozenie();
}
interface IOperacje : IMnozenie
{
int Dzielenie();
int
KwadratSumy();
}
public class MojaKlasa : IMojInterfejs,
IOperacje
{
int
a, b;
private
int _wynik = 0;
public
MojaKlasa(int a, int
b)
{
this.a = a;
this.b = b;
}
public
int Dodawanie()
{
return a + b;
}
public
int Wynik
{
get { return _wynik;
}
set { _wynik = value;
}
}
public
int Dzielenie()
{
return a / b;
}
public
int KwadratSumy()
{
return (a + b) * (a + b);
}
public
int Mnozenie()
{
return a * b;
}
}
public class Glowna
{
static
void Main()
{
MojaKlasa mk = new MojaKlasa(45, 8);
if (mk is IMojInterfejs)
{
IMojInterfejs imMk = (IMojInterfejs)mk;
imMk.Wynik = imMk.Dodawanie();
System.Console.WriteLine("Suma liczb: 45 i 8 wynosi: {0}",
imMk.Wynik);
}
if (mk is IMnozenie)
{
IMnozenie
imnMk = (IMnozenie)mk;
mk.Wynik = imnMk.Mnozenie();
System.Console.WriteLine("Wynik mnożenia liczb: 45 i 8 wynosi: {0}",
mk.Wynik);
}
if (mk is IOperacje)
{
IOperacje
io = (IOperacje)mk;
System.Console.WriteLine("Dzielenie liczby: 45 przez liczbę: 8 wynosi:
{0}", io.Dzielenie());
System.Console.WriteLine("Kwadrat sumy liczb: 45 i 8 wynosi: {0}",
io.KwadratSumy());
}
}
}
Powyższy
przykład ma taką samą logikę biznesową jak poprzedni, ale różni się jedną zasadniczą
rzeczą. A mianowicie zastosowaliśmy w nim nowy operator, jakim jest słówko is.
W poniższym fragmencie kodu zdefiniowaliśmy sobie obiekt mk typu MojaKlasa:
MojaKlasa mk = new MojaKlasa(45, 8);
if (mk is IMojInterfejs)
{
IMojInterfejs
imMk = (IMojInterfejs)mk;
imMk.Wynik = imMk.Dodawanie();
System.Console.WriteLine("Suma liczb: 45 i 8 wynosi: {0}",
imMk.Wynik);
}
a następnie sprawdzamy, czy obiekt ten obsługuje
interfejs IMojInterfejs. W tym celu używamy właśnie operatora is.
Operator ten zwraca wartość true, gdy obiekt mk można zrzutować
na dany sprawdzany typ (czyli interfejs ImojInterfejs). W przeciwnym
przypadku operator is zwraca false. W powyższym fragmencie kodu
obiekt mk zwraca true, a więc możemy go bez żadnych przeszkód
zrzutować na interfejs IMojInterfejs, a następnie na referencji
wskazującej obiekt implementujący ten interfejs wywoływać odpowiednie metody
oraz właściwości.
Na koniec krótkie podsumowanie: operator is
sprawdza czy można rzutować wyrażenie na dany typ, natomiast operator as łączy
w sobie funkcję właśnie operatora is, a także cast. W pierwszej
kolejności as sprawdza, czy dane rzutowanie jest dozwolone (czyli czy
operator is zwraca true), a gdy ten warunek jest spełniony, to wykonuje
rzutowanie.
Używanie operatora as eliminuje potrzebę
obsługi wyjątków (o wyjątkach napiszemy sobie wkrótce na łamach portalu
CentrumXP), jednocześnie zwiększa wydajność naszego programu związanego z
podwójnym sprawdzaniem wydajności bezpieczeństwa rzutowania. Dlatego też
optymalnym rozwiązaniem jest rzutowanie interfejsów za pomocą słowa kluczowego as.
Drugim punktem niniejszego artykułu jest
przesłanianie implementacji interfejsu. W klasie, która obsługuje dany
interfejs, metody tego interfejsu możemy oznaczyć jako wirtualne. W klasach
pochodnych możemy więc przesłaniać implementację tych metod, dzięki czemu
możliwe jest używanie klas w sposób polimorficzny. Poniższy przykład prezentuje
ten mechanizm:
interface IMojInterfejs
{
int
Wynik
{
get;
set;
}
}
interface IOperacje
{
int Dodawanie();
int
Odejmowanie();
}
public class KlasaPierwsza : IMojInterfejs,
IOperacje
{
int a, b;
public
KlasaPierwsza(int a, int
b)
{
this.a = a;
this.b = b;
}
private
int _wynik = 0;
public
int Wynik
{
get { return _wynik;
}
set { _wynik = value;
}
}
public
int Dodawanie()
{
return a + b;
}
public
virtual int
Odejmowanie()
{
return a - b;
}
}
public class KlasaDruga : KlasaPierwsza
{
int x, y;
public
KlasaDruga(int a, int
b) : base(a, b)
{
this.x = a;
this.y = b;
}
public
override int
Odejmowanie()
{
return (2 * x) - (2 * y);
}
}
class Glowna
{
static void Main()
{
KlasaPierwsza
kp = new KlasaPierwsza(13,
7);
IMojInterfejs im = kp as
IMojInterfejs;
IOperacje io
= kp as IOperacje;
if (im != null
&& io != null)
{
im.Wynik = io.Dodawanie();
System.Console.WriteLine("Suma dwóch liczb wynosi: {0}", im.Wynik
+ ".");
im.Wynik = io.Odejmowanie();
System.Console.WriteLine("Różnica dwóch liczb wynosi: {0}",
im.Wynik + ".");
}
KlasaDruga
kd = new KlasaDruga(10,
4);
IOperacje
iod = kd as IOperacje;
if (iod != null)
System.Console.WriteLine("Różnica dwóch liczb wynosi: {0}",
iod.Odejmowanie() + ".");
}
}
KlasaPierwsza obsługuje dwa interfejsy: IMojInterfejs
oraz IOperacje. Klasa ta implementuje wszystkie metody oraz właściwości
tych interfejsów, przy czym metoda Odejmowanie() jest zainicjowana w
niej jako metoda wirtualna. KlasaDruga, która dziedziczy po klasie KlasaPierwsza
nie musi przesłaniać tej metody, ale jest to dozwolone i właśnie taka sytuacja
ma miejsce w naszym programiku. W klasie głównej widzimy polimorficzne
wykorzystanie metody Odejmowanie(). Najpierw wywoływana jest ona za
pomocą referencji na egzemplarz klasy KlasaPierwsza wskazującej na
interfejs IOperacje, a poźniej wywoływana jest za pomocą referencji na
obiekt kd (typu KlasaDruga) wskazującej na interfejs IOperacje
(tutaj wywoływana jest właśnie przesłonięta wersja metody Odejmowanie()).
Po
skompilowaniu i uruchomieniu powyższego przykładu otrzymamy następujące wyniki:

Na
koniec chcielibyśmy napisać parę słów o jawnej implementacji interfejsów.
Często
się zdarza, że klasa obsługuje np. 2 interfejsy, które maja w swoim ciele
zdefiniowane 2 metody o tej samej sygnaturze i zwracanym typie. W takiej
sytuacji jasne jest, że w danej klasie nie będziemy mogli zaimplementować tych
metod, mimo że będą mięć różną logikę. Aby rozwiązać ten problem, musimy użyć
mechanizmu jawnej implementacji interfejsów. Poniższy przykład to pokazuje:
interface IMojInterfejs
{
int Dodawanie();
int Wynik
{
get;
set;
}
}
interface IOperacje
{
int Odejmowanie();
int Dodawanie();
}
public class MojaKlasa : IMojInterfejs,
IOperacje
{
int a, b;
public
MojaKlasa(int a, int
b)
{
this.a = a;
this.b = b;
}
private
int _wynik = 0;
public
int Wynik
{
get { return _wynik;
}
set { _wynik
= value; }
}
public int Dodawanie()
{
return a + b;
}
public int Odejmowanie()
{
return a -
b;
}
int IOperacje.Dodawanie()
{
return (2 *
a) + (2 * b);
}
}
class Glowna
{
static
void Main()
{
MojaKlasa mk
= new MojaKlasa(18,
14);
mk.Wynik = mk.Dodawanie();
System.Console.WriteLine("Implemetacja metody IMojInterfejs.Dodawanie. Wynik
wynosi: {0}", mk.Wynik +".");
mk.Wynik = mk.Odejmowanie();
System.Console.WriteLine("Implementacje metody IOperacje.Odejmowanie. Wynik
wynosi: {0}", mk.Wynik +".");
IOperacje io
= mk as IOperacje;
if (io != null)
System.Console.WriteLine("Implementacja metody IOperacje.Dodawanie. Wynik
wynosi: {0}", io.Dodawanie());
}
}
W
powyższym przykładzie zarówno IMojInterfejs jak i IOperacje mają w swym ciele zdefiniowaną
metodę Dodawanie(), o tej samej sygnaturze i zwracanym typie. Aby można
było obie zainicjować prawidłowo w klasie MojaKlasa, należy jedną z nich
jawnie zaimplementować. Taka jawna implementacja tejże metody zdefiniowanej w
interfejsie IOperacje odbywa się w następującym fragmencie kodu:
int IOperacje.Dodawanie()
{
return (2 *
a) + (2 * b);
}
Dostęp w klasie głównej do tej metody nie jest
możliwy poprzez obiekt klasy MojaKlasa. Jedynym sposobem dostania się do
tej metody jest zrzutowanie obiektu obsługującej go klasy:
IOperacje io = mk as IOperacje;
if (io != null)
System.Console.WriteLine("Implementacja metody IOperacje.Dodawanie. Wynik
wynosi: {0}", io.Dodawanie());/code>
Należy pamiętać również o tym, że przed jawnie
zaimplementowaną metodą nie może znajdować się żaden modyfikator dostępu.
Metoda taka jest po prostu niejawnie publiczna. Metoda ta nie może też zawierać
takich modyfikatorów jak: abstract, virtual, override oraz new.
Po uruchomieniu powyższygo przykładu otrzymamy
następujące wyniki:

W niniejszym artykule wprowadziliśmy sobie nowe
pojęcia takie jak: operatory is oraz as, a także przesłanianie
interfejsów oraz mechanizm jawnej ich implementacji. Po tej styczności z
interfejsami, każdy z nas powienien wiedzieć do czego one służą i jak stosować.
Na pewno temat interfejsów nie został w pełni wyczerpany, ale najważniejsze
rzeczy zostały o nich powiedziane.
Za tydzień natomiast opowiemy sobie o interfejsach
kolekcji i skupimy się przede wszystkim na słownikach, jakie są dostępne w
języku C# 2.0.