UWAGA! Promocja dla firm - MICROSOFT OFFICE 365  na 12 miesiecy ZA DARMO! Tylko na CentrumXP.pl!
Wielka promocja Office 365 na CentrumXP.pl!
Do góry Skomentuj

11. Interfejsy, część 2

11. Interfejsy, część 2

Autor: Paweł Kruczkowski Opublikowano: 5 grudnia 2006 Odsłon: 45 101

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());

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.

Zobacz również

Komentarze

Jeśli po przeczytaniu obu artykułów o interfejsach ktoś nadal ich nie rozumie to jest naprawdę geniuszem... naprawdę dobry artykuł...
KarolT, 1 czerwca 2007, 00:51
Wydrukujcie ten kurs, a chętnie kupię taką książkę :) - jestem przyzwyczajony do papieru :)
JAcek, 1 września 2007, 13:37
Przydałaby się (jak kolega wyżej wspomniał) wersja może nie tyle papierowa, ale cyfrowa w postaci dokumentu *.pdf bądź czegoś co można by pobrać i wydrukować (bez kopiuj/wklej) =)
Ender, 17 listopada 2007, 14:51
nie no Panowie zabierają się za programowanie a pdf"a nie potrafią zrobić sami (a wystarczy jeden jedyny przycisk kliknąć jak się ma zainstalowanego Adobe"a) a co do interfejsów to jest to naprawde genialna sprawa!!:)
shibby, 12 lutego 2008, 21:18
dzieki naprawde sie przydalo
;], 9 grudnia 2008, 00:26
nie wiem czym wy sie tak ekscytujecie

programmer, 8 lutego 2009, 10:36
Nie za bardzo rozumiem te interfejsy. W sumie mozna sie bez nich obejsc i w zasadzie program powinien dzialac tak samo.
Nie bardzo rozumiem., 11 marca 2009, 22:52
W praktycznie kazdym kursie , w rozdziale Interfejsy brakuje odpowiedzi na fundamentalne pytanie stawiane przez poczatkujacych programistow - " Ale po jaka cholere ? " . Implementacja jest prosta jak budowa cepa, ale jakie sa zastosowania interfejsow ? I nie chodzi mi o jeden przyklad typu "A zeby klasa ladnie wygladala" tylko konkretnie, w jakich przypadkach sie stosuje i jakie korzysci plyna z implementacji . W przeciwnym razie taka wiedza jest zupelnie bezuzyteczna.

The Beginner, 1 maja 2009, 22:45
Ja to rozumiem tak (też początkujący), że jeśli piszesz program taki jak w tym przykładzie, czy inny mało skomplikowany, to interfejsy możesz sobie darować. Rozumiem, że przydają się przy pisaniu aplikacji wielkich, nad którymi np. pracuje więcej niż jedna osoba, gdzie można się pogubić, gdy jedna klasa zawiera kilkadziesiąt metod. Wydaje mi się, że wtedy może mieć to sens i wtedy interfejsy poza tym, że dbają, aby nie pominąć jakiejś metody, czy pola to działają tak jakby słowniki, spisy treści. No ale to moja interpretacja, może nie do końca właściwa..

arystosedes, 15 sierpnia 2009, 23:27
Po co interfejsy? Faktycznie to pytanie jest najbardziej istotne, niestety nie ma w artykule odpowiedzi. A po to żeby grupować klasy, tworzyć tablice różnych typów (w tym przypadku obiekty różnych klas), które implementują ten sam interfejs. Generalnie porządkujemy kod (klasy) wg ich przeznaczenia. Wychodzi w praniu przy dużych projektach.
bartas, 20 marca 2010, 21:42
nie do końca jarzę sens istnienia operatora is po co obsługa wyjątku w kodzie jeśli dużo łatwiej jest po prostu fizycznie sprawdzić czy nasza klasa implementuje dany interface czy nie, chyba że ten operator ma szersze zastosowanie o którym nie wiem...
dl, 19 maja 2011, 11:08
Interfejsy są elementem projektu technicznego aplikacji, a nie elementem programowania - de facto. Możliwość wprowadzenia Intefejsów do kodu programu wizualizuje (formalizuje?) tę część pracy nad aplikacją, która w językach bez obsługi mechanizmu Interfejsów była robiona na papierze, lub "na gębę". Praca nad zdefiniowaniem Interfejów pozwala się skupić na opracowaniu modelu rozwiązania problemu bez potrzeby wchodzenia w dżunglę, jaką jest docelowy kod programu z obsługą wszystkich programistycznych szczegółów. Przyczyną wielu trudnych problemów w pracach programistycznych jest zbyt wczesne wchodzenie w kodowanie, bez solidnego zrozumienia problemu i algorytmu bedącego jego rozwiązaniem.
janf, 10 sierpnia 2011, 12:48

Dodaj swój komentarz

Zasady publikacji komentarzyZasady publikacji komentarzy

Redakcja CentrumXP.pl nie odpowiada za treść komentarzy publikowanych na stronach Portalu
i zastrzega sobie prawo do usuwania wypowiedzi, które:

  • zawierają słowa wulgarne, obraźliwe, prowokujące i inne naruszające dobre obyczaje;
  • są jedynie próbami reklamowania stron internetowych (spamowanie poprzez umieszczanie linków);
  • przyczyniają się do złamania prawa bądź warunków licencyjnych oprogramowania (cracki, seriale, torrenty itp.);
  • zawierają dane osobowe, teleadresowe, adresy mailowe lub numery GG;
  • merytorycznie nie wnoszą nic do dyskusji lub nie mają związku z tematem komentowanego newsa, artykułu bądź pliku.

Autor:

Komentarz:

Dodaj komentarz