struct - C#

struct - C#

Avtor: Aleš Brelih

Kaj je struct?

Structi so eni izmed osnovnih podatkovnih tipov v C#. S structi je imel opravka vsak, ki je programiral v jeziku C#, saj se osnovni podatkovni tipi (npr. System.Int32, System.Boolean, System.Char, System.Double) načeloma štejejo med structe.

Torej kaj je struct? Struct je podatkovni tip, ki je zelo podoben razredom. V takšni obliki kot jih bom predstavil obstajajo samo v programskem jeziku C# (čeprav je Java zelo podoben programski jezik, imajo structi v Javi popolnoma drugačen pomen). Torej struct je podatkovni tip, v katerem so zapisane lastnosti objekta. V njem pa so lahko zapisane tudi metode, s katerimi upravljamo z objektom. Vse skupaj pa namiguje, da imamo spet opravka z razredi.

Prvi struct

Structi so zelo preprosti za uporabo, saj si jih lahko predstavljamo kok nekakšen preprostejši nadomestek za razrede. Čeprav se razredi in structi razlikujejo v tipu podatkov, pa se ustvarjanje structov ne razlikuje veliko od ustvarjanja razredov.

  • struct (opis točke)
        public class classTocka
        {
            private int _xKoord;
            private int _yKoord;

            public int XKoord
            {
                get { return _xKoord; }
                set { _xKoord = value; }
            }
            public int YKoord
            {
                get { return _yKoord; }
                set { _yKoord = value; }
            }
            public override string ToString()
            {
                return XKoord+"-"+YKoord;
            }
        }
     
  • razred (opis točke)
    public struct structTocka
        {
            private int _xKoord;
            private int _yKoord;

            public int XKoord
            {
                get { return _xKoord; }
                set { _xKoord = value; }
            }
            public int YKoord
            {
                get { return _yKoord; }
                set { _yKoord = value; }
            }
            public override string ToString()
            {
                return XKoord+"-"+YKoord;
            }
        }

Ustvarjanje structa in razreda (Film).

Klik

Prvi struct

Prvi struct - 2

Razlik pri ustvarjanju structa in razreda v tem primeru ni. Vendar kljub vsemu moramo paziti na dve ključni razliki:

  • Lastnosti structa ne smejo imeti privzetih vrednosti!
  • V strutih imamo tudi konstruktorje, vendar jih ne smemo definirati brez parametrov!
(napake.jpg)
description of the image

Upravljanje s pomnilnikom

Za boljše razumevanje naslednjih razdelkov si najprej poglejmo kako se v pomnilnik shranijo različni podatki v programu. Za pravilno delovanje programa .NET orodje uporablja dve različni mesti v pomnilniku. To sta kopica (angl. heap) in sklad (angl. stack). V pomnilniku pa najdemo tudi kazalce (angl. pointers), ki nam kažejo na naslov objekta, ki ga potrebujemo.

(stack_heap.jpg)
-Prikaz- sklada in kopice

Sklad (angl. stack)

Sklad je programska struktura, ki deluje po načelu LIFO (Last in first out). Sklad si lahko predstavljamo kot več škatel, ki jih polagamo eno na drugo. Škatlo dodamo vedno, ko pokličemo novo metodo. Uporabljamo pa lahko samo škatlo, ki se nahaja na vrhu (torej vrh sklada). Ko se metoda zaključi in zadnje škatle ne potrebujemo več, jo preprosto odstranimo in skočimo v naslednjo škatlo, ki bo na vrhu. Vrh sklada nam lahko pomaga pri iskanju mesta, kje v kodi se trenutno nahajamo.

(stack_que.jpg)
Način delovanja sklada

Kopica (angl. heap)

Kopica služi samo kot prostor za zapis podatkov. Ker pri kopici vrstni red zapisa podatkov ni pomemben, lahko do vseh podatkov vedno dostopamo (torej nismo omejeni samo na zadnje podatke). Torej si lahko kopico predstavljamo kot različne predmete razporejene na mizi. Ko enega izmed njih potrebujemo, ga preprosto vzamemo.

Čiščenje podatkov

Ker pomnilnika ni neomejeno, moramo tudi poskrbeti za čiščenje pomnilnika. Sklada ni potrebno čistiti, saj se podatki odstranijo, ko jih ne potrebujemo več. Tukaj pa se zadeva za kopico zakomplicira, saj podatki vedno ostanejo v pomnilniku. Zato za optimalno delovanje programov potrebujemo Zbiralca smeti (angl. Garbage collector - GC). Zbiralec smeti se izvede, ko se pomnilnik zapolni s podatki. Zbiralca smeti lahko izvedemo tudi sami na naključnem mestu v kodi z ukazom GC.Collect().

