NULL hodnota a smerník na smerník

Accendo > Publikované > OOP 2

Autor:                  Libor Bešenyi

Dátum:                2011/01/02

Zmena:                                2011/01/02

 

                V predchádzajúcej kapitole sme načŕtli komplikovanejší spôsob práce so smerníkami. Pre úplnosť si musíme povedať, že aj samotná adresa smerníka sa musí niekde v pamäti nachádzať. Kvôli zjednodušeniu opäť raz budeme vysvetľovať réžiu pamäti trocha skreslene a budeme predpokladať, že reťazec ma fixnú dĺžku 5.

                V predchádzajúcej časti sme si ukázali, ako môžu vyzerať dáta proramu uložené v prgrame. Aby ale program vedel kde začínajú dáta jednej triedy a končia inej, potrebuje ešte jednú „tabuľku“ v ktorej bude držať tieto adresy. Tak majme triedy s jedným reťazcom:

    // *************************************************************************

    // *** Trieda **************************************************************

    // *************************************************************************

    public class Trieda

    {

        // ---------------------------------------------------------------------

        public string Text;

    } // public class Trieda

 

                A vytvorme tri inštancie:

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda1 = new Trieda();

            trieda1.Text = "A";

 

            Trieda trieda2 = new Trieda();

            trieda1.Text = "B";

 

            Trieda trieda3 = new Trieda();

            trieda1.Text = "C";

        }

 

                V pamäti to v našom prípade vyzerá takto:

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

A

 

 

 

 

B

 

 

 

 

C

 

 

 

 

 

                Program si ale vytvorí ešte pomocnú tabuľku, kde ku jednotlivým smerníkom priradí aj adresy (samozrejme názvoslovie nie je rovnaké ako v programovacom jazyku – lepšiu predstavu hádam dá preštudovanie assembleru alebo nejaký Úvod do prekladačov – toto ale nie je vôbec potrebné na zvládnutie OOP).

trieda1

0

trieda2

5

trieda3

10

(vyhradené)

 

(vyhradené)

 

 

                Takto by mohla vyzerať tabuľka smerníkov. Ak by sme to chceli usporiadať sekvenčne do pamäti, môžeme si to predstaviť tak, že určitý priestor je vyhradený pre smerníky a potom je vyhradený priestor pre dáta:

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

5

10

15

-

-

A

 

 

 

 

B

 

 

 

 

C

 

 

 

 

 

                Ako si môžeme všimnúť v prvej časti máme vyhradené miesto na smerníky, pričom obsah tohto miesta v pamäti je adresa na iné miesto v pamäti. Aby sme teda naviazali na predchádzajúcu kapitolu (pojdenávanie o použitia referencii v metóde) pozrime sa najprv na neexistujúce objekty. Čo sa stane ak nevytvorím inštanciu?

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda1;

            UrobNieco(trieda1);

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(Trieda trieda)

        {

        }

 

                V tomto prípade kompilátor nedovolí kód preložiť (Delphi by dovolilo):

Use of unassigned local variable 'trieda1'

Toto však nie je jediný prípad kedy objekt nie je vytvorený – všimnime si, že kompilátor dokáže túto chybu odhaliť len v mieste, kde vzniká premenná. V run-time (spustený program) však kompilátor nemôže odhaľovať konflikty vznikané programom a tieto stavy musí ošetriť programátor. Aby sme si ukázali tieto chyby tak nevytvorme novú inštanciu, ale zadefinujme objekt ako prázdny (NULL / nil):

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda1 = null;

            UrobNieco(trieda1);

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(Trieda trieda)

        {

        }

 

Teraz sme vlastne urobili program preložiteľný (design-time čo má pod kontrolou prekladač). Run-time je už na programátorovi. V tomto prípade aj keď inštancia je prázdna, v run-time sa nič nestane, pretože objekt sa nikde nepožíva. Naša pamäť môže vyzerať nejako takto:

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

 

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

-

...

 

Program zaregistroval nový smerník, ale nevyhradil žiadne miesto pre objekt, lebo neexistuje. Ak by sme ho chceli použiť, program zdochne (pri prvom použití v run-time). Upravme teda metódu tak, aby zobrazovala obsah premennej Text danej triedy:

Ako vidíme, program ide preložiť, aj keď tam je zjavne logická chyba. Toto nie je chyba prekladača, ale programátora (pretože niekedy podobný stav je žiadúci). Čo sa teda stane keď spustím tlačidlo? Dostaneme výnimku (chybu) programu:

Object reference not set to an instance of an object.

                Táto chyba pojednáva o tom, že sa snažíme pristupovať do dátovej časti objektu, ktorý ale neexistuje. Pozrime sa ešte raz na tabuľku v pamäti ako vyzerala keby objekt existoval:

            Trieda trieda1 = new Trieda();

            trieda1.Text = "bla";

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

5

-

-

-

-

b

l

a

 

 

-

-

-

-

-

-

-

-

-

...

 

                Dátova časť je teda tá pravá. Ak chceme zobraziť text z datovej oblasti, v zdrojovom kóde je to vlastne „pravá“ časť za objektom:

