Lekce 10 - Vlastnosti
V předešlém cvičení, Řešené úlohy k 9. lekci OOP v C# .NET, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
V dnešním C# .NET tutoriálu se podíváme na další prvky tříd, které ještě neznáme. Začněme slíbenými vlastnostmi.
Vlastnosti
Velmi často se nám stává, že chceme mít kontrolu nad změnami
nějakého atributu objektu zvenčí. Budeme chtít atribut nastavit jako
read-only nebo reagovat na jeho změny. Založme si nový projekt (název
Vlastnosti
) a vytvořme následující třídu
Student
, která bude reprezentovat studenta v nějakém
informačním systému.
class Student { public string jmeno; public bool muz; public int vek; public bool plnolety; public Student(string jmeno, bool pohlavi, int vek) { this.jmeno = jmeno; this.muz = pohlavi; this.vek = vek; plnolety = true; if (vek < 18) plnolety = false; } public override string ToString() { string jsemPlnolety = "jsem"; if (!plnolety) jsemPlnolety = "nejsem"; string pohlavi = "muž"; if (!muz) pohlavi = "žena"; return String.Format("Jsem {0}, {1}. Je mi {2} let a {3} plnoletý.", jmeno, pohlavi, vek, jsemPlnolety); } }
Třída je velmi jednoduchá, student se nějak jmenuje, je nějakého
pohlaví a má určitý věk. Podle tohoto věku se nastavuje atribut
plnolety
pro pohodlnější vyhodnocování plnoletosti na
různých místech systému. K uložení pohlaví používáme hodnotu
bool
, zda je student muž. Konstruktor dle věku určí, zda je
student plnoletý. Metoda ToString()
je navržena pro potřeby
tutoriálu tak, aby nám vypsala všechny informace. V reálu by vrátila
pravděpodobně jen jméno studenta. Pomocí konstruktoru si nějakého studenta
vytvořme:
{CSHARP_CONSOLE} Student s = new Student("Pavel Hora", true, 20); Console.WriteLine(s); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Student { public string jmeno; public bool muz; public int vek; public bool plnolety; public Student(string jmeno, bool pohlavi, int vek) { this.jmeno = jmeno; this.muz = pohlavi; this.vek = vek; plnolety = true; if (vek < 18) plnolety = false; } public override string ToString() { string jsemPlnolety = "jsem"; if (!plnolety) jsemPlnolety = "nejsem"; string pohlavi = "muž"; if (!muz) pohlavi = "žena"; return String.Format("Jsem {0}, {1}. Je mi {2} let a {3} plnoletý.", jmeno, pohlavi, vek, jsemPlnolety); } } {/CSHARP_OOP}
Výstup:
Konzolová aplikace
Jsem Pavel Hora, muž. Je mi 20 let a jsem plnoletý.
Vše vypadá hezky, ale atributy jsou přístupné jak ke čtení, tak k zápisu. Objekt tedy můžeme rozbít například takto (hovoříme o nekonzistentním vnitřním stavu):
{CSHARP_CONSOLE} Student s = new Student("Pavel Hora", true, 20); s.vek = 15; s.muz = false; Console.WriteLine(s); Console.ReadKey(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Student { public string jmeno; public bool muz; public int vek; public bool plnolety; public Student(string jmeno, bool pohlavi, int vek) { this.jmeno = jmeno; this.muz = pohlavi; this.vek = vek; plnolety = true; if (vek < 18) plnolety = false; } public override string ToString() { string jsemPlnolety = "jsem"; if (!plnolety) jsemPlnolety = "nejsem"; string pohlavi = "muž"; if (!muz) pohlavi = "žena"; return String.Format("Jsem {0}, {1}. Je mi {2} let a {3} plnoletý.", jmeno, pohlavi, vek, jsemPlnolety); } } {/CSHARP_OOP}
Výstup:
Konzolová aplikace
Jsem Pavel Hora, žena. Je mi 15 let a jsem plnoletý.
Určitě musíme ošetřit, aby se plnoletost obnovila při změně věku.
Když se zamyslíme nad ostatními atributy, není nejmenší důvod, abychom je
taktéž umožňovali modifikovat. Student si za normálních okolností asi jen
stěží změní pohlaví nebo jméno. Bylo by však zároveň vhodné je
vystavit ke čtení, nemůžeme je tedy pouze pouze nastavit jako
private
. V dřívějších lekcích C# kurzu jsme k tomuto účelu
používali metody, které sloužily ke čtení privátních atributů. Jejich
název jsme volili jako VratVek()
a podobně. Ke čtení vybraných
atributů vytvoříme také metody a atributy označíme jako privátní, aby se
nedaly modifikovat zvenčí. Třída by nově vypadala např. takto (vynechal
jsem konstruktor a ToString()
):
class Student { private string jmeno; private bool muz; private int vek; private bool plnolety; ... public string VratJmeno() { return jmeno; } public bool VratPlnoletost() { return plnolety; } public int VratVek() { return vek; } public bool Muz() { return muz; } public void NastavVek(int hodnota) { vek = hodnota; // přehodnocení plnoletosti plnolety = true; if (vek < 18) plnolety = false; } }
Metody, co hodnoty jen vracejí, jsou velmi jednoduché. Nastavení věku má
již nějakou vnitřní logiku, při jeho změně musíme totiž přehodnotit
atribut plnolety
. Zajistili jsme, že se do proměnných nedá
zapisovat jinak, než my chceme. Máme tedy pod kontrolou všechny změny
atributů a dokážeme na ně reagovat. Nemůže se stát, že by nám někdo
vnitřní stav nekontrolovaně měnil a rozbil.
Metodám k navrácení hodnoty se říká gettery a metodám
pro zápis settery. Pro editaci ostatních atributů bychom
udělali jednu metodu EditujStudenta
, která by byla podobná
konstruktoru. Jméno, věk a podobně by se tedy měnily pomocí této metody,
tam bychom mohli např. kontrolovat, zda hodnoty dávají smysl, opět bychom
odchytili všechny pokusy o změnu na jediném místě.
Ruční psaní getterů a setterů je jistě velmi pracné. Nemohl by to udělat někdo za nás? Ano, C# nám je umí vygenerovat. Poté již nehovoříme o atributech, ale o vlastnostech. Syntaxe vlastnosti je velmi podobná atributu:
public string Jmeno { get; set; }
Zprvu to vypadá, jako bychom deklarovali atribut. Jméno vlastnosti je však velkým písmenem, jedná se totiž o metodu (přesněji 2 metody). Ve složených závorkách poté specifikujeme, které metody si přejeme vygenerovat. Za vlastností nepíšeme středník! V ukázce výše se vygeneruje setter i getter, vlastnost půjde tedy normálně číst i modifikovat:
Console.WriteLine(objekt.Jmeno); // číst objekt.Jmeno = "Jan Malý"; // zapisovat
Jediný rozdíl oproti atributu je zvenčí v tom, že počáteční písmeno je velké. C# ve skutečnosti vygeneruje privátní atribut a k němu dvě metody, které podle kontextu volá (pozná dle situace zda čteme nebo zapisujeme). Když do vlastnosti nevygenerujeme setter, nepůjde měnit ani zevnitř, ani zvenčí. Pokud si přejeme, aby vlastnost nešla mimo třídu měnit, označíme setter jako privátní:
public string Jmeno { get; private set; }
Tohoto budeme hojně využívat a právě takto bude vypadat většina vlastností našich budoucích tříd.
Pokud si přejeme, aby se v getteru nebo setteru dělo více, než jen načtení/zápis hodnoty, můžeme si ho definovat ručně. Ukažme si to na našem příkladu s plnoletostí, která se musí po změně věku přehodnotit:
private int vek; public int Vek { get { return vek; } set { vek = value; // kontrola plnoletosti Plnolety = true; if (vek < 18) Plnolety = false; } }
Zprvu je nutné si vytvořit privátní atribut vek
s malým
písmenem, ve které bude hodnota ve skutečnosti uložena. V getteru a
setteru poté pracujeme s tímto atributem, pokud použijete v
get{}
nebo set{}
Vek
, program se
zacyklí!. Není možné definovat jen getter nebo setter, buď se oba
vygenerují samy nebo oba definujeme ručně. Pro přístup k zadané hodnotě
je nám v setteru k dispozici klíčové slovo
value
. Takto se v C# do verze 3.0 musely definovat
všechny vlastnosti, až poté Microsoft zavedl tzv. autoimplementaci a
zkrácený zápis, jaký jsme si uvedli výše. U drtivé většiny vlastností
totiž v metodách nepotřebujeme žádnou logiku.
Občas se můžete setkat s pojmenováním atributů vlastností
s podtržítkem na začátku (tedy např. _vek
). Cílem je odlišit
tyto atributy od ostatních privátních atributů ve třídě, ale zas je to
neobvyklé oproti tomu, jak se atributy v C# běžně pojmenovávají.
S Vek
nyní pracujeme opět stejně, jako s atributem, jen s
velkým písmenem. Nenápadné přiřazení do věku vnitřně spustí další
logiku k přehodnocení vlastnosti Plnolety
:
objekt.Vek = 15; // nyní se změní i plnoletost
Stejně můžeme pochopitelně implementovat i getter a například něco někam logovat.
Upravíme si naši třídu Student
tak, aby používala
vlastnosti. Vypadala by takto:
class Student { public string Jmeno { get; private set; } public bool Muz { get; private set; } public bool Plnolety { get; private set; } private int vek; public int Vek { get { return vek; } set { vek = value; // kontrola plnoletosti Plnolety = true; if (vek < 18) Plnolety = false; } } public Student(string jmeno, bool pohlavi, int vek) { EditujStudenta(jmeno, pohlavi, vek); } public void EditujStudenta(string jmeno, bool pohlavi, int vek) { Jmeno = jmeno; Muz = pohlavi; Vek = vek; } public override string ToString() { string jsemPlnolety = "jsem"; if (!Plnolety) jsemPlnolety = "nejsem"; string pohlavi = "muž"; if (!Muz) pohlavi = "žena"; return String.Format("Jsem {0}, {1}. Je mi {2} let a {3} plnoletý.", Jmeno, pohlavi, Vek, jsemPlnolety); } }
To je o hodně lepší, že? Vlastnosti budeme odteď používat stále,
umožňují nám totiž objekty dokonale zapouzdřit. V .NET jsou
všechny veřejné atributy tříd vlastnosti (např. nám
známá vlastnost Length
na string
u), platí pravidlo,
že co jde ven je vlastnost, co se používá jen
uvnitř je privátní atribut. Veřejný atribut se defakto
příliš nepoužívá. Celou třídu i s ukázkovým programem si samozřejmě
opět můžete stáhnout pod článkem. Kontrolu plnoletosti můžeme z
konstruktoru nyní vyjmout, jakmile totiž dosadíme do vlastnosti
Vek
, nastaví se plnoletost sama. Ještě si opět vyzkoušejme
problémový příklad:
{CSHARP_CONSOLE} Student s = new Student("Pavel Hora", true, 20); s.Vek = 15; // s.Muz = false; // tento řádek nyní způsobí chybu a musí být odebrán Console.WriteLine(s); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Student { public string Jmeno { get; private set; } public bool Muz { get; private set; } public bool Plnolety { get; private set; } private int vek; public int Vek { get { return vek; } set { vek = value; // kontrola plnoletosti Plnolety = true; if (vek < 18) Plnolety = false; } } public Student(string jmeno, bool pohlavi, int vek) { EditujStudenta(jmeno, pohlavi, vek); } public void EditujStudenta(string jmeno, bool pohlavi, int vek) { Jmeno = jmeno; Muz = pohlavi; Vek = vek; } public override string ToString() { string jsemPlnolety = "jsem"; if (!Plnolety) jsemPlnolety = "nejsem"; string pohlavi = "muž"; if (!Muz) pohlavi = "žena"; return String.Format("Jsem {0}, {1}. Je mi {2} let a {3} plnoletý.", Jmeno, pohlavi, Vek, jsemPlnolety); } } {/CSHARP_OOP}
A výstup:
Konzolová aplikace
Jsem Pavel Hora, muž. Je mi 15 let a nejsem plnoletý.
Pokud celou vlastnost označíme jako private
, nelze poté
settery nebo gettery označit jako public
.
Automatické read-only vlastnosti
Read-only vlastnosti můžeme kromě private set
deklarovat tak,
že setter úplně vynecháme. Taková vlastnost musí mít přiřazenu hodnotu
přímo (nebo to můžeme udělat v konstruktoru třídy) a není možné ji
zvenku změnit:
public int Plnoletost { get; } = 18;
init
settery
Podobně máme možnost setter nastavit jako init
. Značíme
tím, že vlastnost může být modifikována pouze v konstruktoru nebo při
inicializaci instance pomocí {}
. Jakmile je
vlastnosti jedním z těchto způsobů přiřazena hodnota, stává se zas
read-only. Nelze ji mimo konstruktor měnit ani ve třídě, ve které se
nachází:
public int Id { get; init; }
Rozdíl oproti read-only vlastnosti je, že vlastnost můžeme inicializovat při tvorbě instance takto:
Student student = new Student { Id = 1, Jmeno = "Jan Novák" };
Lambda výrazy
Zápis jednoduchých getterů a setterů můžeme ještě zkrátit pomocí
tzv. lambda výrazů =>
, které obecně zkracují zápisy metod
vynecháním slova return
a závorek { }
. K lambdám
se sice v kurzu podrobně teprve dostaneme, ale toto jednoduché použití si
můžeme představit již nyní, abychom měli kapitolu vlastností
kompletní.
Libovolnou vlastnost bychom za předpokladu, že getter i setter obsahuje pouze jeden řádek, napsali takto:
private int vlastnost; public int Vlastnost { get => vlastnost; // getter může obsahovat libovolnou operaci na jeden řádek private set => vlastnost = value; // setter může obsahovat libovolnou operaci na jeden řádek }
Konkrétně bychom lambdu v příkladu s naším člověkem mohli využít
pro kontrolu plnoletosti a místo do setteru Vek
ji vložit do
getteru Plnolety
, čímž by tyto vlastnosti vypadaly
následovně:
public int Vek { get; set; } public bool Plnolety => Vek >= 18;
O lambda výrazech se podrobně ještě dozvíme dále v OOP kurzu.
V následujícím kvízu, Kvíz - Dědičnost, statika, vlastnosti v C# .NET OOP, si vyzkoušíme nabyté zkušenosti z předchozích lekcí.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 1391x (26.85 kB)
Aplikace je včetně zdrojových kódů v jazyce C#