IT rekvalifikace s garancí práce. Seniorní programátoři vydělávají až 160 000 Kč/měsíc a rekvalifikace je prvním krokem. Zjisti, jak na to!
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

Lekce 4 - Referenční a hodnotové datové typy

V předešlém cvičení, Řešené úlohy k 3. lekci OOP v C# .NET, jsme si procvičili nabyté zkušenosti z předchozích lekcí.

Začínáme pracovat s objekty a objekty jsou referenčními datovými typy, které se v některých ohledech chovají jinak, než typy hodnotové (např. int). Je důležité, abychom přesně věděli, co se uvnitř programu děje, jinak by nás v budoucnu mohlo leccos překvapit.

Zopakujme si pro jistotu ještě jednou, co jsou to hodnotové typy. Obecně jsou to jednoduché struktury, např. jedno číslo, jeden znak. Většinou se chce, abychom s nimi pracovali co nejrychleji, v programu se jich vyskytuje velmi mnoho a zabírají málo místa. V anglické literatuře jsou často popisovány slovy light-weight. Mají pevnou velikost. Příkladem jsou např. int, float, double, char, bool a další.

Aplikace (resp. její vlákno) má operačním systémem přidělenou paměť v podobě tzv. zásobníku (stack). Jedná se o velmi rychlou paměť s přímým přístupem, její velikost aplikace nemůže ovlivnit, prostředky jsou přidělovány operačním systémem. Tato malá a rychlá paměť je využívána k ukládání lokálních proměnných hodnotového typu (až na výjimky při iteracích, kterými se nebudeme zabývat). Proměnnou si v ní můžeme představit asi takto:

Zásobník vb paměti počítače - Objektově orientované programování v C# .NET

Na obrázku je znázorněna paměť, kterou může naše aplikace využívat. V aplikaci jsme si vytvořili proměnnou a typu int. Její hodnota je 56 a uložila se nám přímo do zásobníku. Kód by mohl vypadat takto:

int a = 56;

Můžeme to chápat tak, že proměnná a má přidělenu část paměti v zásobníku (velikosti datového typu int, tedy 32 bitů), ve které je uložena hodnota 56.

Vytvořme si novou konzolovou aplikaci a přidejme si k ní jednoduchou třídu, která bude reprezentovat uživatele nějakého systému. Pro názornost vypustím komentáře a nebudu řešit viditelnosti:

class Uzivatel
{
    public int vek;
    public string jmeno;

    public Uzivatel(string jmeno, int vek)
    {
        this.jmeno = jmeno;
        this.vek = vek;
    }

    public override string ToString()
    {
        return jmeno;
    }
}

Třída má 2 jednoduché veřejné atributy, konstruktor a přepsanou metodu ToString(), abychom uživatele mohli jednoduše vypisovat. Do našeho původního programu přidejme vytvoření instance této třídy:

int a = 56;
Uzivatel u = new Uzivatel("Jan Novák", 28);

Proměnná u je nyní referenčního typu. Podívejme se na novou situaci v paměti:

Zásobník a halda v paměti počítače - Objektově orientované programování v C# .NET

Vidíme, že objekt (proměnná referenčního datového typu) se již neukládá do zásobníku, ale do paměti zvané halda. Je to z toho důvodu, že objekt je zpravidla složitější než hodnotový datový typ (většinou obsahuje hned několik dalších atributů) a také zabírá více místa v paměti.

Zásobník i halda se nacházejí v paměti RAM. Rozdíl je v přístupu a velikosti. Halda je prakticky neomezená paměť, ke které je však přístup složitější a tím pádem pomalejší. Naopak zásobník je paměť rychlá, ale velikostně omezená.

Proměnné referenčního typu jsou v paměti uloženy vlastně nadvakrát, jednou v zásobníku a jednou v haldě. V zásobníku je uložena pouze tzv. reference, tedy odkaz do haldy, kde se poté nalézá opravdový objekt.

