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.