Lekce 3 - Hrací kostka v Dartu - Konstruktory a náhodná čísla

Dart Objektově-orientované programování Hrací kostka v Dartu - Konstruktory a náhodná čísla

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, První objektová aplikace v Dartu - Hello object world, 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 typu int
  • random typu Random, 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, Odkazy na objekty, jejich kopírování a garbage collector, si řekneme něco o odlišnostech mezi referenčními datovými typy (objekty) a typy hodnotovými (např. int). :)


 

Stáhnout

Staženo 0x (2.38 kB)
Aplikace je včetně zdrojových kódů v jazyce Dart

 

 

Článek pro vás napsal Honza Bittner
Avatar
Jak se ti líbí článek?
2 hlasů
Milovník Dartu. Student FIT ČVUT. Sleduj mě na https://twitter.com/tenhobi a ptej se na cokoli na https://github.com/tenhobi/ama.
Aktivity (2)

 

 

Komentáře

Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zatím nikdo nevložil komentář - buď první!