Lekce 9 - Statika
V předešlém cvičení, Řešené úlohy k 5.-8. lekci OOP v C# .NET, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
Dnes se budeme věnovat pojmu statika. Až doposud jsme byli zvyklí, že data (stav) nese instance. Atributy, které jsme definovali, tedy patřily instanci a byly pro každou instanci jedinečné. OOP však umožňuje definovat atributy a metody na samotné třídě. Těmto prvkům říkáme statické (někdy třídní) a jsou nezávislé na instanci.
POZOR! Dnešní lekce vám ukáže statiku, tedy
postupy, které v podstatě narušují objektový model. OOP je obsahuje jen pro
speciální případy a obecně platí, že vše jde napsat bez
statiky. Vždy musíme pečlivě zvážit, zda statiku opravdu
nutně potřebujeme. Obecně doporučujeme statiku vůbec
nepoužívat, jestliže si nejste naprosto jistí tím, co děláte.
Podobně jako globální proměnné (které C# naštěstí nemá) je i statika v
objektovém programování něco, co umožňuje psát špatný kód a porušovat
dobré praktiky. Dnes si ji tedy spíše vysvětlíme, abyste pochopili určité
metody a třídy v .NET, které statiku používají. Znalosti použijte s
rozvahou, na světě bude potom méně zla.
Statické (třídní) atributy
Jako statické můžeme označit různé prvky. Začněme u atributů. Jak
jsme již v úvodu zmínili, statické prvky patří třídě, nikoli instanci.
Data v nich uložená tedy můžeme číst bez ohledu na to, zda nějaká
instance existuje. V podstatě můžeme říci, že statické atributy jsou
společné pro všechny instance třídy, ale není to přesné, protože s
instancemi doopravdy vůbec nesouvisejí. Založme si nový projekt (název
např. Statika
) a vytvořme si jednoduchou třídu
Uzivatel
:
class Uzivatel { private string jmeno; private string heslo; private bool prihlaseny; public Uzivatel(string jmeno, string heslo) { this.jmeno = jmeno; this.heslo = heslo; prihlaseny = false; } public bool PrihlasSe(string zadaneHeslo) { if (zadaneHeslo == heslo) { prihlaseny = true; return true; } else return false; // hesla nesouhlasí } }
Třída je poměrně jednoduchá, reprezentuje uživatele nějakého
systému. Každá instance uživatele má své jméno, heslo a také se o ní
ví, zda je přihlášená, či nikoli. Aby se uživatel přihlásil, zavolá se
na něm metoda PrihlasSe()
a v jejím parametru se předá heslo,
které člověk za klávesnicí zadal. Metoda ověří, zda se jedná opravdu o
tohoto uživatele, a pokusí se ho přihlásit. Vrátí
true
/false
podle toho, zda přihlášení proběhlo
úspěšně. V praxi by se heslo ještě tzv. hashovalo, ale to zde
opomineme.
Když se uživatel registruje, systém mu napíše, jakou minimální délku
musí jeho heslo mít. Toto číslo bychom měli mít někde uložené.
Ve chvíli, kdy uživatele registrujeme, ještě nemáme k dispozici
jeho instanci. Objekt není vytvořený a vytvoří se až po
vyplnění formuláře. Nemůžeme tedy ve třídě Uzivatel
k
tomuto účelu použít veřejný atribut minimalniDelkaHesla
. Bylo
by však velmi přínosné, kdybychom údaj o minimální délce hesla měli ve
třídě Uzivatel
uložený, protože k uživateli logicky patří.
Údaj tedy uložíme do statického atributu pomocí
modifikátoru static
:
class Uzivatel { private string jmeno; private string heslo; private bool prihlaseny; public static int minimalniDelkaHesla = 6; ... }
Nyní se přesuňme do Program.cs
a zkusme si atribut vypsat. K
atributu nyní přistoupíme přímo přes třídu:
{CSHARP_CONSOLE} Console.WriteLine(Uzivatel.minimalniDelkaHesla); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Uzivatel { private string jmeno; private string heslo; private bool prihlaseny; public static int minimalniDelkaHesla = 6; public Uzivatel(string jmeno, string heslo) { this.jmeno = jmeno; this.heslo = heslo; prihlaseny = false; } public bool PrihlasSe(string zadaneHeslo) { if (zadaneHeslo == heslo) { prihlaseny = true; return true; } else return false; // hesla nesouhlasí } } {/CSHARP_OOP}
Vidíme, že atribut opravdu náleží třídě. Můžeme se na ni ptát v různých místech programu, aniž bychom měli uživatele vytvořeného. Naopak na instanci uživatele tento atribut nenalezneme:
Uzivatel u = new Uzivatel("Tomáš Marný", "heslojeveslo"); Console.WriteLine(u.minimalniDelkaHesla);
Visual Studio zahlásí chybu a kód se nezkompiluje.
Jako další praktické využití statických atributů se nabízí
číslování uživatelů. Budeme chtít, aby měl každý uživatel
přidělené unikátní identifikační číslo. Bez znalosti statiky bychom si
museli hlídat zvenčí každé vytvoření uživatele a počítat je. My si
však můžeme vytvořit přímo na třídě Uzivatel
privátní
statický atribut dalsiId
, kde bude vždy připraveno číslo pro
dalšího uživatele. První uživatel bude mít id
1
,
druhý 2
a tak dále. Uživateli tedy přibude nový atribut
id
, který se v konstruktoru nastaví podle hodnoty
dalsiId
. Pojďme si to vyzkoušet:
class Uzivatel { private string jmeno; private string heslo; private bool prihlaseny; private int id; private static int minimalniDelkaHesla = 6; private static int dalsiId = 1; public Uzivatel(string jmeno, string heslo) { this.jmeno = jmeno; this.heslo = heslo; prihlaseny = false; id = dalsiId; dalsiId++; } ... }
Třída si sama ukládá, jaké bude id
další její instance.
Toto id
přiřadíme nové instanci v konstruktoru a zvýšíme ho
o 1, aby bylo připravené pro další instanci. Statické však nemusí být
jen atributy, možnosti jsou mnohem větší.
Statické metody
Statické metody se volají na třídě. Jedná se zejména o
pomocné metody, které potřebujeme často používat, a proto
se nám nevyplatí tvořit instanci. Mnoho takovýchto metod již známe, jen
jsme si to neuvědomovali. Nikdy jsme např. netvořili instanci konzole k tomu,
abychom do ní mohli zapisovat. Metoda WriteLine()
na třídě
Console
je statická. Konzole je jen jedna a bylo by zbytečné
tvořit si z ní instanci, když ji chceme používat. Podobně je tomu např. u
metody Round()
na třídě Math
. Když chceme
zaokrouhlit číslo, nebudeme si k tomu přece tvořit objekt. Jedná se tedy
většinou o pomocné metody, kde by instanciace zbytečně zdržovala nebo
nedávala smysl.
Ukažme si opět reálný příklad. Při registraci uživatele potřebujeme
znát minimální délku hesla ještě před jeho vytvořením. Bylo by také
dobré, kdybychom mohli před jeho vytvořením také zkontrolovat, zda má
heslo správnou délku, neobsahuje diakritiku, je v něm alespoň jedno číslo
a podobně. Za tímto účelem si vytvoříme pomocnou statickou
metodu ZvalidujHeslo()
:
public static bool ZvalidujHeslo(string heslo) { if (heslo.Length >= minimalniDelkaHesla) { // podrobnou logiku validace hesla vynecháme return true; } return false; }
Opět si zkusíme, že metodu můžeme na třídě Uzivatel
zavolat:
{CSHARP_CONSOLE} Console.WriteLine(Uzivatel.ZvalidujHeslo("heslojeveslo")); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Uzivatel { private string jmeno; private string heslo; private bool prihlaseny; private int id; private static int minimalniDelkaHesla = 6; private static int dalsiId = 1; public Uzivatel(string jmeno, string heslo) { this.jmeno = jmeno; this.heslo = heslo; prihlaseny = false; id = dalsiId; dalsiId++; } public bool PrihlasSe(string zadaneHeslo) { if (zadaneHeslo == heslo) { prihlaseny = true; return true; } else return false; // hesla nesouhlasí } public static bool ZvalidujHeslo(string heslo) { if (heslo.Length >= minimalniDelkaHesla) { // podrobnou logiku validace hesla vynecháme return true; } return false; } } {/CSHARP_OOP}
Pozor! Kvůli tomu, že metoda
ZvalidujHeslo()
náleží třídě, nemůžeme v ní přistupovat k
žádným instančním atributům. Tyto atributy totiž neexistují v
kontextu třídy, ale instance. Ptát se na jmeno
by v naší
metodě nemělo smysl! Můžete si zkusit, že to opravdu nejde.
Stejné funkčnosti při validaci hesla samozřejmě můžeme dosáhnout i
bez znalosti statiky. Vytvořili bychom si nějakou třídu, např.
ValidatorUzivatelu
, a do ní bychom napsali tyto metody. Museli
bychom poté vytvořit instanci třídy, abychom metody mohli volat. Bylo by to
trochu matoucí, protože logika uživatele by byla zbytečně rozdělena do
dvou tříd, ačkoli pomocí statiky může být pohromadě.
U příkladu se statickým atributem minimalniDelkaHesla
jsme
porušili zapouzdření. Neměli bychom dovolovat atribut nekontrolovaně
měnit. Atribut můžeme samozřejmě nastavit jako privátní a k jeho čtení
vytvořit statickou metodu. To ostatně dobře známe z minulých lekcí.
Takovou metodu doplníme i pro navrácení id
:
public static int VratMinimalniDelkuHesla() { return minimalniDelkaHesla; } public int VratId() { return id; }
A vyzkoušíme si ještě nakonec naše metody. Program.cs
bude
vypadat takto:
{CSHARP_CONSOLE} Uzivatel u = new Uzivatel("Tomáš Marný", "heslojeveslo"); Console.WriteLine("ID prvního uživatele: {0}", u.VratId()); Uzivatel v = new Uzivatel("Olí Znusinudle", "csfd1fg"); Console.WriteLine("ID druhého uživatele: {0}", v.VratId()); Console.WriteLine("Minimální délka hesla uživatele je: {0}", Uzivatel.VratMinimalniDelkuHesla()); Console.WriteLine("Validnost hesla \"heslo\" je: {0}", Uzivatel.ZvalidujHeslo("heslo")); Console.ReadKey(); {/CSHARP_CONSOLE}
{CSHARP_OOP} class Uzivatel { private string jmeno; private string heslo; private bool prihlaseny; private int id; private static int minimalniDelkaHesla = 6; private static int dalsiId = 1; public Uzivatel(string jmeno, string heslo) { this.jmeno = jmeno; this.heslo = heslo; prihlaseny = false; id = dalsiId; dalsiId++; } public bool PrihlasSe(string zadaneHeslo) { if (zadaneHeslo == heslo) { prihlaseny = true; return true; } else return false; // hesla nesouhlasí } public static bool ZvalidujHeslo(string heslo) { if (heslo.Length >= minimalniDelkaHesla) { // podrobnou logiku validace hesla vynecháme return true; } return false; } public static int VratMinimalniDelkuHesla() { return minimalniDelkaHesla; } public int VratId() { return id; } } {/CSHARP_OOP}
A výstup bude:
Konzolová aplikace
ID prvního uživatele: 1
ID druhého uživatele: 2
Minimální délka hesla uživatele je: 6
Validnost hesla "heslo" je: False
Všimněme si, že i metoda Main()
je statická, program totiž
máme jen jeden. Z Main()
můžeme volat také jen statické metody
v hlavní třídě našeho programu. Umíme tedy přidávat metody přímo do
Program.cs
, což však úplně nemá smysl, neboť by se veškerá
logika měla odehrávat v zapouzdřených objektech.
Statický konstruktor
Třída může mít i statický konstruktor. Ten se vykoná někdy ve chvíli, kdy se aplikace spustí a třída se zaregistruje k použití. Takový konstruktor můžeme použít pro přípravné práce, výpočty a podobně. Můžeme v něm podobně jako v instančním konstruktoru vytvořit instance nějakých tříd a uložit si je do statických atributů.
Statické třídy
Pokud se nám vyskytne třída, která obsahuje jen pomocné
metody nebo od níž nemá smysl tvořit instance
(např. nikdy nebudeme mít dvě konzole), můžeme ji označit jako statickou.
Takovouto třídu poté nelze instanciovat (vytvořit její
instanci). Statické třídy v C# nelze dědit. Je to pravděpodobně z toho
důvodu, aby nevznikaly příliš divoké a špatně napsané struktury.
Statické třídy, s nimiž jsme se setkali, jsou Console
a
Math
. Zkusme si vytvořit instanci statické třídy
Math
:
Math m = new Math();
Dostaneme vyhubováno. Statická třída má všechny prvky statické, a tedy nedává smysl od ní tvořit instanci, jež by nic neobsahovala.
Statický registr
Pojďme si takovou jednoduchou statickou třídu vytvořit. Mohlo by se
jednat o třídu, která obsahuje jen pomocné metody a atributy (jako
Math
). Rozhodli jsme se však vytvořit tzv. statický registr.
Ukážeme si, jak je možné předávat důležitá data mezi třídami, aniž
bychom museli mít instanci.
Mějme nějakou větší a rozsáhlejší aplikaci, např. diář. Aplikace bude obsahovat přepínání jazyka rozhraní, zvolení používaných záložek, složky k ukládání souborů, barevného schématu a ještě třeba možnost, zda ji chceme spouštět při spuštění operačního systému. Aplikace tedy bude mít nějaká nastavení, k nimž se bude přistupovat z různých míst programu. Bez znalosti statiky bychom museli všem objektům (kalendáři, úlohám, poznámkám…) předat v konstruktoru, v jakém jazyce pracují, případně jim dodat tímto způsobem další nastavení, jako první den v týdnu (neděle/pondělí) a podobně.
Jednou z možností, jak toto řešit, je použít k uložení těchto nastavení statickou třídu. Třída tedy bude přístupná ve všech místech programu, a to i bez vytvoření instance. Obsahovat bude všechna potřebná nastavení, která si z ní budou objekty libovolně brát. Třída by mohla vypadat např. takto:
static class Nastaveni { private static string jazyk = "CZ"; private static string barevneSchema = "cervene"; private static bool spustitPoStartu = true; public static string Jazyk() { return jazyk; } public static string BarevneSchema() { return barevneSchema; } public static bool SpustitPoStartu() { return spustitPoStartu; } }
Všechny atributy, metody i samotná třída musejí obsahovat modifikátor
static
. Záměrně jsme do třídy nedávali veřejné atributy,
ale vytvořili jsme metody, aby se hodnoty nedaly měnit. Pro programátora je
to trochu nepohodlné, příště si ukážeme, jak to udělat lépe,
představíme si totiž vlastnosti.
Zkusme nyní třídu použít, i když nemáme program diář. Vytvoříme si
jen na ukázku třídu Kalendar
a zkusíme si, že v ní opravdu
máme bez problému přístup k nastavení. Do třídy vložíme metodu, která
vrátí všechna nastavení:
class Kalendar { public string VratNastaveni() { string s = ""; s += String.Format("Jazyk: {0}\n", Nastaveni.Jazyk()); s += String.Format("Barevné schéma: {0}\n", Nastaveni.BarevneSchema()); s += String.Format("Spustit po startu: {0}\n", Nastaveni.SpustitPoStartu()); return s; } }
Následně vše vypíšeme do konzole:
{CSHARP_CONSOLE} Kalendar kalendar = new Kalendar(); Console.WriteLine(kalendar.VratNastaveni()); Console.ReadKey(); {/CSHARP_CONSOLE}
{CSHARP_OOP} static class Nastaveni { private static string jazyk = "CZ"; private static string barevneSchema = "cervene"; private static bool spustitPoStartu = true; public static string Jazyk() { return jazyk; } public static string BarevneSchema() { return barevneSchema; } public static bool SpustitPoStartu() { return spustitPoStartu; } } {/CSHARP_OOP}
{CSHARP_OOP} class Kalendar { public string VratNastaveni() { string s = ""; s += String.Format("Jazyk: {0}\n", Nastaveni.Jazyk()); s += String.Format("Barevné schéma: {0}\n", Nastaveni.BarevneSchema()); s += String.Format("Spustit po startu: {0}\n", Nastaveni.SpustitPoStartu()); return s; } } {/CSHARP_OOP}
Konzolová aplikace
Jazyk: CZ
Barevné schéma: cervene
Spustit po startu: True
Vidíme, že instance kalendáře má opravdu bez problému přístup ke všem nastavením programu.
Opět pozor! Tento kód lze nesprávně použít pro předávání nezapouzdřených dat a používá se jen ve specifických situacích. Většina předávání dat do instance probíhá pomocí parametru v konstruktoru, nikoli prostřednictvím statiky.
Statika se velmi často vyskytuje v návrhových vzorech, o kterých jsme se zde
již bavili. Jsou to postupy, které dovádí objektově orientované
programování k dokonalosti a o kterých se tu ještě zmíníme. Pro dnešek
je toho však již dost
V následujícím cvičení, Řešené úlohy k 9. lekci OOP v C# .NET, si procvičí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 854x (54.94 kB)
Aplikace je včetně zdrojových kódů v jazyce C#