V tomto dílu seriálu o programovacím jazyce VB.NET se seznámíme s rozdělením základních datových typů v prostředí MS .NET Framework a s rozhraním výchozího typu System.Object.
Výchozí dělení datových typů
V předchozí části jsem se seznámili s výhodami používání datových typů a významem specifikace typu při deklaraci proměnných VB.NET. Ačkoliv datové typy prostředí VB.NET lze členit podle celé řady hledisek, jejich základní dělení vychází z toho, jak prostředi VB.NET nakládá s proměnnými při výměně a sdílení proměnných jako parametry. V 16. dílu seriálu jsme se seznámili se dvěmi základními možnostmi: předávání parametrů hodnotou a odkazem (referencí).
Ty datové typy proměnných, které se v prostředí VB.NET předávají výlučně hodnotou se nazývají hodnotové datové typy. Ty ostatní, které se předávají odkazem (tzv. referencí) jako tzv. referenční datové typy. Výchozím datovým typem je System.Object, od něhož jsou odvozeny všechny ostatní datové typy. Jelikož prostředí .NET nemůže u takto obecného typu určit, zda jej bude schopno při manipulaci klonovat, je zřejmé, že System.Object je referenční datový typ:
- System.Object
- Hodnotové datové typy - odvozené od třídy System.ValueType
- Jednoduché (primitivní) datové typy
- Číselné (numerické) datové typy
- Celočíselné (integrální) typy: Byte, Int16 (Short), Int32 (Integer), Int64 (Long)
- Typy s plovoucí řádovou čárkou (Single, Double)
- Typ Decimal
- Nečíselné datové typy - datový typ Boolean, Char (znak) a Date
- Složené datové typy
- Struktury - definované typy s členy pole (field) a metoda (method)
- systémové (např. Guid, Drawing.Color, Drawing.Point, Drawing.Rectangle, Drawing.Size, Void,...)
- uživatelské (složené z libovolných hodnotových i referenčních typů)
- Výčty (enumerace) - odvozené od třídy System.Enum
- systémové (např. System.Date.DateFormat nebo System.IO.FileAttributes)
- uživatelské (využívající hodnotové typy Byte, Int16, Int32, Int64)
- Referenční datové typy- odvozené od třídy System.Object
- Sebepopisné datové typy
- Pole - odvozené od třídy System.Array
- Řetězce - odvozené od třídy System.String
- Třídy (standardní třídy) s členy událost (event), pole (field), metoda (method) a vlastnost (property)
- systémové (např. System.Console, System.Windows.Forms.Form, ....)
- uživatelské (uživatelsky definované třídy)
- Moduly (standardní moduly, statické třídy) s členy pole (field) a funkce (method)
- systémové (např. System.Math....)
- uživatelské (všechny uživatelsky definované moduly)
- Rozhraní (Interfaces)
- systémové (např. ICollection, IDictionary, IDesigner, ...)
- uživatelské (libovolná uživatelsky definovaná rozhraní)
- Ukazatel (odkaz, pointer)
- řízené (spravované CLR, tzv. managed)
- Delegáty (tzv. odkazy na funkce, funkční odkazy - managed function pointers)
- Managed (System.IntPtr)
- neřízené (v prostředí VB.NET nejsou podporovány)
Rozdíl mezi hodnotovými a referenčními typy
Hodnotové a referenční typy se zásadně liší výchozím způsobem alokace objektů v paměti. Zatímco pro hodnotové typy se vyhražuje zásobník (tzv. stack), což je dynamický alokovaný blok paměti spojený s volajícím vláknem spravovaný operačním systémem přístupem LIFO ("last-in, first-out"), pro referenční datové typy, které nemají předem omezenou velikost se vyhražuje náhodně alokovaný souvislý blok z volné oblasti systémové paměti - tzv. hromady, nebo též haldy (angl. "heap"), která je spravována samostatným procesem GarbageCollector (GC). Jelikož hodnotové typy nejsou spravovány procesem GC přímo, nelze jim přiřadit hodnotu Nothing ani vytvořit odkaz na hodnotový typ. A protože GC neudržuje pointer na hodnotové typy, přírazení hodnoty proměnné hodnotového typu ve skutečnosti alokuje v paměti prostor pro její kopii.
V této souvislosti je důležité, že ačkoliv struktury mohou být tvořeny i z referenčních typů, samy o sobě se předávají vždy hodnotou - což bychom měli mít na paměti, pokud pomocí nich předáváme rozsáhlejší objekty (při předávání a přiřazení hodnoty proměnníé hodnotového typu se v paměti alokuje prostor pro nezávislou, samostatnou kopii objektu). Naopak referenční datové typy, jako jsou pole se stejně jako všechny třídy a řetězce se přenášejí vždy odkazem (a to i v případě, kdyby samy o sobě byly tvořeny výlučně hodnotovými typy).
Jelikož se při priřazení hodnotového typu vytváří v paměti kopie objektu, nemůže změna veřejného členu ovlivnit stav této kopie po přiřazení - což b případě referenčního typu platí přesně obráceně. Tuto skutečnost si můžeme snadno demonstrovat na následujícím příkladu, který navrhl čtenář Benjamin Hejda:
Nejprve vytvoříme po dvou instancích struktury STest a třídy CTest s veřejným členem (polem) iFld, který inicializujeme na hodnotu 5. Do proměné první instance pak přiřadíme druhou a hodnotu jejího privátního členu následně nastavíme na odlišnou hodnotu, např. číslo 10. Pak do systémové konzole vypíšeme hodnotu privátního členu původní proměnné. Celý pokus pak zopakujeme s oběma instancemí třídy a výsledky porovnáme:
Imports System, System.Console
Module modMain
Sub Main
Dim strA As New STest(), strB As New STest()
Dim clsA As New CTest(), clsB As New CTest()
strA.iFld = 5
strB = strA
strB.iFld = 10
WriteLine("strA.iFld: " & strA.iFld)
clsA.iFld = 5
clsB = clsA
clsB.iFld = 10
WriteLine("clsA.iFld: " & clsA.iFld)
End Sub
End Module
Class CTest
Public iFld
End Class
Structure STest
Public iFld
End Structure
Po zkompilování a spuštění ukázky např. v prostředí Snippet Compiler získáme následující výsledek
strA.iFld: 5
clsA.iFld: 10
Jak vidíme, pokus s instancemi třídy nedopadl stejně, jako analogický pokus s instancemi struktur. Pokud nebudeme brát zřetel na rozdíl mezi chováním hodnotových a referenčních typů, může tento rozdíl vést ke vzniku dosti záludných chyb. Z tohoto důvodu je hodnotový a referenční typ proměnných v prostředí ClassBrowser a nabídkách IntelliSense vývojových prostředí jako je Snippet Compiler nebo Visual Studio 2005 Beta výrazně rozlišen vizuálně - struktrurám a třídám jsou zde přiřazeny odlišné symboly (piktogramy).
Z historického hlediska struktury v klasickém procedurálním programování představují složené datové typy: v podstatě souvislý blok bajtů, který se na zásobník vláknu (prováděcí frontě instrukcí) předává jako jeden celek. Efektivita takové práce s pamětí umožňuje předávat struktury hodnotou, stejně jako ostatní primitivní datové typy, proto jsou všechny struktury odvozeny stejně jako ostatní hodnotové proměnné od třídy System.ValueType, nikoliv přímo od System.Object a měly by sloužit především ke sdílení a výměně dat, nikoliv kódu. Struktury se v důsledku toho od tříd liší ještě v celé řadě dalších ohledů, které si probereme v části seriálu věnované objektovému programování a práci s objekty.
Common Type System
Jelikož prostředí .NET je postaveno na objektovém základě a pojem "primitivní datový typ" nezná, primitivni datové typy VB.NET jsou v prostředí .NET Framework vnitřně reprezentovány (zapouzdřeny) strukturami, odpovídajícím Obecnému Systému Typů (Common Type System, tzv. CTS) a jazyk VB.NET pro vyhražené datové typy používá zavedený zástupný název (přezdívka, tzv. alias). S nejběžnějšími aliasy se seznámíme při popisu odpovídajících datových typů, ale v praxi je možné v rámci typových deklarací stejně dobře používat přimo typy CTS. Vybrané primitivní typy VB.NET dále podporují tzv. specifikátory typu - přípony, kterými je možné specifikaci typu v rámci typové deklarace nahradit: Níže uvedené příklady deklarací VB.NET jsou v důsledku toho funkčně ekvivalentní:
Dim i As System.Int32 ` deklarace s použitím CTS typu
Dim i As Integer ` deklarace s použitím VB.NET aliasu
Dim i% ` deklarace s použitím VB.NET specifikátoru typu
S ohledem na lepší přenositelnost ukázek do prostředí ostatních jazyků .NET bude v tomto seriálu dávána přednost CTS typům, před deklaracemi s použítím jazykově specifických aliasů. Za zmínku stojí, že CTS neumožňuje odvozovat datový typ od více datových typů současně a jeho struktura je reprezentována třídou System.Type, zastřešující rozhraní jednoltivých typů a jejich vzájemné typové konverze, se kterými se seznámíme při popisu jednotlivých datových typů. Z přehledu uvedeného níže vyplývá, jak jsou datové typy VB.NET mapovány na typy CTS:
Obecný systém typů .NET (CTS)
Boxing a unboxing, implicitní a explicitní přetypování
Pro snazší práci s typy VB .NET podporuje mechanismus, který umožňuje na vyžádání zacházet s hodnotovým typem jako s nařazeným referenčním typem (objektem) a provádět s jeho pomocí typově specifické datové konverze.Procesu zapouzdření na nadřazený objekt, tj. převedení hodnotové proměnné na referenční objekt a zpět se říká boxing, resp. unboxing a ve výchozím nastavení prostředí VB.NET (Option Strict Off) probíhá na pozadí (implicitně). Jelikož to však znamená, že běhové prostředí při přířazování hodnot proměnným hodnotových typů neustále kontroluje, zda a jak je možné provést implicitní typovou konverzi, jde o činnost, která běh prostředí zpomaluje.
Jak jsme si uvedli v předchozím dílu, s ohledem na výkon a riziko nepředpokládané ztráty dat je vhodné používat direktivu Option Strict On, která implicitní typové konverze potlačuje a převádí tak prostředí VB.NET na robustní, staticky a současně silně typový jazyk. Statický typ nemůže být na rozdíl od dynamického za běhu předefinován a silná typová kontrola zajištuje, že proměnné nemůže být za běhu přiřazena hodnota, vyžadující automatické přetypování směrem k méně obecnému datovému typu, spojenému s potenciální ztrátou informace (tzv. narrowing). V těchto případech je nutné použít výslovné (explicitní) přetypování metodou System.CType(), vyžadující dva parametry - převáděný objekt a požadovanou hodnotu cílového typu:
Option Strict On
Imports System
Module modMain
Sub Main
Dim i As Int32 = Int32.MaxValue, n As Int64 = Int64.MaxValue
n = i ` v pořádku, Int64 zahrnuje všechny hodnoty oboru Int32
i = n ` chyba, datový typ Int64 nelze automaticky převést na Int32 !
i = CType(n, Int32) ` formálně v pořádku, ale může dojít ke ztrátě informace
End Sub
End Module
Poznámky:
- Prostředí .NET kromě hodnotových a odkazových typů rozeznává ještě třetí typ, tzv. typ ukazatel (odkaz, tzv. pointer). Rozlišují se dva základní typy ukazatelů - na kód (tzv. function pointer) a na data. Jelikož prostředí VB.NET nepodporuje běh v neřízeném (unsafe) kontextu s přímým přístupem k systémové paměti, nepodporuje na rozdíl od C# proměnné typu odkaz přímo, s odkazy však VB.NET umožňuje pracovat nepřímo prostřednictvím struktury System.IntPtr a třídy System.Reflection.Pointer.
- Prostředí C# se od VB.NET liší také přístupem k implicitním typovým konverzím - volba Option Strict On je v něm použita jako výchozí. Z toho vyplývá, že boxing a unboxing proměnných v jazyku C# probíhá na vyžádání, podobně jako ve VB.NET při zapnuté direktivě Option Strict On. Je proto součástí syntaxe jazyka C#, na rozdíl od explicitních typových konverzí VB.NET, které pro tento účel využívají metodu System.CType(), popř. specifické konverzní funkce.
Datový typ System.Object, zjištění typu objektu
Jak již bylo řečeno, výchozím typem proměnných VB.NET (a obecně prostředí .NET Framework) je referenční datový typ System.Object, který je deklarovaným proměnným bez uvedení typu přidělován automaticky. Deklarace:
Dim pocetKolecek As Object
je tedy funkčně rovnocenná deklaraci bez uvedení datového typu, kterou jsme používali doposud:
Dim pocetKolecek
Jak je uvedeno výše, datový typ Object může být po inicializaci proměnné implicitně ("na pozadí") přetypován na libovolný odvozený datový typ, např. typ System.String pro textové řetězce nebo System.Double, což je výchozí datový typ pro číselné hodnoty. Každý objekt .NET disponuje základní sadou metod, jednou z nich je metoda GetType(), umožňující zjistit, na jaký datový typ je objekt momentálně přetypován. Implicitní přiřazení typu objektové proměnné si můžeme vyzkoušet v následující ukázce:
Imports System.Console
Module modMain
Sub Main
Dim obj As Object
obj = New Object : WriteLine(obj.GetType)
obj = 1.1 : WriteLine(obj.GetType)
obj = 1 : WriteLine(obj.GetType)
obj = "1" : WriteLine(obj.GetType)
End Sub
End Module
Program po spuštění v prostředí VS.NET nebo Snippet Compiler vypíše sadu různých hodnot typu, podle aktuální hodnoty proměnné a nastavení lokalizace prostředí. Vidíme, že objekt obj může být současně svým vlastním datovým typem a v závislosti na obsahu proměnné může zapouzdřit libovolný odvozený typ:
System.Object
System.Double
System.Int32
System.String
Poznámka:
- Ačkoliv starší verze VB datový typ Objekt podporují, výchozím typem proměnné v prostředí VB 6 a nižší je hodnotový typ Variant, který dále mohl nabývat celou řadu podtypů.
Typ Variant je i výchozím datovým typem v dynamických prostředích a jazycích typu Visual Basic for Applications (VBA) a JavaScript, ale v prostředí VB.NET se nepoužívá a je zde nahražen obecným typem System.Object.
Porovnání objektů, hodnota Nothing
V předchozí ukázce stojí za povšimnutí, že metoda GetType() vyžaduje, aby do hodnoty proměnné byla přiřazena nějaká hodnota. Jelikož datový typ System.Object může obsahovat cokoliv a prostředí VB.NET s pamětí šetří, deklarace proměnné typu System.Object sama o sobě pouze vymezí v paměti místo pro proměnnou, ale ne už pro její data (tomuto se říká pozdní vazba typu na objekt, tzv. "late binding", ukazatel na objekt je oddělen od hodnoty objektu). Vyprázdnění obsahu proměnné lze v prostředí VB.NET provést nastavením na prázdnou (void) arbitrární hodnotu Nothing (null, čili "Nic"):
Imports System.Console
Module modMain
Sub Main
Dim p = "něco": WriteLine(p.GetType)
p = Nothing : WriteLine(p.GetType) ` výjimka, typ prázdné proměnné není definován...
End Sub
End Module
Poslední řádek kódu po spuštění vyvolá výjimku typu System.NullReferenceException, jelikož odkaz p již není nastaven na instanci žádného objektu. Nastavením hodnoty proměnné na Nothing současně dojde k uvolnění paměti. Velikost fyzické paměti v bajtech, kterou v prostředí VB.NET zabírá instance objektu lze pro základní hodnotové typy zjistit funkcí Microsoft.VisualBasic.Len(), která v případě, že proměnná není inicializována, nebo je nastavena na hodnotu Nothing vypíše 0:
Dim void: WriteLine(Microsoft.VisualBasic.Len(void))
Pro porovnání obsahu dvou objektů lze použít metodu Equals(), což je - stejně jako metoda GetType() - implicitní metoda každého objektu v prostředí .NET. Metoda Equals() vrací logickou hodnotu True, pokud jsou obsahy obou proměnných shodné a nahražuje tak operátor ekvivalence (=) co do typu i hodnoty. Porovnání referencí objektových proměnných, tj. toho, zda dvě proměnné odkazují na shodný objekt je naproti tomu odlišný úkol a lze jej provést pomocí operátoru Is ("je (rovno)"), např. příkaz:
Dim void = Nothing: WriteLine(void Is Nothing)
v tomto případě vrátí logickou hodnotu True. Z logiky věci vyplývá, že operátor Is lze použít jen po porovnávání odkazů na referenční typy. Porovnávání odkazů (tzv. referencí) nezaměňujme s porovnávání obsahu objektů, ani s porovnáváním jejich datových typů:
Dim datumCLR = DateTime.Now, datumVB = Microsoft.VisualBasic.Now
WriteLine(datumCLR = datumVB) ` vrátí True, proměnné obsahují stejná data
WriteLine(datumCLR.Equals(datumVB)) ` vrátí True, proměnné obsahují shodné objekty
WriteLine(datumCLR Is datumVB) ` vrátí False, proměnné neodkazují na týž objekt!
Pro určení toho, zda proměnná není inicializována, resp. vyprázdněna lze také použít operátor pro porovnání referencí Is ve spojení s hodnotou Nothing, např.
Dim void : WriteLine(void Is Nothing)
void = Nothing: WriteLine(void Is Nothing)
vrátí v obou případech logickou hodnotu True (pravda).
Poznámky:
- Ke zjištění toho, zda je hodnotová proměnná inicializována se ve starších verzích VB používala funkce IsEmpty(). Tato funkce ve VB.NET již není podporována a byla nahražena metodou Microsoft.VisualBasic.IsNothing(). Touto funkcí je nutné nahradit i VB6 funkci IsMissing() sloužící pro zjištění, zda byl při volání funkce vypuštěn volitelný parametr. Hodnota Nothing v prostředí VB verze 6.0 a starších nabývala referenčního typu a používala se pouze ve spojení s objektovými proměnnými, v prostředí VB.NET ji lze porovnat / přiřadit i proměnným hodnotových typů.
Dim void
WriteLine(void Is Nothing)
WriteLine(Microsoft.VisualBasic.IsNothing(void))
WriteLine(IsEmpty(void)) ` chyba, v prostředí VB.NET již není podporováno!
Stručné shrnutí
Seznámili jsme se se základním členěním datových typů prostředí VB.NET. Výchozím referenčním datovým typem je třída System.Object s výchozí hodnotou Nothing (bez hodnoty). podporující metody GetType() a Equals() pro navrácení a porovnání zapouzdřeného typu. Výchozím hodnotovým typem je System.ValueType a pokud není zapnuta silná typová kontrola (direktiva Option Strict On) probíhá implicitní typová konverze na výchozí referenční typ nazývané boxing. V případě, že by bylo spojeno se ztrátou informace při zúžení typu, dojde v prostředí se silnou typovou kontrolou k výjimce a pak je nutné přetypování provést explicitně s využitím metody System.CType().
V dalším dílu seriálu se seznámíme se základními hodnotovými typy prostředí VB.NET a jejich nejčastějším použitím.