Slovo na úvod

Accendo > Publikované > OOP 1

Autor:                  Libor Bešenyi

Dátum:                2011/01/02

Zmena:                                2011/01/02

 

                Už niekoľko rokov som sa chystal napísať seriál o OOP, tak hádam už teraz sa mi to podarí. Problém s OOP je ten, že ono je to strašne jednoduché... ak tomu rozmieme. Z vlastnej skúsenosti viem, že OOP som sa nenaučil z kníh a ani mi to nebol nikto poriadne schopný vysvetliť... asi si musí tým prejsť každý. Z tohto pohľadu je teda otázne, či ma zmysel písať ďalší seriál pre tých čo nerozumejú – nie, necítim sa kompetentnejší ako tí predomnou, ale pokúsiť sa o to môžem.

                V tomto seriáli budú príklady napísané v C# (java bude pravdepodobne obdobná), no všetko platí až na drobné výnimky aj v Delphi (Pascal). Požiadavky na čitateľa nie sú náročné, no nechcem sa v seriáli venovať základným veciam.

                Seriál prosím neberte ako akademickú pomôcku, s týmto materiálom určite neuspejete na skúškach, no hádam Vám pomôže porozumieť tomu, o čom pojdenávajú na to určené skriptá – týmto sa chcem ospravedlniť dopredu všetkým teoretikom za nedokonalé konvencie či terminológiu.

                Taktiež by som sa chcel pozrieť na OOP z mojich skúseností v praxi. Veľa ľudí pochopí časť OOP a potom vznikajú chybné implementácie – na tie by som chcel poukázať tiež.

Trieda a objekt

                V prvom rade musíme rozumieť tomu, čo je to trieda a čo je to objekt. Trieda je vlastne niečo ako „forma“ objektu. Ak chceme robiť koláčiky tak potrebujeme len jednú formičku, ale z nej vieme vytvoriť viacej koláčov. V tomto ponímaní je trieda forma a koláčik objekt.

                Vždy keď riešim komplikovanejší problém, snažím sa ho nasimolovať na jednoduchej aplikácii. Zvyknem si robiť WinForms applikáciu s jedným tlačidlom a logiku programujem na udalosť OnClick. Ako si to budete riešiť vy, nechám na Vás.

                Tak, pozrime sa na ukážku triedy:

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

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

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

    public class Trieda

    {

    } // public class Trieda

 

                Ako z tejto triedy (na oko prázdnej) vytvoríme objekt? Jednoducho:

Trieda trieda = new Trieda();

 

Takže Trieda (veľké „T“) je trieda a objekt je trieda (malé „t“). Prečo je objekt koláčikom? Pretože jednú triedu vieme vytvoriť X-krát (ako formu):

            Trieda trieda = new Trieda();

            Trieda trieda2 = new Trieda();

            Trieda triedaMoja = new Trieda();

 

 

                Ako si môžeme všimnúť, mení sa názov objektu, ale „predpis“ (forma) ostáva tá istá. Ako teda vieme že tieto objekty sú odlišné, ak použivajú rovnakú triedu? Ukážeme si to hneď. Najprv trocha teórie. Ak vytvárame koláčiky, formička nám poslúži len na vytvorenie – pri životnom cykle koláčika už nehrá rolu (keď ho zjeme, nezjeme pritom aj formičku; keď sa poláme, nepoláme sa formička). Takto treba pristupovať ku triedam a objektom.

Objekt je niečo reálne s čím vieme narábať a formička je len akýsi „predpis“ ktorý so samotným objektom veľa spoločného nemá (len sa vďaka nej zadefinovali určité atribúty – ako tvar koláčika). Každý jeden koláč teda vieme prisposobovať nezávisle (napríklad na každú hviezdičku medovníka vieme nakresliť niečo iné – formička hviezdičky je trieda, každá jedna hviezdička je tzv. inštancia formičky – teda objekt). To že trieda nie je funkčná (pomáha len pri definícii) toto voláme že je statická.

Pridajme teda do triedy niečo, na čo budeme môcť „maľovať“ (nejakú plochu):

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

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

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

    public class Trieda

    {

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

        public string Text;

    } // public class Trieda

 

Aby sme predviedli ako sú nezávislé šablóny (formičky) / triedy od objektov, pre každú inštanciu vytvoríme špeciálny text:

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda = new Trieda();

            trieda.Text = "Text A";

 

            Trieda trieda2 = new Trieda();

            trieda2.Text = "Text 2";

 

            Trieda triedaMoja = new Trieda();

            triedaMoja.Text = "Text III.";

 

            MessageBox.Show(trieda.Text);

            MessageBox.Show(trieda2.Text);

            MessageBox.Show(triedaMoja.Text);

        }

 

Ako môžeme vydieť, trieda s veľkým T sa nemení. Ak však vytvoríme N inštancií, tie si držia svoje dáta. Samotná trieda vie, že môže obsahovať reťazec „Text“ – no obsah už neriedi. Ten riadi inštancia danej triedy. Každá inštancia teda má premennú „Text“, ale obsah premennej už je v réžií každého obejku!

