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 ve VB.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ř. Integer). 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. Právě o tom bude dnešní VB.NET tutoriálu.

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ř. Integer, Float, Double, Char, Boolean 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 paměti počítače - Objektově orientované programování ve Visual Basic .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 Integer. Její hodnota je 56 a uložila se nám přímo do zásobníku. Kód by mohl vypadat takto:

Dim a As Integer = 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 Integer, 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:

Public Class Uzivatel
    Public vek As Integer
    Public jmeno As String

    Public Sub New(jmeno As String, vek As Integer)
        Me.vek = vek
        Me.jmeno = jmeno
    End Sub

    Public Overrides Function ToString() As String
        Return jmeno
    End Function
End Class

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:

Dim a As Integer = 56
Dim u As Uzivatel = 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í ve Visual Basic .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.

Pozn.: Např. v C++ je velký rozdíl mezi pojmem ukazatel a reference. VB.NET žá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 VB.NET 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 Integer a 2 proměnné typu Uzivatel:

Dim a As Integer = 56
Dim b As Integer = 28
Dim u As Uzivatel = New Uzivatel("Jan Novák", 28)
Dim v As Uzivatel = New Uzivatel("Josef Nový", 32)

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

Referenční hodnoty v v paměti počítače - Objektově orientované programování ve Visual Basic .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:

Dim a As Integer = 56
Dim b As Integer = 28
Dim u As Uzivatel = New Uzivatel("Jan Novák", 28)
Dim v As Uzivatel = New Uzivatel("Josef Nový", 32)
a = b
u = v

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

Referenční hodnoty v paměti počítače - Objektově orientované programování ve Visual Basic .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 Module1.vb 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
Dim a As Integer = 56
Dim b As Integer = 28
Dim u As Uzivatel = New Uzivatel("Jan Novák", 28)
Dim v As Uzivatel = New Uzivatel("Josef Nový", 32)
Console.WriteLine("a: {0}{4}b: {1}{4}u: {2}{4}v: {3}{4}", a, b, u, v, vbCrLf)
' přiřazování
a = b
u = v
Console.WriteLine("a: {0}{4}b: {1}{4}u: {2}{4}v: {3}{4}", a, b, u, v, vbCrLf)
Console.ReadKey()

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}{2}v: {1}{2}", u, v, vbCrLf)

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 paměti počítače - Objektově orientované programování ve Visual Basic .NET

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

Garbage collector - Objektově orientované programování ve Visual Basic .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 (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 - Objektově orientované programování ve Visual Basic .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 VB.NET a C#. 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í VB.NET, 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 VB.NET, který je modernější.

Garbage collector - Objektově orientované programování ve Visual Basic .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á.

Poslední věc, o které se zmíníme, je tzv. hodnota Nothing. Referenční typy mohou, na rozdíl od hodnotových, nabývat speciální hodnoty a to Nothing. Nothing je klíčové slovo a označuje, že reference neukazuje na žádná data. Když nastavíme proměnnou v na Nothing, 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 = Nothing

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. Nothing se bohatě využívá jak uvnitř .NET, tak v databázích (často pod označením null). 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 ve VB.NET OOP, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.


 

Předchozí článek
Řešené úlohy k 3. lekci OOP ve VB.NET
Všechny články v sekci
Objektově orientované programování ve Visual Basic .NET
Přeskočit článek
(nedoporučujeme)
Kvíz - Úvod, konstruktory, metody, datové typy ve VB.NET OOP
Článek pro vás napsal Michal Žůrek - misaz
Avatar
Uživatelské hodnocení:
12 hlasů
Autor se věnuje tvorbě aplikací pro počítače, mobilní telefony, mikroprocesory a tvorbě webových stránek a webových aplikací. Nejraději programuje ve Visual Basicu a TypeScript. Ovládá HTML, CSS, JavaScript, TypeScript, C# a Visual Basic.
Aktivity