Např. v C++ je velký rozdíl mezi pojmem ukazatel a reference. C# žádné ukazatele naštěstí nemá a používá termín reference, ty se paradoxně principem podobají spíše ukazatelům v C++. Pojmy ukazatel a reference zde zmíněné tedy znamenají referenci ve smyslu C# a nemají s C++ nic společného.

Můžete se ptát, proč je to takto udělané. Důvodů je hned několik, pojďme si některé vyjmenovat:

  1. Místo ve stacku je omezené.
  2. Když budeme chtít použít objekt vícekrát (např. ho předat jako parametr do několika metod), nemusíme ho v programu předávat jako kopii. Předáme pouze malý hodnotový typ s referencí na objekt místo toho, abychom obecně paměťově náročný objekt kopírovali. Toto si vzápětí ukážeme.
  3. Pomocí referencí můžeme jednoduše vytvářet struktury s dynamickou velikostí, např. struktury podobné poli, do kterých můžeme za běhu vkládat nové prvky. Ty jsou na sebe navzájem odkazovány referencemi, jako řetěz objektů.

Založme si 2 proměnné typu int a 2 proměnné typu Uzivatel:

int a = 56;
int b = 28;
Uzivatel u = new Uzivatel("Jan Novák", 28);
Uzivatel v = new Uzivatel("Josef Nový", 32);

Situace v paměti bude následující:

Referenční hodnoty v C# v paměti počítače - Objektově orientované programování v C# .NET

Nyní zkusme přiřadit do proměnné a proměnnou b. Stejně tak přiřadíme i proměnnou v do proměnné u. Hodnotový typ se v zásobníku jen zkopíruje, u objektu se zkopíruje pouze reference (což je vlastně také hodnotový typ), ale objekt máme stále jen jeden. V kódu vykonáme tedy toto:

int a = 56;
int b = 28;
Uzivatel u = new Uzivatel("Jan Novák", 28);
Uzivatel v = new Uzivatel("Josef Nový", 32);
a = b;
u = v;

V paměti bude celá situace vypadat následovně:

Referenční hodnoty v C# v paměti počítače - Objektově orientované programování v C# .NET

Přesvědčme se o tom, abyste viděli, že to opravdu tak je :) Nejprve si necháme všechny čtyři proměnné vypsat před a po změně. Protože budeme výpis volat vícekrát, napíši ho poněkud úsporněji. Mohli bychom dát výpis do metody, ale ještě nevíme, jak deklarovat metody přímo v Program.cs a zpravidla se to ani moc nedělá, pro vážnější práci bychom si měli udělat třídu. Upravme tedy kód na následující:

            // založení proměnných
            int a = 56;
            int b = 28;
            Uzivatel u = new Uzivatel("Jan Novák", 28);
            Uzivatel v = new Uzivatel("Josef Nový", 32);
            Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v);
            // přiřazování
            a = b;
            u = v;
            Console.WriteLine("a: {0}\nb: {1}\nu: {2}\nv: {3}\n", a, b, u, v);
            Console.ReadKey();
    class Uzivatel
    {
        public int vek;
        public string jmeno;

        public Uzivatel(string jmeno, int vek)
        {
            this.jmeno = jmeno;
            this.vek = vek;
        }

        public override string ToString()
        {
            return jmeno;
        }
    }

Na výstupu programu zatím rozdíl mezi hodnotovým a referenčním typem nepoznáme:

Konzolová aplikace
a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

Nicméně víme, že zatímco v a a b jsou opravdu 2 různá čísla se stejnou hodnotou, v u a v je ten samý objekt. Pojďme změnit jméno uživatele v a dle našich předpokladů by se měla změna projevit i v proměnné u. K programu připíšeme:

            // změna
            v.jmeno = "John Doe";
            Console.WriteLine("u: {0}\nv: {1}\n", u, v);
                class Uzivatel
    {
        public int vek;
        public string jmeno;

        public Uzivatel(string jmeno, int vek)
        {
            this.jmeno = jmeno;
            this.vek = vek;
        }

        public override string ToString()
        {
            return jmeno;
        }
    }