trieda.Text

 

                V podstate až na tomto mieste vykonávania kódu program zdochol. Samotné volanie smerníka nie (pretože on v ľavej časti existuje – o to sa už vlastne postaral kompilátor). Ak by sme teda vykonali operáciu nad smerníkom samotným v prípade, že neexistuje, nič sa nedeje, kým sa neznážíme pristupovať ku jeho dátam (oblasti na ktorú by mal odkazovať):

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda1 = null;

            UrobNieco(trieda1);

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(Trieda trieda)

        {

            trieda = trieda;

        }

 

                V tomto prípade chyba nenastane, pretože pracujeme len so smerníkom a on existuje v tabuľke referencií (len má neplatnú hodnotu) a tak sa nič nedeje. Preto vieme ošetriť našú procedúru o to, aby sme zistili, či objekt je vytvorený,ň. Ak nebude, tak nič neurobíme. Na zistenie či objekt existuje vieme porovnať hodnotu smerníka s NULL hodnotou:

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda1 = null;

            UrobNieco(trieda1); // Neurobi nic

 

            Trieda trieda2 = new Trieda();

            trieda2.Text = "bob";

            UrobNieco(trieda2); // Zobrazi text

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(Trieda trieda)

        {

            if (trieda != null)

                MessageBox.Show(trieda.Text);

        }

 

                V predchádzajúcej kapitole sme si ukázali, že ak zmeníme dáta v objekte vyslanom cez smerník, po návrate sa zmenia, pretože smerník posiela do procedúri adresu. Čo sa ale stane ak zmenimíme smerník?

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda = new Trieda();

            trieda.Text = "bob";

           

            UrobNieco(trieda);

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(Trieda trieda)

        {

            MessageBox.Show(trieda.Text);

           

            trieda = null;

 

            MessageBox.Show(trieda.Text);   // ZDOCHNE

        }

 

                Ak si predstavíme smerník ako číslo poslané do metódy tak na prvom riadku ešte stále ukazuje na to isté miesto, kde objekt existuje (preto sa hláška zobrazí). Keď mu však zmeníme adresu tak už program zdochne (to sme si ukázali prečo). Ako to v pamäti teda vyzerá:

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

5

5

-

-

-

b

l

a

 

 

-

-

-

-

-

-

-

-

-

...

 

                Vznikol dočasný nový smernik (smerníkova tabuľa vľavo), ktorý ukazuje na to isté miesto v pamäti (preto vieme meniť dáta, lebo smerník ukazuje na spoločnú adresu v pamäti ako ten „pred“ nim), kde sa nachádza vytvorený objekt. V druhom kroku dočasnému smerníku priradíme NULL hodnotu a tak sa zmení tabuľka takto:

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

5

 

-

-

-

b

l

a

 

 

-

-

-

-

-

-

-

-

-

...

 

                Smerník má neplatnú hodnotu – nič to ale nemení na tom, že objekt stále existuje! Ak program upravíme tak, aby ignoroval zobrazovanie po priradení NULL hodnoty, toto bude fungovať:

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda = new Trieda();

            trieda.Text = "bob";

           

            UrobNieco(trieda);

 

            MessageBox.Show(trieda.Text);   // Opat funguje

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(Trieda trieda)

        {

            MessageBox.Show(trieda.Text);

           

            trieda = null;

        }

 

                Prečo to funguje? Pretože keď sa skončila procedúra UrobNieco() a my sme sa vrátili späť, pôvodný smerník (z adresy 0) stále odkazuje na adresu objektu v dátovej časti (5)! Čo ale spôsobí referencia v tomto prípade? Teda smerník na smerník? Že do metódy nedôjde adresa objektu ale adresa smerníka! Teda číslo z ľavej strany:

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda = new Trieda();

            trieda.Text = "bob";

           

            UrobNieco(ref trieda);

 

            MessageBox.Show(trieda.Text);   // ZDOCHNE

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(ref Trieda trieda)

        {

            MessageBox.Show(trieda.Text);

            

            trieda = null;

        }

 

                Čo sa teda stalo v pamäti? V metóde sa nevytvoril nový smerník, ale použil sa pôvodný:

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

5

-

-

-

-

b

l

a

 

 

-

-

-

-

-

-

-

-

-

...

 

                Ak sme jemu priradili NULL hodnotu, aj po návrate z metódy pôvodný smerník odkazuje na neplatnú hodnotu (môžeme si všimnúť, že objekt v dátovej časti existuje, ale my sme stratili informáciu o tom – viac si povieme pri pamäťových leakoch a garbage collectoroch neskôr).

 

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

...

 

-

-

-

-

b

l

a

 

 

-

-

-

-

-

-

-

-

-

...

 

                Teda použitie ref má trocha iné opodstatnenie v smerníkovej aritmetike a ak pracujeme v tíme, musíme písať programy tak, aby zbytočne nezavádzali (v prípade že niekto iný chce upraviť metódu a je tam ref, musí refaktorovať kód, či sa smerník nemení a ak je to irelevantné, je to zbytočná strata času).

                Príklad na záver, kedy by malo význam REF použiť:

        // ---------------------------------------------------------------------

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda = null;

            UrobNieco(ref trieda);

            MessageBox.Show(trieda.Text);

 

            Trieda trieda2 = new Trieda();

            trieda2.Text = "BMW";

            UrobNieco(ref trieda2);

            MessageBox.Show(trieda2.Text);

        }

 

        // ---------------------------------------------------------------------

        public void UrobNieco(ref Trieda trieda)

        {

            if (trieda == null)

            {

                trieda = new Trieda();

                trieda.Text = "Auto";

            } // if

 

            MessageBox.Show(trieda.Text);

        }