11. Interfejsy, część 2

11. Interfejsy, część 2

Autor: Paweł Kruczkowski

Opublikowano: 12/5/2006, 12:00 AM

Liczba odsłon: 66582

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.

Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży
Jak wykorzystać Copilot w codziennej pracy? Kurs w przedsprzedaży

Wydarzenia