Změnili jsme objekt v proměnné v a znovu vypíšeme u a v:

Konzolová aplikace
a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

u: John Doe
v: John Doe

Spolu se změnou v se změní i u, protože proměnné ukazují na ten samý objekt. Jestli se ptáte, jak vytvořit opravdovou kopii objektu, tak nejjednodušší je objekt znovu vytvořit pomocí konstruktoru a dát do něj stejná data. Dále můžeme použít klonování, ale o tom zas až někdy jindy. Připomeňme si situaci v paměti ještě jednou a zaměřme se na Jana Nováka.

Referenční hodnoty v C# v paměti počítače - Objektově orientované programování v C# .NET

Co se sním stane? "Sežere" ho tzv. Garbage collector.

Garbage collector - Objektově orientované programování v C# .NET

Garbage collector a dynamická správa paměti

Paměť můžeme v programech alokovat staticky, to znamená, že ve zdrojovém kódu předem určíme, kolik jí budeme používat. Doposud jsme to tak vlastně dělali a neměli jsme s tím problém, hezky jsme do zdrojového kódu napsali potřebné proměnné. Brzy se ale budeme setkávat s aplikacemi (a už jsme se vlastně i setkali), kdy nebudeme před spuštěním přesně vědět, kolik paměti budeme potřebovat. Vzpomeňte si na program, který zprůměroval zadané hodnoty v poli. Na počet hodnot jsme se uživatele zeptali až za běhu programu. CLR tedy musel za běhu programu pole v paměti založit. V tomto případě hovoříme o dynamické správě paměti.

V minulosti, hlavně v dobách jazyků C, Pascal a C++, se k tomuto účelu používaly tzv. pointery, neboli přímé ukazatele do paměti. Vesměs to fungovalo tak, že jsme si řekli operačnímu systému o kus paměti o určité velikosti. On ji pro nás vyhradil a dal nám její adresu. Na toto místo v paměti jsme měli pointer, přes který jsme s pamětí pracovali. Problém byl, že nikdo nehlídal, co do paměti dáváme (ukazatel směřoval na začátek vyhrazeného prostoru). Když jsme tam dali něco většího, zkrátka se to stejně uložilo a přepsala se data za naším prostorem, která patřila třeba jinému programu nebo operačnímu systému (v tom případě by naši aplikaci OS asi zabil - zastavil). Často jsme si však my v paměti přepsali nějaká další data našeho programu a program se začal chovat chaoticky. Představte si, že si uložíte uživatele do pole a v tu chvíli se vám najednou změní barva uživatelského prostředí, tedy něco, co s tím vůbec nesouvisí. Hodiny strávíte tím, že kontrolujete kód pro změnu barvy, poté zjistíte, že je chyba v založení uživatele, kdy dojde k přetečení paměti a přepsání hodnot barvy.

Když naopak nějaký objekt přestaneme používat, musíme po něm místo sami uvolnit, pokud to neuděláme, paměť zůstane blokovaná. Pokud toto děláme např. v nějaké metodě a zapomeneme paměť uvolňovat, naše aplikace začne padat, případně zasekne celý operační systém. Taková chyba se opět špatně hledá, proč program přestane po několika hodinách fungovat? Kde tu chybu v několika tisících řádků kódu vůbec hledat? Nemáme jedinou stopu, nemůžeme se ničeho chytit, musíme projet celý program řádek po řádku nebo začít prozkoumávat paměť počítače, která je v binárce. Brrr. Podobný problém nastane, když si někde paměť uvolníme a následně pointer opět použijeme (zapomeneme, že je uvolněný, to se může lehce stát), povede někam, kde je již uloženého něco jiného a tato data budou opět přepsána. Povede to k nekontrolovanému chování naší aplikace a může to dopadnout i takto:

