OOP v C# - Dědičnost, hodnotové a referenční typy

Zdroj: SOOM.cz [ISSN 1804-7270]
Autor: sukovanej
Datum: 31.5.2013
Hodnocení/Hlasovalo: 3.86/7

Tématem článku je jedna z mocných zbraní objektově orientovaného přístupu k programování - dědičnost. V první části článku je vysvětlen rozdíl mezi referenčními a hodnotovými typy. Dále je zde nastíněna funkčnost Garbage Collectoru a poslední část článku se již věnuje dědičnosti.

Referenční a hodnotové typy

V .Net Frameworku se proměnné podle typu dělí na referenční a hodnotové. Prvním a základním typem je hodnotový typ. Do této kategorie patří datové typy jako je int, decimal, double, float, ale dále také struct (struktura) a enum(výčtový typ). Základními specifikami pro hodnotové typy je, že aplikace pracuje přímo s hodnotami proměnných, pracuje se s nimi velmi rychle a zabírají co nejméně místa. Každá hodnotová proměnná má paměť přidělenou v tzv. zásobníku. To je poměrně malá paměť přidělená operačním systém, ke které přímo přistupuje aplikace.

Druhým typem je tzv. referenční typ. Mezi ně patří object, string, třída, pole, delegát a rozhraní. Z minulého dílu můžeme o třídě tvrdit, že může být docela robustní a o nějaké rychlosti a úspornosti paměti nemůže být ani řeč. Proto se referenční proměnné neukládají do zásobníku (resp. nejen do zásobníku a to si vysvětlíme za chvíli), ale do tzv. haldy. To je speciální paměť prakticky neomezené velikosti, s pomalejším přístupem, ve které se ukládají referenční proměnné. Zásobník v tuto chvíli hraje ovšem také důležitou roli. Do něj totiž ukládáme odkazující proměnné, skrze které přistupujeme k informacím v haldě. Vše je pěkně vyobrazeno v následujícím diagramu.

Může se také stát, že referenční proměnná v zásobníku bude odkazovat na objekt v haldě, na který už odkazuje jiná proměnná. Co se stane, pokud změníme vlastnost objektu pomocí jedné proměnné a potom necháme udělat výpis vlastnosti druhou proměnnou? Vypíše se nová verze upravená pomocí první proměnné. Zkusme si to ukázat na příkladu. Vytvoříme si následující jednoduchou třídu reprezentující nějaké zvíře.

class Animal
{
    public string Name { get; set; }

    public Animal(string name)
    {
        this.Name = name;
    }

    public void Shout()
    {
        Console.WriteLine("Ahoj, já jsem " + this.Name);
    }
}

Nyní vytvoříme dvakrát novou instanci této třídy, potom do druhé z instancí uložíme hodnotu z první instance a budeme pozorovat, jak se bude chovat metoda Shout() při změně vlastnosti Name.

Animal kocka = new Animal("Micka");
Animal pes = new Animal("Hafík");

kocka.Shout();
pes.Shout();

pes = kocka;
kocka.Name = "Mutanťák";

kocka.Shout();
pes.Shout();

Pokud jste si aplikaci zkusili vytvořit a spustit, na konzoli byste měli dostat asi takovýto výstup.

Ahoj, já jsem Micka
Ahoj, já jsem Hafík
Ahoj, já jsem Mutanťák
Ahoj, já jsem Mutanťák

Dost často je klíčové slovo null mylně zaměňováno za prázdnou hodnotu. Ve skutečnosti tímto slovíčkem po dosazení do referenční proměnné (pozor! ne do hodnotové) říkáme, že ji zatím nechceme odkazovat na žádný objekt v haldě.

Garbage Collector

Nasnadě je otázka, co se stane s objektem pes(Hafík) po vykování pátého řádku. Abychom význam následujícího textu viděli zřetelněji, představme si nyní, že aplikace nemá osm řádků, ale osm tisíc řádků kódu a při průběhu aplikace se nashromáždí hned několik stovek takovýchto nepotřebných objektů. Kdyby v paměti zůstávali, jen by zabírali místo a zpomalovali chod programu. Abychom se o uvolňování takovýchto objektů nemuseli starat, používá se v .Netu tzv. Garbage Collector. To je speciální aplikace běžící paralelně s naší aplikací, která kontroluje a uvolňuje „mrtvé objekty“.

Dědičnost

V této části započneme další velkou kapitolu objektově orientovaného programování – dědičnost. Některé základní informace jsou opět zmíněny v článku Objektově orientované programování. Připomínám, že se tedy jedná o schopnost třídy dědit prvky z třídy jiné. Třídě, ze které se bude dědit, se říká bázová a třídě, která bude dědit, odvozená. Aby byly prvky ve třídě "zděditelné", musí mít modifikátory public, protected nebo iternal protected.

Pro demonstraci využijeme náš příklad s třídou Animal. Chtěli bychom nyní vytvořit třídu pro psa takovou, aby obsahovala prvky, co třídy Animal, ale k tomu ještě navíc metodu OznackovatUzemi(), která je specifická pouze pro psa. Analogicky bychom mohli vymyslet ještě třídy pro kočku a opici jako na diagramu níže.

Samozřejmě nebudeme vytvářet znovu novou třídu s totožnými metodami, ale využijeme dědičnosti. To, že chceme dědit z jiné třídy, dáme najevo dvojtečkou za názvem třídy a názvem bázové třídy. Pokud v této třídy napíšete this, po stisknutí tečky vám okamžitě Intellisense nabídne prvky z bázové třídy. Při vytváření konstruktoru máme opět možnost celý ho definovat znovu. Ale protože úkolem programátora je být líný, využijeme konstruktoru z třídy bázové. Jak? To je více než zřejmé z následujícího příkladu.

class Dog : Animal
{
    public Dog(string name) : base(name) { }

    public void OznackovatUzemi()
    {
        Console.WriteLine("Pes se jménem " + this.Name + " označkoval toto území!");
    }
}

Po názvu konstruktoru se za dvojtečku uvede klíčové slovo base a do závorky se mu předají potřebné parametry. Mezi složené závorky lze uvést ještě dodatečné příkazy, v tomto příkladě to ovšem není potřeba. Zkuste nyní použít následující kód.

Dog pes = new Dog("Hafík");
pes.Shout();
pes.OznackovatUzemi();

Výsledkem bude výpis s informací o jménu (z bázové třídy) a o označkování území(nová metoda v odvozené třídě).

Přepisování

Pokud potřebujeme nějakou metodu redefinovat, lze ji pomocí klíčového slova new přepsat a definovat znovu. Použití je jednoduché. Ukážeme si to na metodě Shout().

public new void Shout()
{
    Console.WriteLine("Ahoj, já jsem pes " + this.Name);
}

Výsledek kódu Příklad_1 se bude nyní lišit v druhém řádku, kde jsme dodali slovo „pes“.

To je k tomuto článku vše. V dalším díle se budu dále věnovat dědičnosti, konkrétně to budou například abstraktní metody a porovnávání typu objektů.