09. Struktury

09. Struktury

Autor: Paweł Kruczkowski

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

Liczba odsłon: 54511

Tematem dzisiejszego artykułu będą struktury, które są prostym typem definiowanym i często używanym przez programistów. Nauczymy się je prawidłowo definiować i deklarować, a także opowiemy sobie o różnicach występujących między nimi a klasami.

Struktury są bardzo podobne do klas, ponieważ reprezentują pewne struktury danych, które zawierają dane składowe oraz składowe funkcyjne. Innymi słowy, struktura to alternatywa dla klasy, może bowiem zawierać konstruktory, właściwości, metody, pola a nawet typy zagnieżdżone. Jednak w przeciwieństwie do klas, struktur nie możemy dziedziczyć. W strukturach również nie można używać destruktorów. Trzecią istotną różnicę między strukturą a klasą stanowi to, że ta pierwsza jest typem skalarnym, a druga – typem referencyjnym (o typach skalarnych i referencyjnych pisaliśmy na łamach portalu CentrumXP przy okazji omawiania sposobu przekazywania parametrów w języku C# 2.0).

Ta trzecia różnica ma ogromne znaczenie. Dzisiaj bowiem znamy różnicę między typami referencyjnymi a skalarami, dlatego też fakt, że struktury powinniśmy używać jedynie w przypadku małych i prostych typów nie jest dla nas żadnym zaskoczeniem. Mówiąc inaczej, struktury są przydatne do reprezentowania obiektów, które nie wymagają współpracy z typami referencyjnymi.

Struktury definiujemy niemal identycznie jak klasy:

[atrybuty] [modyfikatory dostępu] struct identyfikator [:interfejsy]
{
                składowe struktury
}

Do definicji struktur używamy słowa kluczowego: struct. Jedyną dla nas nowością w powyższym zapisie jest pojęcie interfejsów. Są one pewną alternatywą dla klas abstrakcyjnych (za tydzień opowiemy sobie szerzej o interfejsach).

Tymczasem napiszmy pierwszy program, w którym zdefiniujemy i użyjemy strukturę:

public struct Zwierzeta
{
    private string zwierzak1;
    private string zwierzak2;
 
    public Zwierzeta(string zwierzak1, string zwierzak2)
    {
        this.zwierzak1 = zwierzak1;
        this.zwierzak2 = zwierzak2;
    }
 
    public string Zwierzak1
    {
        get { return zwierzak1; }
        set { zwierzak1 = value; }
    }
 
    public string Zwierzak2
    {
        get { return zwierzak2; }
        set { zwierzak2 = value; }
    }
 
    public override string ToString()
    {
        return (String.Format("{0} oraz {1}", zwierzak1, zwierzak2));
    }
}
 
public class MojeZwierzeta
{
    public void MojaMetoda(Zwierzeta z)
    {
        z.Zwierzak1 = "pies";
        z.Zwierzak2 = "gołąb";
        System.Console.WriteLine("Moje ulubione zwierzaki to: {0}.", z);
    }
}
class Glowna
{
    static void Main()
    {
        Zwierzeta z = new Zwierzeta("kot", "kuna");
        System.Console.WriteLine("Nie lubię takich zwierzaków jak: {0}.", z);
        MojeZwierzeta mz = new MojeZwierzeta();
        mz.MojaMetoda(z);
        System.Console.WriteLine("Nie jest fajny: {0}.", z);
    }
}

Podobnie jak w przypadku klasy, aby stworzyć egzemplarz struktury należy użyć słowa kluczowego new:

                        Zwierzeta z = new Zwierzeta("kot", "kuna");

Powyższy nowy egzemplarz „z” w konstruktorze otrzymuje 2 wartości: „kot” oraz „kuna”.

Po skompilowaniu powyższego programu otrzymamy następujące wyniki:

Jak już wiemy, struktury są typami skalarnymi. Oznacza to po prostu, że gdy przekazuje się je do funkcji, to przekazuje się je poprzez wartość. W powyższym kodzie obiekt „z” przekazywany jest do metody: MojaMetoda() poprzez właśnie wartość (obiekt „z” jest przecież typu struktury Zwierzeta). W metodzie tej składowym: Zwierzak1 oraz Zwierzak2 przypisywana jest nowa wartość, która zostaje następnie wyświetlona („Moje ulubione zwierzaki to: pies oraz gołąb”). Jednak po powrocie do metody Main() w klasie głównej i ponownym wywołaniu metody WriteLine() na obiekcie „z” możemy zauważyć, że wartości te nie zostały zmienione na stałe („Nie jest fajny: kot oraz kuna”). Efekt mamy pożądany, ale dlaczego właśnie w taki sposób go otrzymaliśmy? Odpowiedź jest prosta: struktura została przekazana jako typ skalarny, dlatego też metoda MojaMetoda() operuje na kopiach a nie na oryginalnych obiektach.

Spróbujmy teraz sprawdzić powyższą tezę i zamiast inicjalizacji struktury utwórzmy klasę Zwierzeta. Nasz program będzie wyglądał w następujący sposób:

public class Zwierzeta
{
    private string zwierzak1;
    private string zwierzak2;
 
    public Zwierzeta(string zwierzak1, string zwierzak2)
    {
        this.zwierzak1 = zwierzak1;
        this.zwierzak2 = zwierzak2;
    }
 
    public string Zwierzak1
    {
        get { return zwierzak1; }
        set { zwierzak1 = value; }
    }
 
    public string Zwierzak2
    {
        get { return zwierzak2; }
        set { zwierzak2 = value; }
    }
 
    public override string ToString()
    {
        return (String.Format("{0} oraz {1}", zwierzak1, zwierzak2));
    }
}
 
public class MojeZwierzeta
{
    public void MojaMetoda(Zwierzeta z)
    {
        z.Zwierzak1 = "pies";
        z.Zwierzak2 = "gołąb";
        System.Console.WriteLine("Moje ulubione zwierzaki to: {0}.", z);
    }
}
class Glowna
{
    static void Main()
    {
        Zwierzeta z = new Zwierzeta("kot", "kuna");
        System.Console.WriteLine("Nie lubię takich zwierzaków jak: {0}.", z);
        MojeZwierzeta mz = new MojeZwierzeta();
        mz.MojaMetoda(z);
        System.Console.WriteLine("Nie jest fajny: {0}.", z);
    }
}

Gdy teraz uruchomimy powyższy program, to otrzymamy inne wyniki niż poprzednio:

>Tym razem program traktuje obiekt „z” jako typ referencyjny. Dlatego też metoda MojaMetoda() operuje teraz na oryginalnych wartościach a nie na kopiach obiektów (zmiany jakie nastąpią dzięki metodzie MojaMetoda() będą również dotyczyć oryginalnego obiektu w metodzie Main()).

Podsumowując, struktura to alternatywa dla klasy, której niestety nie możemy dziedziczyć. Innymi słowy, struktura jest zamknięta dla innych struktur oraz dla wszystkich innych klas. Struktury niejawnie dziedziczą po klasie Object, którą doskonale już znamy.

Ważną różnicą jest również to, że w strukturze nie można bezpośrednio inicjalizować pól np.:

private string zwierzak1 = "pies";
private string zwierzak2 = "kot";

jest dozwolone w klasie, a w strukturze jest już kodem niepoprawnym. Ostatnią różnicą, o której warto wspomnieć jest to, że struktury nie mogą zawierać domyślnego konstruktora bez parametrów, a także destruktora. Gdybyśmy w powyższym programie nie zainicjalizowali publicznego konstruktora Zwierzeta (przyjmąjącego 2 parametry typu string) to kompilator zainicjalizowałby tę strukturę poprzez przypisanie domyślnych wartości wszystkim jej składowym (zwierzak1 oraz zwierzak2).

Na koniec chciałbym napisać parę słów na temat tworzenia struktur bez słowa kluczowego new. Oczywiście jest to dozwolone i tak stworzona struktura jest jak najbardziej prawidłowa, ponieważ jest to zgodne z definicją typów wbudowanych. Spróbujmy więc dobrze już nam znany przykład napisać tak, aby struktura w nim nie była stworzona za pomocą słowa kluczowego new. W tym celu w metodzie Main() zdefiniujmy strukturę „z” bez słowa kluczowego new w następujący sposób:

public struct Zwierzeta
    {
        private string zwierzak1;
        private string zwierzak2;
 
        public Zwierzeta(string zwierzak1, string zwierzak2)
        {
            this.zwierzak1 = zwierzak1;
            this.zwierzak2 = zwierzak2;
        }
 
        public string Zwierzak1
        {
            get { return zwierzak1; }
            set { zwierzak1 = value; }
        }
 
        public string Zwierzak2
        {
            get { return zwierzak2; }
            set { zwierzak2 = value; }
        }
 
        public override string ToString()
        {
            return (String.Format("{0} oraz {1}", zwierzak1, zwierzak2));
        }
    }
 
    public class MojeZwierzeta
    {
        public void MojaMetoda(Zwierzeta z)
        {
            z.Zwierzak1 = "pies";
            z.Zwierzak2 = "gołąb";
            System.Console.WriteLine("Moje ulubione zwierzaki to: {0}.", z);
        }
    }
    class Glowna
    {
        static void Main()
        {
            //definiujemy strukture bez wywolania konstruktora
            Zwierzeta z;
            //uzywamy wlasciwosci
            z.Zwierzak1 = "kot";
            z.Zwierzak2 = "kuna";
            System.Console.WriteLine("Nie lubię takich zwierzaków jak: {0}.", z);
            MojeZwierzeta mz = new MojeZwierzeta();
            mz.MojaMetoda(z);
            System.Console.WriteLine("Nie jest fajny: {0}.", z);
        }
    }

Tak napisany przez nas program wyświetli prawidłowe wyniki. Niemniej jednak kompilator w czasie kompilacji zgłosi mniej więcej nastepujący błąd:

Aby ten błąd jak najszybciej poprawić musimy przypommieć sobie pojęcie właściwości. Jak już doskonale wiemy właściwości pozwalają nam na hermetyzację danych i deklarowanie ich jako prywatne. Ale – jak też pamiętamy – w rzeczywistości są metodami składowymi, a wywołanie metody przed inicjalizacją zmiennych składowych, których to te metody będą wykorzystywać jest procesem niedozwolonym (o czym kompilator nas czym prędzej poinformuje, co widzimy powyżej). Tak więc, aby powyższy błąd natychmiast poprawić, musimy również przypisać wszystkim zmiennym składowym danej struktury odpowiednie wartości:

public struct Zwierzeta
{
    public string zwierzak1;
    public string zwierzak2;
 
    public Zwierzeta(string zwierzak1, string zwierzak2)
    {
        this.zwierzak1 = zwierzak1;
        this.zwierzak2 = zwierzak2;
    }
 
    public string Zwierzak1
    {
        get { return zwierzak1; }
        set { zwierzak1 = value; }
    }
 
    public string Zwierzak2
    {
        get { return zwierzak2; }
        set { zwierzak2 = value; }
    }
 
    public override string ToString()
    {
        return (String.Format("{0} oraz {1}", zwierzak1, zwierzak2));
    }
}
 
public class MojeZwierzeta
{
    public void MojaMetoda(Zwierzeta z)
    {
        z.Zwierzak1 = "pies";
        z.Zwierzak2 = "gołąb";
        System.Console.WriteLine("Moje ulubione zwierzaki to: {0}.", z);
    }
}
 
class Glowna
{
    static void Main()
    {
        //definiujemy strukture bez wywolania konstruktora
        Zwierzeta z;
        //przypisanie do zmiennych odpowiednich wartosci, w naszym przypadku bedzie to pusty string
        z.zwierzak1 = "";
        z.zwierzak2 = "";
        //uzywamy wlasciwosci
        z.Zwierzak1 = "kot";
        z.Zwierzak2 = "kuna";
        System.Console.WriteLine("Nie lubię takich zwierzaków jak: {0}.", z);
        MojeZwierzeta mz = new MojeZwierzeta();
        mz.MojaMetoda(z);
        System.Console.WriteLine("Nie jest fajny: {0}.", z);
    }
 }

Teraz nasz program skompiluje się bez żadnych przeszkód i otrzymamy prawidłowe wyniki, mimo że zadeklarowaliśmy strukturę bez słowa kluczowego: new.

Powyższy przykład demonstruje nam przede wszystkim różnicę między strukturami a klsami. Oczywiście z punktu widzenia programisty tworzenie struktur bez słówka new ma niewielkie zalety, dlatego też nie zaleca się tak pisać programów. Dlaczego? Bo takie pisanie kodu pogarsza tylko jego czytelność oraz zwiększa podatność na błędy.

Mamy nadzieje, że powyższe informacje na temat struktur będą dla nas – początkujących programistów - bardzo przydatne i czasem użyjemy ich w swoich programach. Szczególnie tam, gdzie będziemy chcieli zbudować mały i prosty typ. Struktura jako alternatywa dla klasy ma być bowiem prosta, ale za to wydajna i dlatego też czasem warto z niej skorzystać.

Struktury nie wolno używać w dziedziczeniu, ale same mogą dziedziczyć interfejsy. O nich już za tydzień na łamach portalu CentrumXp.pl.