Blue Screen Of Death – BSOD ve Windows - Objektově orientované programování v C# .NET

Můj kolega jednou pravil: "Lidský mozek se nedokáže starat ani o správu paměti vlastní, natož aby řešil memory management programu." Měl samozřejmě pravdu, až na malou skupinu géniů lidi přestalo bavit řešit neustálé a nesmyslné chyby. Za cenu mírného snížení výkonu vznikly řízené jazyky (managed) s tzv. garbage collectorem, jedním z nich je i C# a Java. C++ se samozřejmě nadále používá, ale pouze na specifické programy, např. části operačního systému nebo 3D enginy komerčních her, kde je potřeba z počítače dostat maximální výkon. Na 99 % všech ostatních aplikací se hodí C#, kvůli možnosti používat .NET a hlavně automatické správě paměti. Používat .NET bylo umožněno i v C++, hovoříme o tzv. managed C++, kde výsledná aplikace používala garbage collector. Projekt se však neuchytil, protože C++ tak již nemělo žádné výhody oproti C#, který je modernější.

Garbage collector - Objektově orientované programování v C# .NET

Garbage collector je vlastně program, který běží paralelně s naší aplikací, v samostatném vlákně. Občas se spustí a podívá se, na které objekty již v paměti nevedou žádné reference. Ty potom odstraní. Ztráta výkonu je minimální a značně to sníží procento sebevražd programátorů, ladících po večerech rozbité pointery. Zapnutí GC můžeme dokonce z kódu ovlivnit, i když to není v 99% případů vůbec potřeba. Protože je jazyk řízený a nepracujeme s přímými pointery, není vůbec možné paměť nějak narušit, nechat ji přetéct a podobně, interpret se o paměť automaticky stará.

Hodnota null

Poslední věc, o které se zmíníme, je tzv. hodnota null. Referenční typy mohou, na rozdíl od hodnotových, nabývat speciální hodnoty a to null. Klíčové slovo null označuje, že reference neukazuje na žádná data. Když nastavíme proměnnou v na null, zrušíme pouze tu jednu referenci. Pokud na náš objekt existuje ještě nějaká reference, bude i nadále existovat. Pokud ne, bude uvolněn GC. Změňme ještě poslední řádky našeho programu na:

            // změna
            v.jmeno = "John Doe";
            v = null;
            Console.WriteLine("u: {0}\nv: {1}\n", u, v);

Výstup:

Konzolová aplikace
a: 56
b: 28
u: Jan Novák
v: Josef Nový

a: 28
b: 28
u: Josef Nový
v: Josef Nový

u: John Doe
v:

Vidíme, že objekt stále existuje a ukazuje na něj proměnná u, v proměnné v již není reference. Hodnota null se bohatě využívá jak uvnitř .NET, tak v databázích. K referenčním typům se ještě jednou vrátíme.

V následujícím kvízu, Kvíz - Úvod, konstruktory, metody, datové typy v C# .NET OOP, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.


 

Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.

Stáhnout

Stažením následujícího souboru souhlasíš s licenčními podmínkami

Staženo 849x (25.3 kB)
Aplikace je včetně zdrojových kódů v jazyce C#

 

Předchozí článek
Řešené úlohy k 3. lekci OOP v C# .NET
Všechny články v sekci
Objektově orientované programování v C# .NET
Přeskočit článek
(nedoporučujeme)
Kvíz - Úvod, konstruktory, metody, datové typy v C# .NET OOP
Článek pro vás napsal David Hartinger
Avatar
Uživatelské hodnocení:
525 hlasů
David je zakladatelem ITnetwork a programování se profesionálně věnuje 15 let. Má rád Nirvanu, nemovitosti a svobodu podnikání.
Unicorn university David se informační technologie naučil na Unicorn University - prestižní soukromé vysoké škole IT a ekonomie.
Aktivity