V minulém díle jsme si povídali o datových typech, vysvětlili jsme si, co jsou to typy hodnotové a referenční. Dnes se až na pár výjimek budeme bavit o typech referenčních, kterýmižto jsou především třídy.
Nebudeme zde trávit čas vysvětlováním, co je to třída, to už byste měli vědět a znát. Pod tomu tak není, navštivte článek Úvod do objektově orientovaného programování, který se nám zde na webu válí.
Chceme-li deklarovat nějakou vlastní třídu, napíšeme toto. Vše, co se týká dané třídy, píšeme dovnitř.
Class MojeTrida
End Class
class MojeTrida
{
}
Kde se deklarace třídy může vyskytovat?
- uvnitř jmenného prostoru (v bloku namespace) - pak je třída zařazena do tohoto jmenného prostoru
- mimo jmenný prostor - pak je třída zařazena do výchozího jmenného prostoru (nastavuje se ve vlastnostech projektu)
- uvnitř třídy - třída se vztahuje k rodičovské třídě, její celý název zahrnuje namespace a název rodičovské třídy
Konvence
V .NETu je jedno, v jakém souboru se která deklarace vyskytuje. Např. v Javě nebo třeba v ActionScriptu toto neplatí, třída tam musí být definována v souboru se stejným názvem (musí být dodržena i jejich velikost).
Jazykům C# i VB.NET je to úplně jedno, můžete mít v jednom souboru více tříd, od verze 2.0 .NET Frameworku je povoleno i jednu třídu rozdělit do více souborů (tzv. partial classes). To se hodí v případě, že část třídy generuje např. Visual Studio, zatímco druhou část píše člověk a není vhodné, aby psal do stejného souboru, jelikož by mu to vývojové prostředí mohlo přepsat. Tento přístup se používá velmi často. Pro vytvoření parciální třídy stačí před slovo class napsat klíčové slovo partial (ve VB.NET i v C#).
Vnořené třídy (třída ve třídě) se obecně moc nepoužívají, ale to je spíš záležitost zvyku a osobních preferencí.
Vytváření instancí tříd
Novou instanci třídy můžeme vytvořit použitím klíčového slova new.
Dim instance As MojeTrida = New MojeTrida() 'plný zápis
Dim instance As New MojeTrida() 'zkrácený zápis
MojeTrida instance = new MojeTrida();
Co se vlastně stane? Založí se nová proměnná typu MojeTrida a ihned se do ní přiřadí instance třídy MojeTrida. Volání new MojeTrida() zavolá výchozí konstruktor pro danou třídu a vrátí nově vytvořenou instanci. Při vytvoření instance třídy jsou všechny proměnné uvnitř třídy automaticky zinicializovány na své výchozí hodnoty (číselné typy na 0, boolean na false, referenční typy na null). To je rozdíl oproti lokálním proměnným v metodách, ty jsou ve VB.NET inicializované (kvůli zpětné kompatibilitě), v C# ne a kompilátor vám je ani nedovolí použít, dokud do nich něco nepřiřadíte.
Konstruktor
Konstruktor je speciální metoda, která slouží pro inicializaci třídy. Každá třída v .NETu má svůj výchozí konstruktor. Pokud v deklaraci třídy sami neuvedeme žádný konstruktor, bude výchozí konstruktoru (bez parametrů) kompilátorem vygenerován za nás a “nebude dělat nic” (je to jako kdybychom ho nadeklarovali a nic dovnitř nenapsali). Pokud při založení instance třídy chceme dělat něco my, musíme výchozí konstruktor specifikovat.
Konstruktor se deklaruje v obou jazycích značně odlišně. Ve VB.NET to je metoda, která se jmenuje New, v C# je to metoda, která se jmenuje stejně jako třída a nepíšeme u ní návratový typ (ani void ani cokoliv jiného).
Class MojeTrida
Public Sub New()
End Sub
End Class
class MojeTrida
{
public MojeTrida()
{
}
}
Modifikátory přístupnosti
V tomto i v minulém díle jsem používal slova public, private atd., ale blíže jsme si nevysvětlili, co vlastně znamenají. Zde u konstruktoru máme klíčové slovo public, které říká, že konstruktor třídy je možné volat zvenčí (z metody mimo tuto třídu). Jak to vlastně je?
VB.NET |
C# |
Popis |
Public |
public |
Přístupné odkudkoliv |
Protected Friend |
protected internal |
Přístupné odkudkoliv v rámci assembly
Přístupné z potomků této třídy |
Friend |
internal |
Přístupné odkudkoliv v rámci assembly |
Protected |
protected |
Přístupné z potomků této třídy |
Private |
private |
Přístupné jen z aktuální třídy |
Na obrázku je vše vidět. Fialové krabičky jsou třídy, ze kterých chceme volat zelenou metodu. Vidíme, jaká oprávnění potřebujeme, abychom mohli třídu zavolat. Je nutno podotknout, že pokud stačí přístupnost private a máme public (tedy něco, co je v tabulce na vyšším řádku), ničemu to nevadí. Horší by to bylo obráceně.
Pokud voláme metodu z té samé třídy, stačí nám, aby metoda byla private. Pokud voláme metodu ze třídy B, která dědí (je potomek) třídy A, potřebujeme alespoň protected. Pokud voláme ze třídy, která k A nemá žádný vztah, ale je ve stejné assembly, pak nám stačí Friend / internal. Pokud voláme metodu z třídy v jiné assembly, která ale dědí třídu A, stačí nám Protected Friend / protected internal. Pokud jsme v jiné assembly a nemáme s třídou A žádný vztah (nejsme ani syn, ani strýček, ani babička kočky její sestry), potřebujeme přístup public.
Ve většině případů alespoň ze začátku vystačíme s public, protected a private. Relativně často se také používá Friend / internal, zvlášť když píšete nějakou knihovnu a máte v ní pomocné třídy, které potřebujete všude, ale nechcete je publikovat ven, poslední variantu Protected Friend / protected internal jsem viděl v praxi jen párkrát a kdyby tam bylo public, vůbec nic by se nestalo. Ne, že by byla k ničemu, ale moc často ji nepotkáte.
Zpět ke konstruktorům
Našemu výchozímu konstruktoru jsme dali modifikátor public, tím pádem bude veřejně dostupný a bude tedy možné vytvářet instance této třídy kdekoliv. Pokud bychom mu neuvedli nic, byl by private, proměnné a metody uvnitř třídy jsou ve výchozím stavu private. Nebylo by možné vytvořit instanci jinde než v nějaké metodě, která je v této třídě. To se občas využívá, především k implementaci některých návrhových vzorů, např. singleton. Ve většině případů je ale konstruktor veřejný.
Kromě výchozího konstruktoru můžeme mít také konstruktory jiné, takové, které mají parametry. To se používá velmi často - některé třídy v mnoha aplikacích nemají žádnou promyšlenou funkcionalitu a slouží pouze jako reprezentace dat (např. třída Zamestnanec může sloužit jen k reprezentaci záznamu z databáze, bude mít vlastnosti Jmeno, Prijmeni atd.). Hodí se mít možnost vytvořit takovou třídu v kódu na jeden řádek.
To můžeme zařídit pomocí konstruktoru s parametry.
Class Zamestnanec
Dim jmeno, prijmeni As String
Public Sub New(ByVal jmeno As String, ByVal prijmeni As String)
Me.jmeno = jmeno
Me.prijmeni = prijmeni
End Sub
End Class
class Zamestnanec
{
string jmeno, prijmeni;
public Zamestnanec(string jmeno, string prijmeni)
{
this.jmeno = jmeno;
this.prijmeni = prijmeni;
}
}
Tohle je velmi typické použití konstruktoru s parametry a můžeme jej potkat velmi často. Předají se mu parametry, jejichž hodnoty pouze nastavíme do členských položek třídy. Všimněte si klíčového slova Me resp. this, kterým ukazujeme na aktuální instanci třídy a rozlišujeme mezi parametrem metody a proměnnou třídy. Kdyby se proměnné třídy jmenovaly jinak než parametry konstruktoru, nemuseli bychom toto klíčové slovo použít a jít přímo. Pokud se ale jmenují stejně, bez použití Me resp. this se bere ta proměnná, která je “blíž” (nejprve lokální proměnné, pak proměnné uvnitř třídy, kde jsme).
Vytvoření instance pak bude vypadat třeba takto:
Dim zam As New Zamestnanec("Tomáš", "Herceg")
Zamestnanec zam = new Zamestnanec("Tomáš", "Herceg");
Volání jiných konstruktorů
Často potřebujeme konstruktory přetěžovat - podle počtu a typu předaných parametrů rozlišit mezi příslušnými konstruktory. Toto funguje samozřejmě při volání jakékoliv metody, můžeme udělat více metod se stejným názvem, které se budou lišit jen počtem a typy argumentů. Konstruktor je speciální metoda, ale funguje to úplně stejně. O tom, jak přetěžování metod přesně funguje a jaká jsou tam pravidla, si povíme v některém z příštích dílů.
Takto například uděláme možnost volitelně zaměstnanci předat jeho věk. Protože se jménem a příjmením budeme dělat to samé, co o konstruktor výše, bylo by škoda opisovat ten kód znovu. My ale můžeme z jednoho konstruktoru zavolat konstruktor jiný a předat mu parametry. V každém jazyce se to dělá trochu jinak, princip je ale stejný:
Class Zamestnanec
Dim jmeno, prijmeni As String
Dim vek As Integer
Public Sub New(ByVal jmeno As String, ByVal prijmeni As String)
Me.jmeno = jmeno
Me.prijmeni = prijmeni
End Sub
Public Sub New(ByVal jmeno As String, ByVal prijmeni As String, ByVal vek As Integer)
MyClass.New(jmeno, prijmeni) 'musí být na začátku konstruktoru, jinak se kompilátor vztekne!
Me.vek = vek
End Sub
End Class
class Zamestnanec
{
string jmeno, prijmeni;
int vek;
public Zamestnanec(string jmeno, string prijmeni)
{
this.jmeno = jmeno;
this.prijmeni = prijmeni;
}
public Zamestnanec(string jmeno, string prijmeni, int vek)
: this(jmeno, prijmeni)
{
this.vek = vek;
}
}
Ve VB.NET jiný konstruktor zavoláme přes klíčové slovo MyClass, v C# musíme volání zapsat již do deklarace metody za dvojtečku.
Poznámka: Pokud nyní zkusíte vytvořit instanci třídy Zamestnanec bez parametru, nepůjde to. Výchozí konstruktor třída nemá, kompilátor jej vygeneruje pouze v případě, že neuvedeme žádný konstruktor. Zde jsme ale konstruktory uvedli, výchozí mít třída nebude.
Metody uvnitř tříd
Jak se deklarují metody jsme již viděli v předminulém dílu a není k tomu moc co dodat. Každá metoda musí být uvnitř nějaké třídy. Jen pro připomenutí ukážeme syntaxi:
Public Sub Vypis()
Console.WriteLine("Jméno: " & jmeno)
Console.WriteLine("Příjmení: " & prijmeni)
End Sub
public void Vypis()
{
Console.WriteLine("Jméno: " + jmeno);
Console.WriteLine("Příjmení: " + prijmeni);
}
Tato metoda vypíše na konzoli informace o zaměstnanci, tedy jeho jméno a příjmení. Nic světoborného.
Statické versus instanční
Nyní je nutné vysvětlit si pojmy statické a instanční. Abychom mohli zavolat naši metodu Vypis, potřebujeme k tomu instanci třídy Zamestnanec. Metoda se vztahuje ke konkrétní instanci - je tedy instanční. Metodu voláme vždy na proměnné (typu Zamestnanec).
Naproti tomu statické metody jsou sice deklarovány uvnitř třídy, ale mají něco společného s třídou a ne s konkrétní instancí. Velmi často slouží jako metoda, která dostane parametry, a vrátí již hotovou instanci třídy, která je nějak předpřipravená. Hodí se to v případech, kdy vytvoření instance třídy není úplně triviální záležitost a je k tomu třeba víc operací. Samozřejmě možných využití je více.
Statické metody voláme ne přes proměnnou (přes konkrétní instanci), ale přes název třídy. Pokud chceme metodu nadeklarovat jako statickou, přidáme do názvu slovo static v C# a Shared ve VB.NET (pozor! VB.NET má slovo Static také, ale to slouží k jinému účelu).
Public Class A
Public Sub Instancni()
End Sub
Public Shared Sub Staticka()
End Sub
End Class
Dim _a As New A()
' přístup přes "proměnnou"
_a.Instancni()
' přístup přes "datový typ"
A.Staticka()
public class A
{
public void Instancni() { }
public static void Staticka() { }
}
A a = new A();
// přístup přes "proměnnou"
a.Instancni();
// přístup přes "datový typ"
A.Staticka();
Instanční nebo statické může být prakticky vše, co je uvnitř třídy - proměnné, vlastnosti, metody atd. Z instančních metod můžeme přistupovat ke statickým proměnným, opačně to jde jen pokud máme nějakou instanci třídy. V instanční metodě můžeme napsat rovnou volání Staticka() tak, jak leží a běží, a bude to fungovat. Ve statické metodě ale Instancni() napsat nemůžeme, kompilátor nám vynadá, že přistupujeme k instanční položce.
Proměnné uvnitř tříd
Pokud máte uvnitř třídy proměnné, měly by být nejvýše protected. Žádná proměnná by neměla být viditelná mimo třídu, ve které se používá. Má to mnoho důvodů a je dobré to dodržovat.
Asi nejhlavnějším důvodem je možnost narušení konzistence stavu objektu. Máme například třídu, která reprezentuje seznam něčeho a celkem logicky si v nějaké proměnné pamatuje počet položek. Někdo, kdo tuto třídu používá, potřebuje venku počet položek číst, takže vcelku přímočarým řešením je udělat proměnnou Count veřejnou a je to.
Pak ale přijde někdo, kdo neví, jak třída uvnitř funguje (což je u větších projektů naprosto běžná věc, každý píše svoji část a ostatní lidé hotové třídy jen používají). Řekne si třeba “chci seznam pro 15 položek, aha jasně, tak nastavím Count na 15 a je to”. To je ještě relativně v pořádku, program spadne a nebude fungovat. Horší to ale bude, až někdo udělá podobnou chybu, vynuluje nějakou veřejně dostupnou proměnnou, což může to mít dalekosáhlé důsledky. Například to někomu to smaže výplatu a on vás pak přijde defenestrovat, což tak trochu dovede zkazit odpoledne.
Vlastnosti (properties)
Elegantním řešením, jak zpřístupnit proměnnou ven ze třídy (protože to je dost často potřeba) a nedělat kvůli tomu separátní metodu, je použít vlastnosti. Vlastnost je povětšinou dvojice metod (getter a setter), které se navenek chovají jako proměnná (a v drtivé většině případů se používají i k řízení přístupu k proměnné). Je nutné si ale uvědomit, že vlastnosti nemusí být vázány na konkrétní proměnnou, přestože to tak v drtivé většině případů bývá.
Zde je první typický model použití vlastnosti - vlastnost řídí přístup k proměnné a zpřístupňuje ji ven ze třídy. Možná si říkáte, že je to zbytečné, a akorát to zpomaluje (ve skutečnosti ne, JIT kompilátor to vyoptimalizuje - většinou). Ve skutečnosti to je velmi užitečné. Kdykoliv v budoucnu bude potřeba přidat do setteru nějaké validace (aby tam někdo nedal hodnotu null resp. Nothing), anebo v getteru ošetřit nějaké speciální případy (např. lazy inicializace viz dále). Pokud byste přistupovali k proměnné, tak snadno se to nepředělá, pokud máte vlastnost, velmi jednoduše do ní můžete příslušný kód doplnit.
Public Property Jmeno() As String
Get
Return _jmeno
End Get
Set(ByVal value As String)
_jmeno = value
End Set
End Property
public string Jmeno
{
get { return jmeno; }
set { jmeno = value; }
}
Pokud chcete do vlastnosti něco přiřadit, zavolá se setter a provede se kód v něm. Pokud chcete hodnotu vlastnosti číst, zavolá se naopak getter. S vlastnostmi pracujeme úplně stejně, jako kdyby to byla proměnná ve třídě.
Všimněte si, že ve VB.NET musíme před název proměnné dát znak podtržítko. Resp. nemusíme, můžeme tu proměnnou pojmenovat úplně jinak, ale je to taková konvence a je velmi dobré ji dodržovat, abyste byli kompatibilní se zbytkem světa. Ve VB.NET totiž nezáleží na velikosti písmen a jmeno je to samé co Jmeno (Visual Studio samo písmena zvětší a zmenší tak, aby to všude vypadalo stejně).
Naproti tomu v C# bývá zvyklost pojmenovávat proměnnou stejně jako vlastnost, akorát vlastnost začíná velkým písmenem. Přestože to jde, neměli byste nikdy dovnitř třídy dávat veřejné názvy lišící se jen velikostí písmen, protože pak je tato třída prakticky nepoužitelná z jiných jazyků, které na velikost písmen z vysoka kašlou. To ale není tento případ, ven je vidět jen vlastnost, proměnná ne.
Druhým velmi častým případem, kdy se vlastnosti používají, je tzv. lazy inicializace. Hodí se to například v případě, kdy potřebujete načíst třeba konfiguraci aplikace, což může trvat poněkud déle, a nechcete tím zdržovat start aplikace, načtete ji až v momentě, kdy ji opravdu použijete, třeba v okamžiku zobrazování okna s nastaveními. Navíc ji chcete načíst jen jednou a ne pokaždé.
Public Class Utils
Private Shared _config As Object
Public Shared ReadOnly Property Configuration() As Object
Get
If _config Is Nothing Then InitConfiguration() 'pokud ještě konfigurace není načtená, načíst ji
Return _config
End Get
End Property
...
End Class
public class Utils
{
private static object config;
public static object Configuration
{
get
{
if (config == null) InitConfiguration(); // pokud ještě konfigurace není načtená, načíst ji
return config;
}
}
...
}
Naše vlastnost je nyní readonly. Ve VB.NET se to zařídí tak, že před klíčové slovo Property přidáte slovo Readonly a neuvedete sekci Set. V C# stačí neuvést sekci set.
Jak se to bude používat? Jednoduše, je to statické, takže v aplikaci napíšete Utils.Configuration.něco atd. Při prvním použití se konfigurace načte, při dalších se již načtená konfigurace rovnou vrátí. Podotýkám, že metoda InitConfiguration musí být také statická, definovaná ve třídě Utils a načtenou konfiguraci musí uložit do proměnné config resp. _config.
Existují dokonce i writeonly properties, které nemají getter, jen setter. K čemu jsou, to mi ještě nikdy nikdo nevysvětlil, v životě jsem je neviděl.
Novinky v .NET Frameworku 3.5 a 4.0
Malá odbočka, která se ale vyplatí. V .NET Frameworku veze 3.5, kde se objevila nová verze jazyka C#. Ta přinesla tzv. auto-implemented properties, což je zkrácená syntaxe pro zápis vlastností. Vychází z faktu, že většina vlastností nedělá nic jiného, než zpřístupnění nějaké proměnné (této proměnné se také říká backing field). Proto se umožnilo v C# zkrácení syntaxe zápisu vlastností.
string Jmeno { get; set; }
Jak se to chová? Úplně stejně jako ukázka kódu pod tímto odstavcem, akorát se nemusíme patlat s tou privátní proměnnou. Vygeneruje ji za nás kompilátor. Výhodou je, že z několikařádkového zápisu se stal zápis na jeden řádek, který je přehlednější a v případě, že budeme do vlastnosti potřebovat přidat nějaké validace nebo něco, snadno jej překlopíme do “ukecané” verze. Oba zápisy jsou ekvivalentní (v tom druhém chybí akorát deklarace privátní proměnné jmeno).
public string Jmeno
{
get { return jmeno; }
set { jmeno = value; }
}
Ve VB.NET tato možnost zatím není, bude v .NET Frameworku verze 4.0, který mimo jiné přinese novou verzi jazyka Visual Basic, a to verzi 10. V době psaní tohoto článku je .NET 4.0 ještě v betaverzi, následující ukázka nebude fungovat ve Visual Studiu 2008 a starších verzích.
Public Property Jmeno As String
Co je výhodou, automatické vlastnosti ve VB.NET budou umět na rozdíl od C# počáteční inicializaci. Pokud za daný kód napíšete = “Tomáš”, bude vlastnost automaticky inicializována na příslušnou hodnotu.
Řízení přístupu ke getteru a setteru
Jak v automatické verzi v C#, tak v té rozšířené můžete specifikovat modifikátor přístupnosti buď pro getter, nebo pro setter (ne pro oba najednou; pro ten, pro nějž to neuvedete, se použije modifikátor přístupu celé vlastnosti).
Zvláště u automatických vlastností se hodí velmi často toto nastavení:
Public Property Jmeno As String
Get
Return _jmeno
End Get
Private Set(ByVal value As String)
_jmeno = value
End Set
End Property
string Jmeno { get; private set; }
Tento přístup se velmi často používá v případě, že potřebujete zevnitř třídy vlastnost nastavit, ale ostatní ji mají jít jen pro čtení. Aby to šlo i s automatickými vlastnostmi, je možnost pro setter nastavit oprávnění private, díky kterému zvenku vlastnost změnit nepůjde a zevnitř ano. Ve VB.NET tohle u automatických vlastností pravděpodobně nepůjde. Jak to bude ve finální verzi mi není známo.
Object initializers
Pokud vytváříte nějakou promyšlenou datovou třídu, která má některé vlastnosti nepovinné, pravděpodobně brzy skončíte u toho, že budete mít 10 přetížených konstruktorů, které se budou lišit počtem parametrů. Desátý bude volat devátý, devátý volá osmý atd., přičemž každý má právě o jeden parametr víc než ten předchozí. To není příliš elegantní, proto v C# 3 ve ve VB9 přišly tzv. object initializers.
Použití je velmi jednoduché a je to opět jen zkrácení syntaxe.
'první způsob
zam = New Zamestnanec()
zam.Jmeno = "Tomas"
zam.Prijmeni = "Herceg"
zam.Vek = 21
'druhý způsob - použití object initializers
zam = New Zamestnanec() With {.Jmeno = "Tomas", .Prijmeni = "Herceg", .Vek = 21}
// první způsob
zam = new Zamestnanec();
zam.Jmeno = "Tomas";
zam.Prijmeni = "Herceg";
zam.Vek = 21;
// druhý způsob - použití object initializers
zam = new Zamestnanec() { Jmeno = "Tomas", Prijmeni = "Herceg", Vek = 21 };
První a druhý způsob jsou naprosto ekvivalentní. Jmeno, Prijmeni a Vek musí být vlastnosti, na proměnné to nefunguje! Navíc třída Zamestnanec musí mít v tomto případě výchozí konstruktor (obecně ten, který použijeme při volání před složenou závorkou).
Závěrem
To je pro tento díl vše. Nedostali jsme se k mnoha věcem, například k dědičnosti, rozhraním atd., ale to si vynahradíme někdy příště.