4. díl - Referenční a hodnotové datové typy

C# .NET Objektově orientované programování Referenční a hodnotové datové typy American English version English version

V minulém tutoriálu pro C# jsme si vytvořili svůj první pořádný objekt, byla jím hrací kostka. 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

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řetížený 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

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.

Pozn.: 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

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

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();

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

uživatelé jako referenční typy v C#

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);

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

uživatelé jako referenční typy v C#

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

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

Garbage collector

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 (pojem viz 1. lekce) 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 nekontorolovanému chování naší aplikace a může to dopadnout i takto:

Blue Screen Of Death – BSOD ve Windows

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

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. Null je klíčové slovo a 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;

Výstup:

uživatelé jako referenční typy v C#

Vidíme, že objekt stále existuje a ukazuje na něj proměnná u, v proměnné v již není reference. 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, příště si zas něco praktického naprogramujeme, ať si znalosti zažijeme. Prozradím, že půjde o objekt bojovníka do naší arény. To je zatím vše :)


 

Stáhnout

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

 

  Aktivity (4)

Článek pro vás napsal David Čápka
Avatar
Autor pracuje jako softwarový architekt a pedagog na projektu ITnetwork.cz (a jeho zahraničních verzích). Velmi si váží svobody podnikání v naší zemi a věří, že když se člověk neštítí práce, tak dokáže úplně cokoli.
Unicorn College Autor se informační technologie naučil na Unicorn College - prestižní soukromé vysoké škole IT a ekonomie.

Jak se ti líbí článek?
Celkem (26 hlasů) :
4.846144.846144.846144.846144.84614


 


Miniatura
Předchozí článek
Cvičení k 3. lekci OOP v C# .NET
Miniatura
Následující článek
Cvičení k 4. lekci OOP v C# .NET

 

 

Komentáře
Zobrazit starší komentáře (18)

Avatar
phoer
Člen
Avatar
phoer:

Je nějaký rozdíl mezi tímto :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ConsoleApplication1
{
    class Program
    {
        static void Main(string[] args)
        {
            Uzivatel z = new Uzivatel("", 23);
            Uzivatel x = new Uzivatel("", 42);

            z.jmeno = "Pepa Novotny";
            x.jmeno = "Pepa Masek";

            Console.WriteLine("z:{0}\nx:{1}\n", z, x);

            z = x;

            z.jmeno = "Petr Novotny";
            Console.WriteLine("z:{0}\nx:{1}\n", z, x);
            x.jmeno = "Karel Hynek";
            Console.WriteLine("z:{0}\nx:{1}\n", z, x);

            Console.ReadKey();

        }
    }
}

a tím co je v článku ? :)

Editováno 24.11.2013 1:21
Odpovědět 24.11.2013 1:20
„Co slyším, to zapomenu. Co vidím, si pamatuji. Co si vyzkouším, tomu rozumím.“ - Konfucius
Avatar
David Čápka
Tým ITnetwork
Avatar
Odpovídá na phoer
David Čápka:

Co je v článku se dá číst.

Odpovědět  +1 27.11.2013 9:29
Miluji svou práci a zdejší komunitu, baví mě se rozvíjet, děkuji každému členovi za to, že zde působí.
Avatar
jandostal
Člen
Avatar
jandostal:

Díky za článek, C# mi zatím přijde oproti C dosti sympatický, tak uvidíme jak dál. Jen pár postřehů, ten kód není třeba kontrolovat řádku po řádce, když se použije mudflap nebo valgrind nebo jiná alternativa. A uvolněný pointer by měl člověk správně po uvolnění nastavit na NULL.

Odpovědět 16.1.2015 8:39
Windows for fun, Linux for work, Mac for what?
Avatar
pracansky
Člen
Avatar
pracansky:

Dovolil bych si říct že stejný program v C# mi zabere třetinu času než v C a bude v něm méně chyb přesto že C znám lépe. Jednoduše se můžu zabývat tím co je podstatné a neřešit provozní problémy.

To že se programátor nemusí starat o správu paměti je ale zavádějící. Když začne pracovat s větším množstvím dat, je potřeba se zamyslet nad tím kdy a proč se paměť alokuje.

Ani alokace ani Garbage collector totiž nejsou zadarmo.

Osobně si myslím že psát aplikace pro PC v C/C++ dnes může jen masochista:)

 
Odpovědět  -4 12.4.2015 21:45
Avatar
O.S.DV.F
Člen
Avatar
O.S.DV.F:

