V tomto seriálu jsme se naučili základní konstrukce jazyka Visual Basic .NET a také jsme se je v několika málo dílech naučili používat. Jen stručně shrnu, co už umíme, pokud si v něčem nejste jisti, projděte si patřičné minulé díly:
Další úroveň - objekty
Těchto pár odrážek, které jsem tady vyjmenoval, jsou základní kameny procedurálního programování. Při návrhu našich programů jsme si problém rozdělili na několik úloh, které se provádějí s různým vstupem opakovaně, a pro každou z nich jsme vytvořili proceduru nebo funkci.
Další úrovní, která nás jako programátory čeká, je tzv. objektově orientované programování. U velkých projektů se v množství procedur ztrácí přehlednost a jednotlivé úlohy se dají velmi efektivně rozdělit do skupin podle jejich funkčnosti. Díky objektovému programování můžeme efektivně oddělit různé vrstvy aplikace, což zvýší přehlednost a sníží počet možných chyb, které bychom mohli "nasekat". V předminulém dílu jsem se už snažil objekty trochu přiblížit, nyní se do toho podíváme hlouběji a podrobněji, první seznámení máme už za sebou.
Co je to objekt?
Filozofická otázka na začátek, objektem ve skutečném světě je všechno. Objekt v programování je instance nějaké třídy. Třída popisuje to, co by objekt měl umět a co by si měl pamatovat, je to taková šablona, obecný model. Podle každé třídy pak můžeme vytvořit kolik chceme nezávislých objektů, které budou přesně vyhovovat její definici, budou si zachovávat svůj vlastní stav, a budou umět provádět nějaké akce.
Deklarace třídy
Vše, co třída obsahuje, musí být ve VB.NET zapsáno uvnitř této konstrukce:
Public Class MojeTrida
End Class
Dovnitř třídy můžeme zapisovat deklarace těchto "věcí" (ve skutečnosti je jich víc, nám teď budou stačit tyhle):
- metody (dříve jsme jim říkali funkce a procedury, u tříd se jim dohromady říká metody)
- proměnné
- vlastnosti
Řízení přístupu
Pro každou z těchto položek uvnitř třídy můžeme určit, kdo k ní může přistupovat. Máme v zásadě 4 možnosti:
- Public - k členům třídy budeme moci přistupovat odkudkoliv
- Friend - k členům třídy budeme moci přistupovat z dané assembly (ve většině případů je to to samé, jako projekt; když tedy napíšete nějakou DLL knihovnu, nedostante se ke členům označeným jako Friend z jiného projektu, který tuto knihovnu využívá)
- Protected - k členům třídy budeme moci přistupovat jen z této třídy a ze tříd, které tuto třídu dědí (o tom až za chvíli)
- Private - k členům třídy budeme moci přistupovat jen z této třídy
Proměnné, metody a vlastnosti uvnitř třídy
Dovnitř třídy můžeme nadeklarovat proměnné. Místo nám známého Dim ale píšeme modifikátor přístupu:
Public prom1 As Integer
Metody uvnitř třídy píšeme úplně stejně jako dříve, opět akorát před Sub nebo Function přihodíme modifikátor přístupu:
Public Sub MojeMetoda(ByVal par As String)
End Sub
Novým elementem, který jsme sice používali, ale nikdy nedeklarovali, jsou vlastnosti. Vlastnost je typicky dvojice metod Get a Set, první z nich má za úkol nějakou hodnotu získat, druhá z nich hodnotu nastavuje. V drtivé většině případů se vlastnosti používají k zajištění a ošetření čtení a změny nějaké proměnné. Pokud vypustíme metodu Get, nebo Set, dostaneme WriteOnly resp. ReadOnly vlastnost, tedy vlastnost určenou pouze k zápisu, resp. ke čtení. První typ se nepoužívá téměř nikdy, druhý typ je poměrně častý. Deklarace vlastnosti vypadá takto:
Private _promenna As Integer
Public Property Promenna() As Integer
Get
Return _promenna
End Get
Set(ByVal value As Integer)
_promenna = value
End Set
End Property
Toto je typické použití vlastnosti, je to taková obálka kolem proměnné - v metodě Set předanou hodnotu uložíme do proměnné, v metodě Get vrátíme to, co v proměnné je.
Private _jenKeCteni As Integer
Public ReadOnly Property JenKeCteni() As Integer
Get
Return _jenKeCteni
End Get
End Property
V této ukázce je proměnná určená jen ke čtení. Tady je význam zřejmý, zvenčí uvidíme hodnotu proměnné, ale nemůžeme ji nijak změnit, protože samotná proměnná je Private. Vlastnost jako taková je ale Public, což znamená, že vlastnost zvenku použít můžeme.
Nestačí nám místo vlastnosti Public proměnná?
Pokud jste se ještě neztratili, možná vám přijde divné, k čemu potřebujeme vlastnosti, když vlastnost je Public a nedělá nic jiného, než že čte a nastavuje Private proměnnou. Nestačilo by udělat proměnnou Public a na vlastnost se vykašlat?
Samozřejmě že stačilo, nicméně v praxi se to téměř nikdy nedělá. Nikdy totiž nevíte, kdy budete potřebovat před změnou hodnoty přidat nějakou kontrolu. Představme si, že bude naše třída nějaká schránka a chceme si u ní pamatovat počet položek, kolik se do ní vejde. Určitě nechceme, aby do proměnné šlo přiřadit záporné číslo, protože to prostě nedává smysl. Proto je vždy lepší kolem proměnné udělat vlastnost, kde v metodě Set či Get můžeme nějaké podmínky ověřit a nějak zareagovat (například přiřadit nulu, nebo vyvolat výjimku a přerušit tak nějakou akci).
Private _pocet As Integer
Public Property Pocet() As Integer
Get
Return _pocet
End Get
Set(ByVal value As Integer)
If value >= 0 Then
_pocet = value
Else
_pocet = 0
End If
End Set
End Property
Obecná zvyklost je dávat vlastnosti všude a Public proměnné až na naprosté minimum případů, kdy je to opodstatněné, nikdy nepoužívat, nikdy totiž nevíte, kdy při čtení nebo přiřazování budete chtít eliminovat možnost nějaké chyby a nějakou logiku tam přidat. Díky vlastnostem můžete zajistit, že vám zvenku nikdo nenadělá nějakou škodu a nezmění něco tak, jak nemá, čímž by mohl narušit funkčnost celé aplikace.
Praktický příklad
Vytvořte si nový projekt typu Console Application. Dnes nebudeme používat žádná okénka, to by nás jenom pletlo. Vytvořil se nám nový soubor, ve kterém je procedura Main. V konzolové aplikaci se při jejím spuštění spustí procedura Main, provede se to, co je uvnitř, a když se dorazí na konec procedury, aplikace se ukončí. Co my nyní uděláme? Nadeklarujeme si třídu.
Přidejte si do projektu novou třídu, pojmenujte ji Zamestnanec.
Visual Studio nám do souboru už připravilo základní kostru, dovnitř deklarací tedy nakopírujte vnitřek třídy:
Public Class Zamestnanec
Private _jmeno As String
''' <summary>
''' Jméno zaměstnance
''' </summary>
Public Property Jmeno() As String
Get
Return _jmeno
End Get
Set(ByVal value As String)
_jmeno = value
End Set
End Property
Private _vek As Integer
''' <summary>
''' Věk zaměstnance
''' </summary>
Public Property Vek() As Integer
Get
Return _vek
End Get
Set(ByVal value As Integer)
_vek = value
End Set
End Property
Private _email As String
''' <summary>
''' E-mail zaměstnance
''' </summary>
Public Property Email() As String
Get
Return _email
End Get
Set(ByVal value As String)
_email = value
End Set
End Property
''' <summary>
''' Vygeneruje vizitku zaměstnance
''' </summary>
Public Sub Vizitka()
End Sub
End Class
Tím jsme vytvořili třídu Zamestnanec, které jsme dali vlastnosti Jmeno, Vek a Email. Dále tato třída má metodu Vizitka, kterou můžeme volat zvenčí. Nyní napíšeme samotné generování vizitky. Napsal jsem to možná přehnaně složitě, ale alespoň si vyzkoušíte pořádně dělit problém na podúlohy. Nahraďte proceduru Vizitka tímto kódem:
''' <summary>
''' Vytiskne vizitku
''' </summary>
Public Sub Vizitka()
VizitkaOkraj() 'horní okraj
VizitkaObsah() 'vnitřek
VizitkaOkraj() 'dolní okraj
Console.WriteLine() 'odřádkovat
End Sub
Private Sub VizitkaOkraj()
Console.WriteLine("**************************************************")
End Sub
Private Sub VizitkaRadek(ByVal text As String)
Console.Write("* ") 'levý okraj
If text.Length > 46 Then text = text.Substring(0, 43) & "..." 'text vizitky (moc dlouhý oříznout)
Console.Write(text)
Console.CursorLeft = 49 'pravý okraj
Console.WriteLine("*")
End Sub
''' <summary>
''' Vytiskne obsah vizitky
''' </summary>
Private Sub VizitkaObsah()
VizitkaRadek("Jméno: " & Me.Jmeno)
VizitkaRadek("Věk: " & Me.Vek)
VizitkaRadek("E-mail: " & Me.Email)
End Sub
Co naše procedura Vizitka dělá? Nejdřív nakreslí horní okraj vizitky, pak vypíše samotný obsah vizitky, pak nakreslí dolní okraj a odřádkuje. Vykreslování horního a dolního okraje je jednoduché, v metodě VizitkaObsah zavoláme třikrát VizitkaRadek a předáme jí to, co chceme vypsat. Tato procedura před samotný výpis nakreslí levý okraj, pak vypíše text (pokud by se tam nevešel, tak ho ořízne a na konec přidá tři tečky), a pak dokreslí pravý okraj. To je celé.
Zdě opět používáme modifikátory přístupu - zvenčí je možné zavolat pouze metodu Vizitka, nemůžeme nakreslit samostatně například její okraj.
Všimněte si dále, jak získáme hodnotu nějaké vlastnosti. Me.vlastnost přistupuje k vlastnostem aktuální instance objektu. Vlastně bychom tam ani nemuseli psát to Me., stačil by jen název vlastnosti, ale je lepší jej zdůraznit, aby bylo hned jasné, odkud se to bere.
Jak naši třídu použít a zobrazovat vizitky?
Přepněte se nyní do prvního souboru Module1.vb, kde máme proceduru Main, do níž musíme napsat hlavní program. Právě jsme si napsali třídu Zamestnanec a budeme tedy vytvářet její instance. Je důležité si uvědomit rozdíl mezi třídou a objektem - třída je jen jedna, je to jenom šablona, fyzicky nikde neexistuje, je to prostě vzor. Objekt je už daný exemplář, určité místo v paměti, které si pamatuje všechny své proměnné, a můžeme na něm používat metody a vlastnosti, které jsou definované uvnitř třídy. Každá instance objektu je nezávislá na ostatních instancích, můžeme si tedy vytvořit třeba 30 zaměstnanců a každý bude mít jiné jméno, jiný věk a jiný e-mail. Jak na to? Zkopírujte do procedury Main tento kód:
Sub Main()
Dim z1 As Zamestnanec 'proměnná z1 typu Zamestnanec
z1 = New Zamestnanec() 'tady vytváříme novou instanci třídy, vzniká nový objekt
z1.Jmeno = "Lubomír Zatrsatr" 'přistupujeme k vlastnostem objektu
z1.Vek = 24
z1.Email = "[email protected]"
Dim z2 As New Zamestnanec() 'zkrácené vytvoření objektu
z2.Jmeno = "Jožin z Bažin" 'přistupujeme k vlastnostem objektu
z2.Vek = 150
z2.Email = "[email protected]"
'vypíšeme vizitky obou zaměstnanců, každá bude jiná, protože každý objekt má jiné hodnoty svých vlastností
z1.Vizitka()
z2.Vizitka()
Console.ReadKey()
End Sub
Všimněte si, že když ve Visual Studiu napíšete z1, objeví se vám v nabídce IntelliSense jenom metody a vlastnosti, které jsou v daném kontextu vidět, neukáže vám žádné Private členy.
''' <summary>
''' Věk zaměstnance
''' </summary>
Public Property Vek() As Integer
Get
Return _vek
End Get
Set(ByVal value As Integer)
_vek = value
End Set
End Property
Dále si všimněte, že pokud použijete nad deklarací vlastnosti nebo metody speciální XML komentáře, ukáže vám je i Visual Studio v této nabídce. To je obrovská výhoda při práci v týmu, velice rychle zjistíte, co dělá metoda, kterou napsal někdo jiný, můžete ji jen použít a nestaráte se o to, jak funguje vevnitř.
Hodnotové a referenční typy
V .NET Frameworku máme tzv. hodnotové a referenční typy (Value a Reference types). Hodnotové datové typy jsou například Integer, Long, Short, Byte, Double, Single, Boolean a dále sem patří struktury, které jsme již používali, ale raději je připomenu:
Public Structure Zaznam
Dim id As Integer
Dim cena As Double
End Structure
Proč hodnotové? Protože proměnná si pamatuje přímo jejich hodnotu a kdykoliv ji přiřazujeme, předává se celá hodnota (kopíruje se paměť). Pozor na kopírování velkých struktur, opravdu se kopírují všechna data v celé struktuře.
Asi všichni víte, co bude na konci v proměnných a a b:
Dim a As Integer = 32
Dim b As Integer = 16
a = b
b += 2
Console.Write(a & " " & b)
V a je 32, v b je 16. Přiřazením hodnoty b do a způsobíme, že v obou proměnných bude 16. Když pak b zvětšíme o 2, bude v b 18. To je naprosto přirozené a každý by to tak čekal.
Referenční datové typy se ale chovají trochu jinak. Proměnná jako taková nenese přímo samotnou hodnotu, ale jenom adresu místa v paměti, kde se hodnota nachází. Říkáme, že proměnná obsahuje referenci (pointer, ukazatel). Mezi referenční datové typy patří např. String, pole a objekty. Při vytváření se pole, řetězec nebo objekt neuloží do proměnné, ale .NET jej uloží na tzv. haldu (místo, které je v paměti pro objěkty vyhrazeno) a do proměnné uloží odkaz na místo, kde se v haldě hodnota nachází. Vezměme si tento kód:
'vytvoření objektů z1 a z2
Dim z1 As Zamestnanec 'proměnná z1 typu Zamestnanec
z1 = New Zamestnanec() 'tady vytváříme novou instanci třídy, vzniká nový objekt
z1.Jmeno = "Lubomír Zatrsatr" 'přistupujeme k vlastnostem objektu
z1.Vek = 24
z1.Email = "[email protected]"
Dim z2 As New Zamestnanec() 'zkrácené vytvoření objektu
z2.Jmeno = "Jožin z Bažin" 'přistupujeme k vlastnostem objektu
z2.Vek = 150
z2.Email = "[email protected]"
'******* nyní přiřadíme z2 do z1
z1 = z2
'vypíšeme vizitky obou zaměstnanců, každá bude jiná, protože každý objekt má jiné hodnoty svých vlastností
z1.Vizitka()
z2.Vizitka()
Console.ReadKey()
V tomto kódu děláme prakticky to samé, akorát se složitějšími datovými typy. Vytvořili jsme si a nějak nastavili proměnné z1 a z2 a pak jsme přiřadili z2 do z1. Na konzoli se nám vypíše dvakrát vizitka druhého zaměstnance. Ke druhému zaměstnanci se již nikdy nedostaneme, protože v žádné proměnné si už nedržíme referenci na jeho objekt. Je to jako s číslem 32, které zmizelo, protože jsme jej přepsali. Tady jsme ale přepsali jenom referenci, tedy čtyřbajtovou adresu (pro rýpaly osmibajtovou na 64bitových systémech) na objekt na haldě.
Třicetdvojku jsme v předchozím případě přepsali v paměti jinými daty, ale tady nám na haldě zůstal objekt, ke kterému se mimo jiné už nikdy nedostaneme. Nebudou se nám zahozené objekty na haldě hromadit? No jistě že budou. .NET Framework má na to takzvaný Garbage Collector (česky něco jako popelář, sběrač odpadu). Ten se jednou za čas spustí, najde na haldě nepoužívané objekty, a ty odstraní. Je to poměrně složitý proces, .NET si u každého objektu pamatuje, kolik referencí na něj existuje, a při každém přiřazení si toto číslo aktualizuje. Pokud se číslo stane nulou, znamená to, že na objekt už nikdo neodkazuje, a tím pádem se může vyhodit. Je nutné si uvědomit, že v jiných jazycích, např. v C++, žádný GC není, tam se z2 = z1 napsat nesmí, objekt by z paměti nikdo nikdy neodstranil a pokud byste to dělali ve velkém, za chvíli by aplikace spotřebovala všechnu paměť. My ale GC máme, přesto je dobré brát v potaz to, že prakticky nemůžeme ovlivnit, kdy se GC bude spouštět, a hlavně naše aplikace se při činnosti GC pozastaví a nemáme nad ničím kontrolu. GC se také většinou spouští až když už paměti opravdu moc nezbývá, rozhodně neběží pořád a nesbírá objekty průběžně.
Co jsme ale nyní udělali, když jsme přiřadili z2 do z1? Obě dvě proměnné nyní ukazují na ten stejný objekt, na ta samá fyzická data. Co to znamená? Že když změníme nějakou vlastnost nebo něco v z1, změní se to automaticky i v z2, obě proměnné odkazují na stejnou paměť. Přidáte-li do předchozí ukázky poslední řádek, změní se věk u obou vizitek.
'******* nyní přiřadíme z2 do z1
z1 = z2
z1.Vek = 35
Předávání parametrů metodám hodnotou a referencí
Velmi podobné chování můžeme potřebovat, když předáváme parametry metodám. Před deklaraci každého parametru nám VB.NET sám přidá kouzelné slovíčko ByVal, které říká, že parametr se předává hodnotou. Pokud toto slovíčko změníme na ByRef, bude se proměnná předávat referencí. Tohle nemá nic společného s hodnotovými a referenčními typy přímo, ale princip je stejný.
Sub Main()
Dim i As Integer = 3 'nastavíme i
Zmen(i) 'zavoláme funkci, předáme jí HODNOTU i
Console.WriteLine(i) 'i bude pořád 3, původní proměnná se nezměnila
End Sub
Sub Zmen(ByVal i As Integer)
i = 5
End Sub
V metodě Zmen je parametr i předáván hodnotou, to znamená, že hodnota proměnné se zkopíruje a nad touto kopií se provede kód metody Zmen. Po návratu do rodičovské metody Main je v i její původní hodnota, změnila se jenom kopie. Pokud změníme ByVal na ByRef, změní se i i v rodičovské metodě Main, protože jsme nepředávali hodnotu proměnné, ale referenci, opět jenom adresu na místo, kde je hodnota uložená (přestože Integer je hodnotový typ).
Stejně to funguje i s typy referenčními, předávání parametrů se chová jako přiřazování. Hodnota, která se zde ale předává, je reference na objekt na haldě, takže když předáme objekt pomocí ByVal, změny na vlastnostech a proměnných objektu se projeví i ven. Pokud ale do parametru přiřadíme jiný objekt, v rodičovské proceduře se nic nezmění, protože jsme změnili jenom kopii reference na objekt. Předáme-li objekt ByRef, přiřazení se projeví i venku. Lépe je to vidět z kódu:
Sub Main()
'vytvoření objektů z1 a z2
Dim z1 As Zamestnanec 'proměnná z1 typu Zamestnanec
z1 = New Zamestnanec() 'tady vytváříme novou instanci třídy, vzniká nový objekt
z1.Jmeno = "Lubomír Zatrsatr" 'přistupujeme k vlastnostem objektu
z1.Vek = 24
z1.Email = "[email protected]"
Dim z2 As New Zamestnanec() 'zkrácené vytvoření objektu
z2.Jmeno = "Jožin z Bažin" 'přistupujeme k vlastnostem objektu
z2.Vek = 150
z2.Email = "[email protected]"
Zmen(z1, z2) 'zavoláme funkci, předáme jí HODNOTY proměnných z1 a z2, tedy ukazatele na haldu
z1.Vizitka() 'vlastnost objektu se změnila
End Sub
Sub Zmen(ByVal z1 As Zamestnanec, ByVal z2 As Zamestnanec)
z1.Vek = 13 'změna vlastnosti objektu se projeví ven
z1 = z2 'přiřazení do samotné proměnné ale ne, máme ByVal
End Sub
Na tomto příkladu se tedy změní věk, ale v z1 v metodě Main bude pořád první zaměstnanec, změnila se jenom kopie. Předáme-li první parametry ByRef, změna se projeví a zavoláním z1.Vizitka() by se vypsala vizitka druhého zaměstnance.
Sub Zmen(ByRef z1 As Zamestnanec, ByVal z2 As Zamestnanec)
z1.Vek = 13 'změna vlastnosti objektu se projeví ven
z1 = z2 'tady se změna projeví i v rodičovské metodě, máme ByRef
End Sub
ByVal tedy předává funkci kopii toho, co je uloženo v proměnné (hodnota u hodnotových typů nebo reference u referenčních typů), ByRef předá referenci na hodnotu v proměnné.
Konstruktor
Občas potřebujeme při vytváření instance objektu provést ještě nějaký další kód, typicky nastavit objektu výchozí hodnoty vlastností. Proto může mít třída svůj konstruktor. Konstruktor se zapisuje jako normální metoda, která má název New. Konstruktor může mít i parametry, které při inicializaci může samozřejmě použít. Parametry konstruktoru předáváme při vytváření instance objektu. Tento kód přidejte do třídy Zamestnanec:
Public Sub New(ByVal jmeno As String, ByVal vek As String, ByVal email As String)
Me.Jmeno = jmeno
Me.Vek = vek
Me.Email = email
Console.WriteLine("Objekt vytvořen, jméno " & jmeno)
End Sub
Všimněte si, že zde je při přístupu do vlastností nutné použít slovo Me, protože pokud máme dva elementy se stejným názvem, VB.NET si vždycky vybere ten s nižším kontextem (viditelností). Tady kdybychom napsali jmeno = jmeno, nic by se nestalo, protože VB.NET si vždycky vybere dříve lokální proměnnou metody, než vlastnost třídy. Pokud řekneme Me.Jmeno, je jasné, že chceme jmeno, které patří objektu, takže v tomto případě vezme vlastnost. Při vytváření objektu ještě vypíšeme na konzoli informaci, že se objekt vytvořil.
Proceduru Main změňte takto:
Sub Main()
'vytvoření objektů z1 a z2
Dim z1 As New Zamestnanec("Lubomír Zatrsatr", 24, "[email protected]")
Dim z2 As New Zamestnanec("Jožin z Bažin", 150, "[email protected]")
'vypíšeme vizitky obou zaměstnanců, každá bude jiná, protože každý objekt má jiné hodnoty svých vlastností
z1.Vizitka()
z2.Vizitka()
Console.ReadKey()
End Sub
Bude to dělat úplně to samé, co první verze metody Main, je to jen kratší a vypíše to navíc dvě hlášky o vytvoření objektů tak, jak je to v kontruktoru. Hodnoty, které chceme přiřadit do jednotlivých vlastností, totiž předáváme jako parametry konstruktoru, a ten už je nastaví do svých vlastností sám.
Jen dodávám, že konstruktorů si můžete napsat více, s různými počty a typy parametrů. Pokud třídě nenapíšete konstruktor žádný, dá jí .NET Framework výchozí konstruktor bez parametrů, který neudělá nic navíc. Konstruktor nemusí být Public, v některých situacích se vyplatí mít například Private nebo Protected konstruktory, ale o tom až někdy příště.
Pokračování příště...
Chápu, že si asi říkáte, co tady pořád valím s těmi objekty, že to je na nic, že když budete chtít vypisovat vizitky do konzole, že se to dá napsat na pár řádků a nemusí se kvůli tomu dělat třídy. Příště náš příklad použijeme a vysvětlíme si, co je to dědičnost, a jak ji používat. Tam teprve uvidíme, proč jsem navrhnul tisk vizitek takto složitě a dělal na každou část vizitky metodu, přestože jsem to všechno mohl plácnout do Vizitka.
Pokud stále nechápete, jak objekty fungují, nezoufejte a klidně se ptejte v diskusi. K čemu objekty jsou, k tomu se ještě dopracujeme, zatím je s tím pětkrát víc psaní a desetkrát víc nudné teorie, ale u větších projektů se objekty opravdu vyplatí.