Kdaj uporabimo GC.Collect()? Preprost odgovor je nikoli. Saj s tem ukazom prisilimo program v dodatne procese čiščenja pomnilnika, ki pa v večini primerov niso bili potrebni.

Zbiralec smeti / Generacije v kopici

Zbiralec smeti upravlja z dodelitvijo in sproščanjem pomnilnika za vsako .NET aplikacijo. Vsi objekti, s katerimi zbiralec lahko upravlja, se nahajajo na določenem razdelku pomnilnika (naslovi si sledijo / so povezani). Objekti se v pomnilnik shranijo v istem vrstnem redu, kot smo jih ustvarili.

  • GC generacija (angl. GC generation) Vsi objekti v kopici so razdeljeni v 3 skupine oz. generacije. To so: Generacija 0 (angl. Generation 0), Generacije 1 (angl. Generation 1) in Generacija (angl.Generation 2).

    • Generacija 0 vsebuje vse novo ustvarjene objekte. Vsi objekti, ki jih vsebuje naj bi obstajali samo začasno. Zato zbiralec smeti največkrat počisti to generacijo.
    • Generacija 1 vsebuje vse objekte, ki so večkrat "preživeli" zbiralca smeti. V tej generaciji se zbiralec smeti ne izvede tako pogosto, kot v generaciji 0.
    • Generacija 2 pa vsebuje objekte, ki so večkrat "preživeli" zbiralca smeti v generaciji 1.

      (IC112504.gif)
      Kopica

Čiščenje kopice

Zbiralec smeti lahko deluje na dva načina:

  • Zbiranje v celoti - zbiralec smeti potuje po celotni kopici in poišče vse objekte, ki jih lahko odstrani.
  • Delno zbiranje - tukaj pa se zbiralec smeti osredotoči samo na določeno generacijo v kopici.

Postopek zbiranja smeti:

  • Program se najprej zaustavi.
  • Zbiralec poišče korenske objekte (angl. root objects). Korenski objekti so tisti objekti iz katerih izhajajo skoraj vsi objekti. Posebnost teh objektov je v tem, da noben drug objekt nima referenc na korenski objekt.
  • Zbiralec nato potuje po vseh referencah, ki potujejo izven korenskih objektov. V primeru delnega zbiranja, pa se zbiralec omeji še na določeno generacijo.
  • Zbiralec obdrži vse objekte, ki izhajajo iz korenskih objektov. Preostale lahko odstrani in s tem sprosti pomnilnik.

Zbiralec smeti torej odstrani vse objekte, do katerih ne moremo dostopati.

Kam se podatki shranijo?

Najprej si poglejmo dva osnovna tipa podatkov. Prvi je referenčni tip. Za referenčne tipe podatkov velja, da je v spremenljivki shranjen naslov, ki kaže na določen objekt. Drugi tip podatkov so podatkovni tipi, pri katerih so podatki shranjeni kar v spremenljivkah.

Shranjevanje

Za referenčne tipe podatkov je preprosto. Vse shranimo v kopico. Za podatkovne tipe pa se stvari malo zakomplicirajo, saj ni točno določeno kam se morajo shraniti. Odvisno je od tega kje so deklarirani. Če podatkovni tip deklariramo v razredu, se bo skupaj z objektom shranil v kopici. V primeru da ga uporabimo v metodi, pa se shrani v sklad.

Razlika med razredi in structi

Čeprav sta si strukturi zelo podobni pa lahko med njimi najdemo veliko razlik.

  • Ker je v spremenljivki, ki predstavlja objekt določenega razreda, zapisan naslov objekta, ima spremenljivka lahko vrednost null. Pri structih pa to ni mogoče.
  • Structi so podatkovni tip. Objekti razredov pa referenčni tip.
  • Ker se structi in razredi razlikujejo v tipu podatkov, se tudi shranijo na različna mesta v pomnilniku. Structi se lahko shranijo na obe mesti, saj so podatkovni tip in njihovo mesto bo določeno od tega, kje smo struct definirali. Objekti razredov pa se shranijo v kopico, saj spadajo med referenčne tipe podatkov.
  • Čeprav v structu ne smemo definirati praznega konstruktorja, pa je vedno na voljo za uporabo. V razredih pa prazen konstruktor "izgine" za uporabo, če definiramo kakšnega s parametri. Primer
  • Verjetno najpomembnejša razlika med razredi in structi pa je v tem, da pri ustvarjanju structov ne moremo uporabiti dedovanja. To pa je tudi razlog, zakaj se večkrat izognemo uporabi structov.

