Lekce 3 - Hrací kostka v Dartu - Konstruktory a náhodná čísla
V předešlém cvičení, Řešené úlohy k 1.-2. lekci OOP v Dartu, jsme si procvičili nabyté zkušenosti z předchozích lekcí.
V minulé lekci, Řešené úlohy k 1.-2. lekci OOP v Dartu, jsme si naprogramovali první objektovou aplikaci. Již umíme tvořit nové třídy a vkládat do nich vlastnosti a metody s parametry a návratovou hodnotou. V dnešním Dart tutoriálu začneme pracovat na slíbené aréně, ve které budou proti sobě bojovat dva bojovníci. Boj bude tahový (na přeskáčku) a bojovník vždy druhému ubere život na základě síly jeho útoku a obrany druhého bojovníka. Simulujeme v podstatě stolní hru, budeme tedy simulovat i hrací kostku, která dodá hře prvek náhodnosti. Začněme zvolna a vytvořme si dnes právě tuto hrací kostku. Zároveň se naučíme jak definovat vlastní konstruktor.
Vytvořme si novou konzolovou aplikaci a pojmenujme ji arena
. K
projektu si přidejme novou class s názvem Kostka
. Zamysleme se
nad vlastnostmi, které kostce dáme. Jistě by se hodilo, kdybychom si mohli
zvolit počet stěn kostky (klasicky 6 nebo 10 stěn, jak je zvykem u tohoto
typu her). Dále bude kostka potřebovat tzv. generátor náhodných čísel.
Ten nám samozřejmě poskytne Dart knihovna dart:math
, která k
těmto účelům obsahuje třídu Random
. Naše třída bude mít
nyní 2 vlastnosti:
pocetSten
typuint
random
typuRandom
, kde bude náhodný generátor
Minule jsme kvůli jednoduchosti nastavovali všechny vlastnosti naší třídy jako veřejně přístupné (public) tím, že jsme před ně nepsali podtržítko. Většinou se však spíše nechce, aby se daly zvenčí modifikovat (private), což nastavíme přidáním podtržítka jako první znak názvu. Vlastnost je poté viditelná jen uvnitř třídy a zvenčí se Dart tváří, že vůbec neexistuje. Při návrhu třídy tedy nastavíme vše na private a v případě, že něco bude opravdu potřeba vystavit, použijeme public. Naše třída nyní vypadá asi takto:
import 'dart:math'; class Kostka { /// Generátor náhodných čísel Random _random; /// Počet stěn kostky int _pocetSten; }
Konstruktory
Až doposud jsme neuměli zvenčí nastavit jiné vlastnosti než public,
protože private nejsou zvenčí viditelné. Již jsme si říkali něco málo o
konstruktoru objektu. Je to metoda, která se zavolá ve chvíli
vytvoření instance objektu. Slouží samozřejmě k nastavení
vnitřního stavu objektu a k provedení případné inicializace. Kostku bychom
nyní v main.dart
vytvořili takto:
Kostka kostka = new Kostka();
Právě Kostka()
je konstruktor. Protože v naší třídě
žádný není, Dart si dogeneruje prázdnou metodu. My si však nyní
konstruktor do třídy přidáme. Deklaruje se jako metoda, ale nemá
návratový typ a musí mít stejné jméno jako je jméno
třídy, v našem případě tedy Kostka
. V konstruktoru
nastavíme počet stěn na pevnou hodnotu a vytvoříme instanci třídy
Random
. Konstruktor bude vypadat následovně:
Kostka() { _pocetSten = 6; _random = new Random(); }
Pokud kostku nyní vytvoříme, bude mít ve vlastnosti
_pocetSten
hodnotu 6
a v _random
bude
vytvořená instance generátoru náhodných čísel. Vypišme si počet stěn
do konzole, ať vidíme, že tam hodnota opravdu je. Není dobré vlastnost
nastavit na public, protože nebudeme chtít, aby nám někdo mohl již u
vytvořené kostky měnit počet stěn. Přidáme do třídy tedy metodu
vratPocetSten()
, která nám vrátí hodnotu vlastnosti
_pocetSten
. Docílili jsme tím v podstatě toho, že je vlastnost
read-only (vlastnost není viditelný a lze ho pouze číst metodou, změnit ho
nelze). Dart má k tomuto účelu ještě další konstrukce, ale tím se zatím
nebudeme zabývat. Nová metoda bude vypadat asi takto:
/// Vrátí počet stěn hrací kostky. int vratPocetSten() { return _pocetSten; }
Přesuňme se do main.dart
a vyzkoušejme si vytvořit kostku a
vypsat počet stěn:
Kostka kostka = new Kostka(); // v tuto chvíli se zavolá konstruktor print(kostka.vratPocetSten());
Výstup programu:
Konzolová aplikace
6
Vidíme, že se konstruktor opravdu zavolal. My bychom ale chtěli, abychom mohli u každé kostky při vytvoření specifikovat, kolik stěn budeme potřebovat. Dáme tedy kostruktoru parametr:
Kostka(int pocetSten) { _pocetSten = pocetSten; _random = new Random(); }
Vraťme se k main.dart
a zadejme tento parametr do
konstruktoru:
Kostka kostka = new Kostka(10); // v tuto chvíli se zavolá konstruktor s par. 10 print(kostka.vratPocetSten());
Výstup programu:
Konzolová aplikace
10
Vše funguje, jak jsme očekávali. Dart nám již v tuto chvíli nevygeneruje prázdný (tzv. bezparametrický konstruktor), takže kostku bez parametru vytvořit nelze. My to však můžeme umožnit a to hned 2 způsoby. Buď si vytvoříme další konstruktor, nebo nastavíme výchozí hodnotu parametru kontruktoru.
V Dartu nemůžeme vytvořit více konstruktorů lišící se parametry, ale můžeme vytvořit tzv. pojmenované konstruktory, které poznáte tak, že v názvu konstruktorů je za tečkou nějaký další identifikátor. Vytvoříme si tedy pojmenovaný konstruktor, který nám vytvoří šestistěnnou kostku:
Kostka(int pocetSten) { _pocetSten = pocetSten; _random = new Random(); } Kostka.sestistenna() { _pocetSten = 6; _random = new Random(); }
Druhou možností je ponechat si pouze jeden konstruktor, avšak parametru
nastavit výchozí hodnotu (a tím pádem i učinit parametr nepovinný).
Nepovinný parametr zabalíme do hranatých závorek [ ]
a můžeme
mu přiřadit výchozí hodnotu operátorem =
. Nepovinné parametry
fungují i pro metody a musí být umístěny vždy až po povinných
parametrech, tj. např.
void mojeMetoda(int a, int b, [int c = 10])
.
Kostka([int pocetSten = 6]) { _pocetSten = pocetSten; _random = new Random(); }
Do programu si necháme jak pojmenovaný konstruktor tak konstruktor s
nepovinným parametrem a zkusíme si v main.dart
vytvořit 3
instance kostky, každou jiným konstruktorem:
Kostka sestistenna = new Kostka(); Kostka sestistenna2 = new Kostka.sestistenna(); Kostka desetistenna = new Kostka(10); print(sestistenna.vratPocetSten()); print(sestistenna2.vratPocetSten()); print(desetistenna.vratPocetSten());
Výstup programu:
Konzolová aplikace
6
6
10
Zapamatujme si, že Dartu tedy vadí, pokud máme 2 metody (nebo 2 konstruktory) se stejným názvem, i když jejich parametry jsou různé. U konstruktorů se s problémem vypořádáme pomocí pojmenovaného konstruktoru nebo nepovinných parametrů, u metod pomocí nepovinných parametrů nebo zvolením jiného názvu metody.
Pokud bychom měli stejný název parametru, jako je název vlastnosti
třídy, nastal by při přiřazování problém. Fiktivně si tedy uděláme
třídu Testovac
, která bude mít vlastnost hodnota
typu int
a tuto vlastnost se budeme snažit v konstruktoru
inicializovat. Parametr se bude logicky jmenovat stejně, protože značí
hodnotu a tak bychom s našimi znalostmi nejspíše napsali něco takového:
class Testovac { int hodnota; Testovac(int hodnota) { hodnota = hodnota; } }
Což ovšem nebude fungovat, jelikož oba výskyty hodnota
v
konstruktoru jsou svázané s parametrem hodnota
, nikoli s
vlastností hodnota
. V tomto případě musíme pro odlišení
vlastnosti od parametru použít klíčové slovo this
, které
značí odkaz na sebe sama, čímž Dartu povíme, že chceme pracovat s
vlastností, ne s parametrem:
class Testovac { int hodnota; Testovac(int hodnota) { this.hodnota = hodnota; } }
Obdobně bychom postupovali i u funkcí, které by pracovali s vlastnostmi. Abychom se v konstruktoru neupsali, nabízí Dart skvělou syntaktickou zkratku:
class Testovac { int hodnota; Testovac(this.hodnota); }
Místo definování parametru a následně uložení parametru do vlastnosti
v tělu konstruktoru stačí pouze uvést vlastnost objektu s klíčovým slovem
this
. Konstruktor je v tomto případě vygenerován
automaticky.
Náhodná čísla
Definujme na kostce metodu hod()
, která nám vrátí náhodné
číslo od 1 do počtu stěn. Je to velmi jednoduché, metoda bude public
(půjde volat zvenčí) a nebude mít žádný parametr. Návratová hodnota
bude typu int
. Náhodné číslo získáme tak, že na generátoru
zavoláme metodu nextInt()
. Ta přijímá jeden parametr,
značící maximální hodnotu, která není zahrnuta. Minimální hodnota je
0
. Jinými slovy, metoda vrátí číslo 0
(včetně)
až max
(bez).
Pokud potřebujeme nastavit jinou minimální hodnotu, jednoduše k výsledku přičteme číslo, které se má stát minimem. Pozor však na to, že tímto posuneme i maximum, tj. v určitých momentech bychom museli zmenšit maximum. Nám ale posun minima i maxima o 1 vyhovuje, jelikož tímto postupem dostaneme rozsah 1 (včetně) až max (včetně).
/// Vykoná hod kostkou. /// /// Vrací číslo od 1 do počtu stěn (včetně). int hod() { return _random.nextInt(_pocetSten) + 1; }
Překrývání metody toString()
Kostka je téměř hotová, ukažme si ještě jednu užitečnou metodu,
kterou kostce přidáme a kterou budeme hojně používat i ve většině
našich dalších objektů. Řeč je o metodě toString()
, o které
jsme se již zmínili a kterou obsahuje každý objekt, tedy i
nyní naše kostka. Metoda je určena k tomu, aby vrátila tzv. textovou
reprezentaci instance. Hodí se ve všech případech, kdy si instanci
potřebujeme vypsat nebo s ní pracovat jako s textem. Tuto metodu mají např.
i čísla. Již víme, že v Dartu funguje implicitní konverze, jakmile tedy
budeme chtít do konzole vypsat číslo nebo kterýkoli jiný objekt, Dart na
něm zavolá metodu toString()
a vypíše její výstup. Pokud si
děláme vlastní třídu, měli bychom zvážit, zda se nám takováto metoda
nehodí. Nikdy bychom si neměli dělat vlastní metodu, např. něco jako
vypis()
, když máme v Dartu připravenou cestu, jak toto řešit.
U kostky nemá toString()
vyšší smysl, ale u bojovníka bude
jistě vracet jeho jméno. My si ji ke kostce stejně přidáme, bude vypisovat,
že se jedná o kostku a vrátí i počet stěn. Nejprve si zkusme vypsat do
konzole naši instanci kostky:
print(sestistenna);
Výstup programu:
Konzolová aplikace
Instance of 'Kostka'
Do konzole se vypíše pouze název naší třídy, tedy Kostka
.
Metodu nemůžeme jen tak definovat, protože je již definována (v dalších
dílech zjistíme proč). Musíme ji tedy přepsat, resp.
překrýt. Tím se opět nebudeme nyní podrobně zabývat,
nicméně chci, abychom již teď uměli toString()
používat. K
překrytí použijeme klíčové slovo override:
/// Vrací textovou reprezentaci kostky. @override String toString() { return 'Kostka s $_pocetSten stěnami'; }
Nyní opět zkusíme do konzole vypsat přímo instanci kostky.
Výstup programu:
Konzolová aplikace
Kostka s 6 stěnami
Ještě si naše kostky vyzkoušíme. Zkusíme si v programu s našima dvěma kostkami v cyklech házet a podíváme se, jestli fungují tak, jak se očekává:
// vytvoření Kostka sestistenna = new Kostka(); Kostka desetistenna = new Kostka(10); // hod šestistěnnou print(sestistenna); for (int i = 0; i < 10; i++) stdout.write('${sestistenna.hod()} '); // hod desetistěnnou print('\n\n$desetistenna'); for (int i = 0; i < 10; i++) stdout.write('${desetistenna.hod()} ');
Výstup programu může vypadat nějak takto:
Konzolová aplikace
Kostka s 6 stěnami
6 5 5 6 5 4 2 6 4 2
Kostka s 10 stěnami
1 6 3 4 6 8 1 9 10 8
Máme hotovou docela pěknou a nastavitelnou třídu, která reprezentuje
hrací kostku. Bude se nám hodit v naší aréně, ale můžete ji použít i
kdekoli jinde. Vidíme, jak OOP umožňuje znovupoužívat komponenty. V
příští lekci, Řešené úlohy k 3. lekci OOP v Dartu, si řekneme něco o odlišnostech mezi referenčními
datovými typy (objekty) a typy hodnotovými (např. int
).
V následujícím cvičení, Řešené úlohy k 3. lekci OOP v Dartu, 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 11x (2.38 kB)
Aplikace je včetně zdrojových kódů v jazyce Dart