Smerníková aritmetika

                Trieda teda predpisuje ako má objekt vyzerať – prečo je to tak dôležité sa dozvieme neskôr. Teraz ale musíme porozumieť pojmu smerník. Smerník je vlastne objekt (koláčik). V pamäti to je len adresa, kde sa nachádzajú dáta triedy, ktoré dokážeme rekonštruovať.

                To, že existuje trieda ako statická forma a objekt ako „žívá“ inštancia znamená, že v pamäti sa nachádzajú dáta daného objektu vo forme ktorú predpisuje trieda. Môžeme si to predstaviť tak, že napr. Ak máme triedu s dvoma reťazcami a reťazec má (napríklad) 5 znakov, tak pamäť zaberá 2x5 znakov. Každý objekt je vlastne len číslo adresy, kde sa nachádza trieda v pamäti a samozrejme definuje aj triedu, aby sme vedeli ako ku tejto pamäti máme pristupovať (kde sa končí objekt, kde sú jednotlivé dáta objektu apod.)

0

1

2

3

4

5

6

8

8

9

10

11

12

13

14

15

16

17

18

19

$

a

 

A

T

e

x

t

 

T

e

x

t

2

 

-

 

@

T

E

 

                Ak by teda zjednodušene existovali hore spomínané pravidlá, objekt by bola len adresa, ktorá by odkazovala na číslo 4. Ak by trieda vyzerala takto:

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

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

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

    public class Trieda

    {

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

        public string Text1;

        public string Text2;

    } // public class Trieda

 

Tak potom by inštancia danej triedy bola toto (je to veľmi zjenodušený pohľad):

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda = new Trieda();

            trieda.Text1 = "Text";

            trieda.Text1 = "Text2";

        }

 

Ako môžeme vydieť, samotný fakt, ako sa volá premenná nehrá rolu. Program (prekladač) vie, že má dva typy (reťazec) a keď predpokladáme, že reťazec môže mať max. 5 znakov, tak vyhradil v pamäti patričný priestor (všmnime si adresu 8 – je prázdna napriek tomu, že nie je definovaná ako hodnota).

Inštancia (objekt) v tomto prípade je len adresa na dáta v pamäti, ktoré majú byť „tlmočené“ podľa určitej šablóny (triedy). Ak teda smerník odkazuje na pamäťovú adresu 4 a viem, že sa jedná o inštanciu triedy Trieda, vieme, že máme „hľadať“ dva reťazce a keďže tie majú po 5 znakov, tak trieda je alokovaná v pamäti od adresy 4..13.

Toto je dôležité si uvedomiť, pretože práve v tomto bode väčšina ľudí robí chyby (napriek tomu, že je to v podstate základným kameňom v porozumení OOP). Takže o čo vlastne ide? Rozoberme si prvú časť syntaxe vytvorenia objektu z triedy:

            Trieda trieda = ...

 

Čo toto znamamená? Treba vytvoriť inštanciu pomenovanú „trieda“ z triedy „Trieda“. Teda vieme, že ak bude vyzerať ako v poslednom príklade (dva reťazce Text1 a Text2) takto a reťazec má 5 znakov, musíme alokovať 10 bitov v pamäti.

Pravá strana určuje to, či vytovríme novú inštanciu (alokujeme priestor a pridelíme premennej novú adresu) alebo ju necháme prázdnu (Null, nil), alebo ku nej alokujeme existujúcu adresu. Ak máme v programe dve inštancie vytvorené takto:

            Trieda triedaA = new Trieda();

            triedaA.Text1 = "Text";

            triedaA.Text2 = "Text2";

 

            Trieda triedaB = new Trieda();

            triedaB.Text1 = "bla";

            triedaB.Text2 = "bla";

 

Pamäť vyzerá takto:

0

1

2

3

4

5

6

8

8

9

10

11

12

13

14

15

16

17

18

19

T

e

x

t

 

T

e

x

t

2

b

l

a

 

 

B

l

a

 

 

 