Petřeboval bych poradit jak se dá nějakému refernčnímu datovému typu přiřadit hodnota tak, aby se nevytvořil odkaz na jiný objekt, ale aby se hodnota "zkopírovala." Mám kód

IntPtr handle = window.Key; //window je položka v IDictionary<IntPtr, string>
string title = window.Value;

icons.Add(new TaskbarIcon(Icon.FromHandle(handle)) { ToolTip = title });

Jenže po tom co se ikona přiřadí k TaskBarIcon mi to vyvolá ObjectDispose­dException, takže bych potřeboval vědět, jak se dá hodnota zkopírovat z jednoho objektu na druhý.

Odpovědět 28.8.2015 11:41
Jo! Zkompilovalo se to!
Avatar
MrPabloz
Člen
Avatar
Odpovědět 28.8.2015 13:23
Harmonie těla a duše, to je to, oč se snažím! :)
Avatar
O.S.DV.F
Člen
Avatar
Odpovědět 28.8.2015 13:28
Jo! Zkompilovalo se to!
Avatar
RooBoo
Člen
Avatar
RooBoo:

Ahojte neviem ci to tak ma byt ale ak napisem

public override string ToString()
        {

        }

tak mi VS automaticky hodi do {}

return base.ToString();
 
Odpovědět 27. ledna 13:23
Avatar
pocitac770
Redaktor
Avatar
Odpovídá na RooBoo
pocitac770:

Každá metoda, která má návratový typ (v tomto případě .ToString()) ho musí i vracet, na to je tam ono slovo return, a aby nenastala chyba kvůli chybějícímu "návratovému" řádku kódu, tak tam rovnou vloží ten předdefinovaný, který si můžeš upravit podle potřeby na co chceš.

Editováno 27. ledna 16:22
 
Odpovědět  +3 27. ledna 16:21
Avatar
blazoid
Člen
Avatar
blazoid:

Ahoj, mám problém s opětovným vytvořením instance objektu, konkrétně jde o zatížení operační paměti. V kódu níž (jde o Windows forms aplikaci) se při stisknutí tlačítka "loadFileBtn" provede načtení datalogu do listu - otevřu dialogové okno, vyberu soubor, cestu k souboru následně předám v konstruktoru třídy sprava_dat, instance třídy se jmenuje "databáze". Problém nastane, když chci načíst za běhu programu jiný soubor. Stisknu opět tlačítko load... a dám načíst totožný soubor jako před tím. Try/catch blok již však není proveden, krokováním jsem zjistil, že provádění selže právě u metody databaze.Nacti­Log(), právě v této metodě probíhá stream reading načítaného souboru a ukládání do listu. Dle ukazatele využití paměti ve VS nedojde po opětovném vytvoření instance třídy sprava_dat k uvolnění. Zkusil jsem po přiřazení databaze = null, což následuje hned po vytvoření nové instance, zavolat garbage collector, situace se však nezměnila. Načítaný csv soubor má poměrně dost řádků (cca 8400).

Píšu to sem, neboť se mně zdá, že právě tento článek se problematiky dotýká...

Díky moc za případnou pomoc.

private void loadFileBtn_Click_1(object sender, EventArgs e)
{

    // procedura pro výběr souboru, vytvoření instance databáze a načtení hodnot do databáze

    OpenFileDialog opf = new OpenFileDialog();

    opf.Filter = "Choose File(*.csv) | *.csv";


    if (opf.ShowDialog() == DialogResult.OK)
    {
        fileSource = opf.FileName;
        loadedFileNameLabel.Text = fileSource;

        fileCreationTimeLabel.Text = Convert.ToString(File.GetCreationTime(fileSource));
    }

    databaze = null;

    GC.Collect();

    databaze = new sprava_dat(fileSource);



    int pocet_zaznamu = 0;
    try
    {
        pocet_zaznamu = databaze.NactiLog();
       // datalogListBox.Items.Clear();
        //datalogListBox.Items.AddRange(databaze.VratVsechny());

        pocetZaznamuTB.Text = pocet_zaznamu.ToString();


        dataGridView1.DataSource = databaze.VratVsechny();

        loadingDataLogGridView.DataSource = databaze.VypisLoadingLog(infoCheckBox.Checked, warningCheckBox.Checked, errorsCheckBox.Checked);


    }
    catch
    {
        MessageBox.Show("Databázi se nepodařilo načíst, soubor zřejmě neexisituje.",
                "Chyba", MessageBoxButtons.OK, MessageBoxIcon.Error);

        pocetZaznamuTB.Text = "-";
    }

}
 
Odpovědět  -1 10. května 20:23
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 28. Zobrazit vše