08. Porozmawiajmy o klasach

08. Porozmawiajmy o klasach

Autor: Paweł Kruczkowski

Opublikowano: 11/14/2006, 12:00 AM

Liczba odsłon: 77891

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.

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

Wydarzenia