Pričom tiredaA ukazuje na adresu 0 a triedaB ukazuje na adresu 10. Teda samotná premenná je vlastne len adresou (smerník) a ten ukazuje na určité miesto v pamäti. Nič viac, nič menej. Vieme ako sa vynucuje priradenie hodnoty v C#:

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda triedaA = new Trieda();

            triedaA.Text1 = "Text";

            triedaA.Text1 = "Text2";

 

            Trieda triedaB = new Trieda();

            triedaB.Text1 = "bla";

            triedaB.Text1 = "bla";

 

            triedaB = triedaA;

            MessageBox.Show(triedaB.Text1);

        }

 

                Toto spôsobi, že aj objekt tiredaB začne ukazovať na tú istú adresu v pamäti. Teraz pozor: To, že sme priradili jednu premennú inej, sme nepreradili jej obsajh, ale je adresu!!! Čo je rozdiel ako pri ordinálnych typoch:

            string nieco = "Nieco";

            string niecoIne = "Ine";

 

            niecoIne = nieco;

            niecoIne += "Text";

 

            MessageBox.Show(nieco);

            MessageBox.Show(niecoIne);

 

                Pri ordinálnych typoch sa „prenáša“ hodnota. Tu je však hodnotou adresa a teda ak dva smerníky odkazujú na rovnakú pamäť, robia operácie nad rovnakým objektom!!!

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda triedaA = new Trieda();

            triedaA.Text1 = "TextA";

            MessageBox.Show(triedaA.Text1);

 

            Trieda triedaB = new Trieda();

            triedaB.Text1 = "TextC";

            MessageBox.Show(triedaB.Text1);

 

            triedaB = triedaA;

            MessageBox.Show(triedaB.Text1);

 

            triedaB.Text1 = "BLA";

            MessageBox.Show(triedaB.Text1);

            MessageBox.Show(triedaA.Text1);

        }

 

                Tu by som chcel podotknúť, že v hornom príklade sme už na dobro stratili referenciu na pamäť druhého objektu, oba už odkazujú na to isté miesto (pamäť nie je sekvenčná, môže byť priradzovaná náhodne – ku týmto veciam sa vrátime neskôr keď si ukážeme tzv. pamätové leaky).

                Pri smerníkovej aritmetike sa chcem ešte zdržať, musíme mať v tomto jasno! Ako je to s hodnotami predávanými v ordinálnych typoch vieme. Pozrime sa na pomocnú metódu:

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

        private void button1_Click(object sender, EventArgs e)

        {

            string text = "Hello World";           

            MessageBox.Show("button1_Click(): " + text);

 

            PridajJednotku(text);

 

            MessageBox.Show("button1_Click(): " + text);

        }

 

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

        public void PridajJednotku(string retazec)

        {

            retazec += "_1";

 

            MessageBox.Show("PridajJednotku(): " + retazec);

        }

 

                Výstup bude jasný:

button1_Click(): Hello World

PridajJednotku(): Hello World_1

button1_Click(): Hello World

 

                Prečo tomu tak je? Pretože v tomto prípade predávame do metódy hodnotu. Ak by sme ju chceli v metóde zmeniť, museli by sme ju poslať nie ako hodnotu, ale ako smerník (áno, aj ordinálne typy musia odkazovať na miesta v pamäti – výhodu tu majú ľudia, ktorí sa začali učiť programovať napr. v jazyku C). V C# je určite každému zrejmé ako sa to dá dosiahnúť (pridáme REF):

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

        private void button1_Click(object sender, EventArgs e)

        {

            string text = "Hello World";           

            MessageBox.Show("button1_Click(): " + text);

 

            PridajJednotku(ref text);

 

            MessageBox.Show("button1_Click(): " + text);

        }

 

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

        public void PridajJednotku(ref string retazec)

        {

            retazec += "_1";

 

            MessageBox.Show("PridajJednotku(): " + retazec);

        }

 

Výstupom teda bude:

button1_Click(): Hello World

PridajJednotku(): Hello World_1

button1_Click(): Hello World_1

 

                Ako to je pri objektoch sme si povedali. Objekty sú implicitne predávané referenciou (smerníkom) a teda vždy sa predáva len adresa. Takže použitie „ref“ v takomto prípade je v podstate hlokým nedorozumením (áno, videl som už aj takéto zdrojáky!). Takže úplne rovnaký výstup dostaneme aj v tomto príklade s triedami:

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

        private void button1_Click(object sender, EventArgs e)

        {

            Trieda trieda = new Trieda();

            trieda.Text1 = "Hello World";

 

            MessageBox.Show("button1_Click(): " + trieda.Text1);

 

            PridajJednotku(trieda);

 

            MessageBox.Show("button1_Click(): " + trieda.Text1);

        }

 

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

        public void PridajJednotku(Trieda ubovolnaInstanciaTriedy)

        {

            ubovolnaInstanciaTriedy.Text1 += "_1";

 

            MessageBox.Show("PridajJednotku(): " + ubovolnaInstanciaTriedy.Text1);

        }

 

                A prečo to spomínam? Pretože REF by v OOP znamenal to, že vieme zmeniť v metóde jej adresu! Preto použitie ref-u tam kde nemá byť môže dosť zneprehliadniť zdrojový kód (keďže programátor môže očakávať „invazívnejšiu“ zmenu).

                Céčkari opäť raz majú výhodu. Ak samotný objekt je smerník, čo je potom referencia na smerník? Bežne sa to nazýva smerník na smerník: aj adresa (smerník) musí byť niekde v pamäti umiestnená (ako napr. číslo). Referencia na smerník je vlastne odkaz na toto miesto a je to dosť nebezpečné (ale niekedy potrebné) – toto si preberieme v druhej kapitole.