Primer

  • V že obstoječem structu in razredu definiramo konstruktor s parametri (v našem primeru x in y koordinata)

    (konstruktor.png)
    Definicija konstruktorja

*Opazimo: Do edine napake pride, ko želimo ustvariti nov objekt razreda samo s praznim konstruktorjem!

(konstruktor_test.png)
Napaka v prevajalniku

Razlike pri uporabi.

Nekaj zanimivosti pri uporabi structov in razredov.

  • Kopiranje structa.

    • Prvi poskus.
    • Struct je bil pravilno kopiran. Objekt razreda pa se je podvojil. Težava je v tem, ker so objekti razredov referenčni tip podatkov in se s tem drugi spremenljivki dodeli naslov prve spremenljivke (s spreminjanjem lastnosti druge spremenljivke bomo spremenili tudi lastnosti prve spremenljivke, saj obe kažeta na isti naslov - isti objekt!). Struct pa samodejno ustvari kopijo prvega structa, saj je podatkovni tip podatkov.
  • Problem: Imamo struct in objekt razreda, ki opisujejeta koordinati točk. S ustreznimi metodami želimo spremeniti koordinati točk.

    • Prvi poskus.
    • Zakaj se struct ne spremeni? Ko se metoda izvede, metoda ustvari novo kopijo našega structa in ji dodeli nove koordinate.
    • Kako torej spremeniti metodo za struct, da nam bo vrnila nekaj podobnega kot pri razredu?
    • Rešitev.

Structi kot parameter

Spremenjena metoda

  • Metodo moramo najprej spremeniti tako, da nam vrne nov struct.
  • Napišemo ustrezen return stavek.
  • Pri klicu metode moramo zamenjati staro vrednost z novo vrednostjo.
  • Rešitev (koda):

    (slika_spremenjena.png)
    Spremenjena metoda
  • Rešitev (izpis):

    (spremenjena_konzola.png)
    Izpis

Kopiranje (koda)

(kopija_koda.png)
Prvi poskus kopiranja
  • Ali je koda pravilna? Izpis

Kopiranje (izpis)

(kopija_izpis.png)
Prvi poskus kopiranja

Kdaj uporabiti struct?

Pri uporabi podatkovnega tipa se zmanjša velikost kopice. S tem Odstranjevalec smeti ne bo imel tako pogostih ciklov (manj dodatnih procesov pri delovanju programa). Toda to še ne pomeni, da je uporaba podatkovnih tipov vedno boljša (shranjevanje podatkov v sklad). Saj premikanje velikega podatkovnega tipa seveda potrebuje več časa kot pa premikanje reference na naslov določenega objekta.

Torej kdaj je bolje uporabiti struct namesto razreda?

  • Objekt bo načeloma preprost. Vseboval bo malo podatkov.
  • Objekt uporabimo samo v zanki in ga nato ne potrebujemo več.
  • Objekt ne potrebujemo po celotni kodi.
  • Objekt ne bo izhajal iz že obstoječega objekta.
  • Uporaba boxinga in unboxinga.

Pakiranje in odpakiranje

Pakiranje je eden izmed najpomembnejših pojmov v C#. Z njim lahko povežemo podatkovne in referenčne tipe, tako da spremenimo podatkovne tipe spremenimo v referenčne in obratno. Torej naj omogoča obravnavo podatkovnega tipa kot objektni tip. Pakiranje pa lahko tudi uporabimo, ko želimo povezati več različnih objektov kot enega. Ta način programiranja obstaja, ker v .NET okolju tudi vsak podatkovni tip deduje iz System.Object.

testiranje pakiranja:

V zanki sem uporabil var, s katerim nam ni potrebno že vnaprej definirati tip spremenljivke, saj ga prevajalec sam določi (v tem primeru bi lahko var zamenjal tudi z object).

static void Main(string[] args)
        {
            int stevilo = 1;
            string beseda = "ena";
            char znak = 'e';
            testcl novObj = new testcl(); //naključen objekt, ToString vrne niz "objekt"
            object[] sezObjektov = {(object) stevilo,(object)beseda,(object)znak,(object)novObj}; && objekte zapakiramo
            Console.WriteLine("Izpiši podatkovne tipe objektov: ");
            foreach (var obj in sezObjektov) //ker so objekti pakirani lahko uporabimo samo eno zanko
            {
                Console.WriteLine("Tip objekta: " + obj.GetType() + ". Vrednost: " + obj.ToString());
            }
            Console.ReadLine();
        }

rezultat:

(testBox.png)
Delovanje testnega programa

Viri

0%
0%