V minulém díle tohoto seriálu jsme si vysvětlili základní syntaktické konstrukce jazyků Visual Basic .NET a C#. Dnes si podrobněji popovídáme o datových typech v .NETu.
Datové typy
Nyní už k samotným datovým typům - .NET Framework rozlišuje mezi dvěma druhy typů – hodnotovými a referenčními. Rozdíl mezi nimi je dosti důležitý a jeho pochopení je pro další práci klíčové.
Adresní prostor procesu
Každý proces, který je v systému spuštěn, má tzv. adresní prostor. Při jakékoliv práci s pamětí se odvolává na konkrétní místa pomocí adresy, což je nějaké číslo. Na 32-bitovém operačním systému, které jsou dnes nejčastější, má každý proces 4GB velký adresní prostor, který může využívat (ve skutečnosti to bývá méně, ale to není podstatné). Neznamená to, že by systém každému procesu přidělil 4GB paměti, tolik paměti nemá. Tento adresní prostor je totiž jen virtuální a skutečná paměť se přidělí až v okamžiku, kdy proces danou část adresního prostoru využije. Operační systém pak překládá tyto virtuální adresy (ty, které používá proces) na adresy ve skutečné paměti, dlouho nepoužívané bloky (stránky) odkládá na disk a když jsou potřeba, tak je zase načte zpět.
V adresovém prostoru je kromě celého kódu programu a různých dalších věcí také zásobník (stack) a halda (heap). Ty se používají pro ukládání dat a režijních informací.
Zásobník (stack)
Zásobník se používá pro ukládání lokálních proměnných metod a návratových adres. Pokud zavoláme metodu A a předáme jí nějaké parametry, na konec zásobníku se předá návratová adresa (adresa, odkud se metoda zavolala) a hodnoty všech parametrů, které se metodě předaly. Metoda A pak na konec zásobníku ještě přidá své lokální proměnné a provede svůj kód.
Navíc může třeba zavolat metodu B, čímž se opět na konec zásobníku přidá návratová adresa, předané parametry a lokální proměnné metody B. Jakmile metoda B skončí, proměnné i parametry se zruší, na zásobník se uloží návratová hodnota metody (výsledek volání funkce) a pokračuje se od návratové adresy v provádění metody A. Po jejím skončení se opět data ze zásobníku odeberou atd.
Následující ilustrace ukazuje zásobník a jeho změny během volání metody B z metody A. Je to samozřejmě trochu zjednodušené. Červeně vyznačený je řádek, který se zatím neprovedl. Datům na zásobníku, které patří nějaké konkrétní metodě, se také někdy říká aktivační záznam.
Je vidět, že když z metody A zavolám sebe samotnou (rekurze), tak nové volání metody A bude mít vlastní lokální proměnné. Na zásobníku se pro druhé zavolání A vytvoří vlastní aktivační záznam.
Pokud uděláte nekonečnou rekurzi (z metody A budete vždy volat A znovu), zásobník se za chvíli zaplní a .NET Framework vyvolá StackOverflowException. To je průšvih a ve většině případů tím aplikace skončí.
Halda (heap)
Na zásobník můžeme ukládat pouze položky, které mají předem známou velikost (už v době kompilace). Navíc tyto položku musí být dostatečně malé. Pokud bychom na zásobník ukládali například pole hodnot, které má desítky kilobajtů, tak při předání tohoto pole kamkoliv jinam (jako parametr do nějaké metody, přiřazení do jiné proměnné atd.) by se musela celá tato obluda kopírovat. To je samozřejmě velmi neefektivní a zbytečné, i když jednou za čas to také je potřeba.
Navíc bychom nemohli udělat metodu, která něco vyrobí a dá to k dispozici všem ostatním – po vyskočení z metody se její lokální proměnné zruší a výše jsme si řekli, že globální proměnné nemáme. Jediný způsob, jak to vrátit, by bylo pomocí návratové hodnoty, kde bychom zase měli několik kopírování. Zásobník tedy evidentně nestačí. Proto máme kromě něj ještě haldu. Halda je takové chytřejší úložiště, které si na začátku zarezervuje určitou paměť a umí ji přerozdělovat pro různé účely.
V praxi když tedy najednou potřebujeme na uložení něčeho příkladně 100 bajtů (což se můžeme dovědět až za běhu aplikace), na haldě se vyhradí volné místo o velikosti 100 bajtů (často o trochu více, kvůli efektivitě se totiž bloky často dorovnávají na nějaký násobek osmi nebo šestnácti atp.). Nám se vrátí pouze adresa začátku tohoto bloku a tu si pak uložíme do lokální proměnné v metodě, se kterou pracujeme. Tomu se říká pointer (ukazatel), v .NET Frameworku používáme spíše pojmu reference, ale je to víceméně totéž. Je to mnohem rychlejší, protože pole sedí na haldě a pokud jej chceme někam předat, předáme jen maličkou (na 32bitové architektuře 4bajtovou) adresu, která je malá a krásně může ležet na zásobníku. Více proměnných nám může ukazovat na stejné pole, pokud pole změníme, změna se projeví v obou proměnných.
Jakmile data na haldě již nejsou potřeba, v aplikacích psaných v C/C++ musíme dát pomocí speciálního příkazu pokyn k uvolnění této naalokované paměti, jinak by nám tam zůstala viset, mohli bychom na ni ztratit ukazatel a už by se nikdy nedala odalokovat. Protože na haldě se postupně přidává a odebírá i z prostřed, vznikají v ní díry a kusy volného místa. Při požadavku na přidělení kusu paměti je tedy třeba volné místo hledat, což zabere nějaký čas.
V .NETu za nás uvolňování paměti obstarává Garbage Collector, který to dělá jednou za čas (když je paměť potřeba) a po větších dávkách. Haldu potom sesype dohromady, takže díry zmizí a hledání volného místa je pak nesrovnatelně rychlejší – vždycky se přidává na konec, uprostřed haldy žádné volné místo není. Obtížnější je to uvolňování paměti a zjišťování, která paměť již není potřeba. Sesypávání haldy je dost náročná operace (klidně to může trvat stovky milisekund) a obnáší to, aby GC prošel všechny reference a změnil jejich hodnoty, protože objektům se může měnit adresa. Tato operace je časově náročná a aplikace je během provádění garbage collection pozastavena.
Větší bloky dat je rozhodně lepší ukládat na haldu, jejich předávání do parametrů či proměnných je velmi efektivní (předává se jen maličká adresa), pomocí této adresy se s daty dá velmi snadno manipulovat a o uvolnění se nemusíme starat, špinavou práci za nás odvede někdo jiný. Proměnná obsahuje jen adresu, ne samotná data. V jazycích C# a VB.NET s ní pracujeme ale úplně stejně, jako kdyby obsahovala přímo nějaká data.
Ve skutečnosti halda nebývá jedna, ale může jich být i více, jedna na malé objekty, jedna na velké atd.
Hodnotové typy
Otázkou je, proč jsme si zde složitě vysvětlovali, co je to halda a co je to zásobník. K pochopení rozdílu mezi hodnotovými a referenčními typy je to nezbytné.
Hodnotové typy se vždy ukládají na místě, kde se používají. Pokud máme lokální proměnnou hodnotového typu, hodnota této proměnné se ukládá přímo na zásobníku. Pokud se jedná o proměnnou, která je uvnitř nějaké třídy na haldě, je uložena přímo v paměťové oblasti dané instance této třídy, a tedy na haldě.
Hodnotové typy mají konstantní předem známou velikost, měly by být malé a vždy obsahují přímo hodnotu.
Hodnotových typů je v .NET Frameworku několik stovek, používáme je dnes a denně a patří sem především primitivní hodnotové typy, struktury a výčtové typy.
Primitivní hodnotové typy
Hodnotovými typy jsou datové typy uvedené v této tabulce (byla již v minulém díle, ale pro úplnost ji zde opakuji).
VB.NET | C# | .NET typ | Velikost | Popis |
SByte | sbyte | System.SByte | 8 bitů | 8-bit celé číslo se znaménkem (-128 až 127) |
Byte | byte | System.Byte | 8 bitů | 8-bit celé číslo bez znaménka (0 až 255) |
Short | short | System.Int16 | 16 bitů | 16-bit celé číslo se znaménkem (-2^15 až 2^15 - 1) |
UShort | ushort | System.UInt16 | 16 bitů | 16-bit celé číslo bez znaménka (0 - 2^16 – 1) |
Integer | int | System.Int32 | 32 bitů | 32-bit celé číslo se znaménkem (-2^31 až 2^31 – 1) |
UInteger | uint | System.UInt32 | 32 bitů | 32-bit celé číslo bez znaménka (0 - 2^32 – 1) |
Long | long | System.Int64 | 64 bitů | 64-bit celé číslo se znaménkem (-2^63 až 2^63 – 1) |
ULong | ulong | System.UInt64 | 64 bitů | 64-bit celé číslo bez znaménka (0 - 2^64 – 1) |
Single | float | System.Single | 32 bitů | 32-bit desetinné číslo podle IEEE 754 (7-8 platných cifer) |
Double | double | System.Double | 64 bitů | 64-bit desetinné číslo podle IEEE 754 |
Decimal | decimal | System.Decimal | 128 bitů | 128-bit desetinné číslo (uložené po desítkových cifrách, menší zaokrouhlovací odchylky) |
Boolean | bool | System.Boolean | | hodnoty true / false |
Char | char | System.Char | 16 bitů | 16-bit znak podle Unicode |
- | - | System.IntPtr | 32/64 bitů | Pointer (kvůli práci s Windows API) |
V jakémkoliv programovacím jazyce můžete pro datové typy použít název ze sloupce .NET typ, pokud si naimportujete jmenný prostor System, nemusíte uvádět celý název. V jazyce VB.NET můžete používat i názvy z 1. sloupce, v C# zase názvy z 2. sloupce (a také se to tak v drtivé většině případů píše, jsou to aliasy pro dané datové typy, aby se alespoň se základními typy pracovalo pohodlně).
Struktury (struct)
Struktura je komplexní datový typ, který sdružuje několik proměnných a může nad sebou definovat určité metody. Struktury se nepoužívají příliš často a je vhodné je používat pouze pro malá seskupení několika proměnných. Typickým příkladem užití je například reprezentace dvojice či vektoru:
Structure Vector
Public x As Double
Public y As Double
End Structure
struct Vector
{
public double x;
public double y;
}
Dovnitř struktury také můžeme přidat konstruktor nebo metody, o tom ale až později. Použití takového vektoru uvnitř metody může vypadat třeba takto:
Dim v As Vector
v.x = 15
v.y = 1.4677
Vector v;
v.x = 15;
v.y = 1.4677;
Proměnná v je lokální v nějaké metodě, na zásobníku tedy bude uloženo 2 * 64 bitů, protože jedna proměnná typu double má právě 64 bitů.
Výčtové typy (enum)
Výčtové typy se používají pro zpřehlednění, jedná se víceméně o pojmenované sady konstant. Pokud bychom měli například proměnnou, v níž bude uchováván stav nějaké operace, můžeme si udělat konvenci, že stav 0 je nezahájeno, stav 1 znamená, že akce probíhá a stav 2 znamená, že byla akce dokončena, ale po čase člověk zapomene, co je co, a proto jsou výčtové typy k nezaplacení. Pokud neřekneme jinak, je hodnota výčtového typu interně reprezentována typem int.
Enum Status
NotStarted = 0
Processing = 1
Finished = 2
End Enum
enum Status
{
NotStarted = 0,
Processing = 1,
Finished = 2
}
Hodnoty jednotlivých konstant nemusíme uvádět, pokud je nespecifikujeme, vygenerují se samy. Použití výčtového typu je jednoduché. Všimněte si, že můžeme přiřadit hodnotu proměnné již při její deklaraci.
Dim s As Status = Status.Finished
Status s = Status.Finished;
Referenční typy
Referenční typy se vždy ukládají na haldě a do proměnných se ukládá vždy pouze tzv. reference, což je ve skutečnosti adresa v paměti, kde jsou data uložena. Je nutné si uvědomit, že pokud do jedné proměnné přiřadíme referenční typ a pak obsah první proměnné přiřadíme do proměnné druhé, nikdy se nebudou kopírovat data samotného objektu. Proměnná drží pouze adresu v paměti, kopíruje se tedy pouze ta adresa. Dvě proměnné pak budou odkazovat na stejná data. Pokud data změníme přes jednu proměnnou, změna bude vidět i z druhé proměnné.
Data referenčního typu se z haldy mohou odstranit až v okamžiku, kdy na ně není v žádné proměnné reference.
Třída (class)
Třída je ve skutečnosti velmi podobná struktuře, ovšem hlavním rozdílem je, že je to referenční typ. Pokud by vektor z příkladu u struktur nebyla struktura, ale třída, ony dvě proměnné typu double by se uchovávaly na haldě, přičemž na zásobníku by byla (jakožto hodnota proměnné v) jen adresa, kde data na haldě leží. Kromě toho se ještě na haldě uloží nějaké dodatečné informace k identifikaci datového typu atd., takže zabrané paměti bude o ještě trochu více. Pro vektor není třída zrovna vhodný příklad, ale ukážeme si na něm ještě něco.
Class Vector
Public x As Double
Public y As Double
End Class
class Vector
{
public double x;
public double y;
}
Při použití v metodě pak narazíme na jednu věc – pokud změníme nějakou položku přes proměnnou v2, projeví se to i v proměnné v. To by s hodnotovými typy nešlo, každý by totiž měl svoji vlastní hodnotu. U referenčních typů je hodnota společná.
Dim v As Vector = New Vector()
v.x = 15
v.y = 1.4677
Dim v2 As Vector = v ' kopírují se jen adresy
v2.y = 15
' ve v.y bude také 15, data proměnných v a v2 jsou společná
Vector v = new Vector();
v.x = 15;
v.y = 1.4677;
Vector v2 = v; // kopírují se jen adresy
v2.y = 15;
// ve v.y bude také 15, data proměnných v a v2 jsou společná
Proměnné referenčního typu mohou mít také speciální hodnotu Nothing (VB.NET) resp. null (C#). Ta říká, že v proměnné není nic, je tam vlastně nulová adresa.
Pokud vytvoříme proměnnou hodnotového typu, nemusíme se o nic starat – v proměnné již bude připravená hodnota, místo v paměti se vyhradí automaticky. U proměnné referenčního typu se ale vyhradí místo jen pro referenci, pokud chceme vytvořit datovou položku, musíme použít klíčové slovo new. Tím zavoláme tzv. konstruktor, což je speciální metoda, která zajišťuje vytvoření a inicializaci datové položky.
Pokud bychom do proměnné v nic nepřiřadili, ukázka by se nezkompilovala, protože kompilátor požaduje, aby se do proměnné přiřadilo před jejím prvním použitím.
Pokud by v proměnné v byla hodnota Nothing, resp. null, a zavolali bychom v.x = 15, nastala by chyba, protože interně by se sáhlo do paměti na adresu, na kterou nemáme povoleno přistupovat, a runtime by nám vyhodil chybu, ale o tom až někdy příště.
Tím, že nadeklarujeme v a přiřadíme do ní new Vector(), se zajistí, že se na haldě vytvoří nová datová položka, která bude obsahovat dvě proměnné typu double, a adresa na tuto položku se uloží do proměnné v. Pak můžeme s v pracovat obdobně jako se strukturou.
Poznámka: pokud programujete v C++, je nutné si uvědomit, že v C++ se struktury a třídy chovají velmi podobně – chovají se jako hodnotové typy a abychom třídu donutili chovat se “kulturně”, musíme s ní pracovat pomocí pointerů. V .NETu jsou všechny třídy automaticky referenční a všechny struktury hodnotové. Při předávání hodnoty do metody se předává vždy obsah proměnné (tzn. u hodnotových typů data, u referenčních reference).
Problém je, že v .NETu nelze mezi hodnotovou a referenční formou přecházet tak jednoduše jako v C++, kde prostě někam předáte referenci na třídu, která leží třeba na zásobníku (samozřejmě jen pokud máte jistotu, že tam bude po celou dobu používání té reference). Naopak velmi jednoduše se v .NETu díky Garbage Collectoru řeší problém, kdy metoda má vrátit nějaký objekt (a chceme jej vrátit bez kopírování). V .NETu stačí pouze napsat něco jako return new Vector() a s instancí třídy Vector můžete vesele pracovat, v C++ se musíte postarat minimálně o to, aby to také někdo dealokoval, což se často řeší tak, že se prostě objekt vrací hodnotou, tím pádem se to kopíruje. díky tomu to ale odalokuje automaticky standardní mechanismus C++, protože kopie bude uložena na zásobníku. V C++ se odalokovávají ručně jen věci na haldě.
Dědičnost
Třídy v .NET Frameworku na rozdíl od struktur podporují dědičnost. Není podporována vícenásobná dědičnost, ale ona zas až tak moc v praxi není potřeba, resp. dá se bez ní obejít. Místo ní jsou podporována rozhraní.
Rozhraní (interface)
Rozhraní je lidově řečeno šablona pro to, co má třída umět. Rozhraní obsahuje seznam metod, vlastností a dalších položek, které musí třída, která toto rozhraní implementuje, obsahovat. Je nutné si uvědomit, že rozhraní neobsahuje žádný kód, jen hlavičky metod, ne jejich těla. Nemůže obsahovat ani proměnné, veškerou logiku a funkčnost musí zařídit třída.
.NET Framework vnitřně používá rozhraní na mnoha místech, pomocí nich například definuje porovnávání a mnoho dalších věcí.
Delegát (delegate)
Delegát je něco jako ukazatel na metodu, používá se pro dynamické volání metod.
O třídách, rozhraních, delegátech a dědičnosti si budeme více povídat v příštím díle tohoto seriálu.
Pole (array)
Pole jsou referenčním typem hlavně z toho důvodu, že mohou obsahovat velké počty položek a tím pádem většinou zabírají hodně místa. Obecně máme několik možností, jak s poli pracovat.
Jednorozměrné pole
Jednorozměrné pole je jednoduše tvořeno položkami naskládanými za sebou. Položky se indexují vždy od 0, není jiná možnost (ani ve Visual Basicu).
Dim p1(2) As Integer
int[] p1 = new int[3];
Všimněte si, že zde se již syntaxe dost podstatně liší. Ve VB.NET jako rozměr pole udáváme horní mez (tedy nejvyšší index, který je ještě platný). V C# naopak udáváme počet položek (tedy první index, který již platný není).
Vícerozměrné pole
Obyčejné vícerozměrné pole může mít více rozměrů. Každý rozměr je opět indexován od nuly. Výhodou je snadné použití pro obdélníkové (resp. kvádrové) oblasti dat.
Dim p2(2, 2) As Integer
int[,] p2 = new int[3,3];
Vícerozměrná pole jsou pohodlná na používání, ovšem jsou o trochu pomalejší než tzv. jagged arrays, totiž pole polí.
Jagged array
Pole polí je trochu těžší na vytvoření, nejprve je totiž třeba vytvořit pole referencí na jednotlivá pole. Výhodou je rychlost a efektivita v případě, že např. každý řádek není stejně dlouhý.
Dim p3(2)() As Integer
For i As Integer = 0 To 2
ReDim p3(i)(2)
Next
int[][] p3 = new int[3][];
for (int i = 0; i < 3; i++)
p3[i] = new int[3];
V prvním řádku se vytvoří pole integerových polí (na obrázku to pole, ze kterého vedou šipky). Pak se v cyklu projde každá položka tohoto pole a přiřadí se do ní nové pole (na obrázku to vodorovné).
Jagged arrays jsou o malinko efektivnější než vícerozměrná pole, ale je s nimi víc práce.
Přístup k položkám pole
K položkám pole přistupujeme podobně, jak pole deklarujeme. První řádek přistupuje k položkám prvního pole (jednorozměrného), druhý řádek přistupuje k položce druhého (vícerozměrného) pole a třetí řádek přistupuje k jagged array. Ve VB.NET se indexy uvádí do kulatých závorek, v C# do hranatých.
p1(0) = 1;
p2(0, 0) = 1;
p3(0)(0) = 1;
p1[0] = 1;
p2[0, 0] = 1;
p3[0][0] = 1;
Závěrem
V tomto díle jsme si spíše teoreticky popsali některé datové typy. V příštích dílech si vysvětlíme, jak pracovat s dědičností a s rozhraními.