C#SPSE_ALG3_OBJEKTOVÉ_PROGRAMOVÁNÍ.pdf
Document Details
Uploaded by BrandNewSandDune9854
Full Transcript
JAZYK C# OBJEKTOVĚ ORIENTOVANÉ PROGRAMOVÁNÍ Obsah: 1. Třídy a objekty ✔ 2. Datové položky ✔ 3. Konstruktory ✔ 4. Kompozice objektů ✔ 5. Met...
JAZYK C# OBJEKTOVĚ ORIENTOVANÉ PROGRAMOVÁNÍ Obsah: 1. Třídy a objekty ✔ 2. Datové položky ✔ 3. Konstruktory ✔ 4. Kompozice objektů ✔ 5. Metody ✔ 6. Vlastnosti ✔ 7. Dědičnost ✔ 8. Polymorfismus ✔ 9. Abstraktní metody a třídy ✔ 10. Rozhraní ✔ 11. Kolekce ✘ 12. Generické typy a metody ✘ 13. Delegáty a lambda-výrazy ✘ 14. Události ✘ 15. Přetěžování operátorů ✘ 16. Statické členy tříd a statické třídy ✔ 17. Výjimky ✔ 18. Soubory ✔ Přílohy: A. Windows Forms ✘ B. UML diagramy ✘ Verze: 12. února 2024 1 OBJEKTOVÉ PARADIGMA Objektově orientovaný programovací styl představuje obecný postup analýza, návrhu a implementace programu, který modeluje objekty z reálného světa aplikace, a to včetně jejich vazeb a interakce. Třídy a objekty Základní jednotkou je objekt, který odpovídá nějakému objektu z reálného světa (např. objekt člověk nebo databáze). Každý objekt má určité vlastnosti (atributy), které uchovávají jeho vniřní stav, a schopnosti (metody), které umí objekt vykonávat: Objekty jsou vytvářeny podle šablony, který se označuje jako třída – třída tedy definuje vlastnosti a schopnosti objektů. (Jinak řečeno – třída je datový typ.) Všechny objekty vytvořené podle jedné třídy (instance) pak mají stejné metody a liší se v hodnotách svých atributů. Výhoda objektového přístupu spočívá v přehlednosti zápisu – každá metoda a atribut „patří“ určitému objektu. Srovnejte procedurální a objektový kód: vymaž_záznam(záznam) oproti záznam.Vymaž(). Zprávy Komunikace mezi objekty probíhá pomocí předávání zpráv, kdy jeden objekt volá metody druhého: uživatel_1.Pozdrav(uživatel_2). Tři pilíře OOP Objektově orientované programování stojí na třech základních pilířích: Zapouzdření (encapsulation) umožňuje skrýt některé metody a atributy tak, aby byly použitelné jen pro samotnou třídu. Objekt si pak lze představit jako „černou skřínku“, která má určité rozhraní (interface), přes které jí předáváme instrukce, které ona zpracovává. Dědičnost (inheritance) vyjadřuje, že jedna třída („potomek“) vychází z jiné („předka“). Potomek získá všechny vlastnosti předka, které buď přijme přesně tak, jak je měl předek, nebo je vlastním způsobem upraví či přepíše. Polymorfismus (polymorphism) znamená, že instance různých tříd (objekty) na stejný podnět (na volání stejné metody) reagují různě. Instance více tříd poskytují svému okolí stejnou službu, ale každá instance na vyžádání této služby provede něco jiného. 2 1. TŘÍDY A OBJEKTY Třída je šablona, podle níž se vytvářejí objekty. Každá třída může mít tyto členy: datové položky neboli atributy (fields) – proměnné, které ukládají hodnoty vlastnosti (properties) – zpřístupňují a ošetřují datové položky objektů metody (methods) – vyjadřují schopnosti objektů události (events) – oznamují, že v objektu došlo k nějaké změně Představme si například třídu Auto: class Auto Fields: cena: float spz: string výrobce: string Properties: Cena { get; set; } : float Spz { get; set; } : string Výrobce { get; } : string Methods: Auto(float cenat, string spz, string výrobce) Nastartuj() : bool Jeď(string cíl) : void Events: ZměnaRychlosti : EventHandler CílDosažen : EventHandle DošelBenzín : EventHandler Deklarace třídy Obsahuje modifikátor přístupu, její identifikátor a případně odkaz na „rodičovskou“ třídu: public class Uživatel { public string jméno; public int id; } Modifikátor určuje přístupnost třídy. Může nabývat pěti hodnot: public– určuje, že třídu lze používat (vytvářet její instance, volat její metody...) v libovolné části programu, kde je deklarována (a v jiném programu, který má odkaz na sestavení, jež tuto třídu obsahuje) internal – je defaultní; určuje, že třídu lze používat v rámci aktuálního sestavení (assembly), tj. v rámci.dll či.exe souboru abstract – deklaruje abstraktní třídu, od níž nelze vytvářet instance; obvykle sdružuje společné vlastnosti potomků dané třídy sealed – uvozuje třídu, od níž nelze vytvářet potomky (např. třída celých čísel int je „zapečetěná“) static – určuje statickou třídu, která obsahuje pouze statické členy; nelze od ní vytvářet instance a slouží tedy jako „sklad“ svých metod a datových položek partial – rozděluje deklaraci třídy do více souborů; jednu část typicky spravuje IDE (např. ve vizuálním návrhu), druhou část spravuje programátor Vytvoření instance Nový objekt se vytvoří operátorem new. Datové položky, které nejsou inicializovány, získají nulovou hodnotu vzhledem k datovému typu. Ke členům instancí se přistupuje operátorem tečka. Uživatel u = new Uživatel(); 3 Struktura programu Struktura programu s definicí uživatelské třídy je následující: using System; // import tříd vnějšího jmenného prostoru namespace OOPconsoleApp // jmenný prostor { public class Bod // veřejná třída { public int x, y; public int barva = 0x00FFFFFF; } internal class Program { static void Main(string[] args) // vstupní bod aplikace s argumenty příkazové řádky { Bod a = new Bod(); // vytvoření instance Bod b = new Bod(); Console.WriteLine(a.GetType()); // OOPconsoleApp.Bod Console.WriteLine(a.x); // 0 (automaticky inicializovaná) Console.WriteLine(b.barva); // 16777215 b.x = 5; Console.WriteLine(b.x) // 5 } } } Třídy se sdružují do jmenných prostorů – viz klíčové slovo namespace. Má-li být třída viditelná v souboru, kde je metoda Main(), tedy vstupní bod aplikace, musí být ve stejném jmenném prostoru. Třídy jiných jmenných prostorů zviditelní příkaz using. using System; namespace Grafika // jmenný prostor Grafika … { public class Bod // … obsahuje veřejnou třídu Grafika.Bod { public int x, y; public int barva = 0x00FFFFFF; } } using System; using Grafika; // using zviditelní jmenný prostor Grafika … namespace Priklad // … ve jmenném prostoru Priklad { class VstupniBodAplikace { static void Main() // … klientský program využívá třídu Grafika.Bod { Bod a = new Bod(); a.x = 100; b.y = 200; Console.WriteLine("[{0}, {1}]", a.x, a.y); } } } 4 Přístupová oprávnění členů třídy Pro každý člen třídy – datové položky, vlastnosti, metody, události – je třeba předepsat přístupová oprávnění určující, kdo jej bude smět používat: public – veřejnou člen lze používat všude, kde lze používat třídu internal – interní člen je dostupný ze třídy a ze sestavení protected internal – člen je dostupný ze třídy, ze sestavení (assembly) a ze všech potomků třídy protected – chráněný člen je dostupný ze třídy a ze všech potomků třídy private – je defaultní; soukromý člen je dostupný jen z dané třídy Deklarujeme-li člen jako soukromý, bude nepřístupný vně vlastní třídy. A o to jde – datové položky mají být přístupně pouze prostřednictvím metod rozhraní, aby je klientský program nemohl měnit: public class Bod { public int x, y; private int barva = 0x00FFFFFF; } internal class Program { static void Main(string[] args) { Bod a = new Bod(); Console.WriteLine(a.barva); // chyba: typ Bod.barva je nepřístupný } } Platí, že třída či člen třídy nesmí mít širší přístupová práva než kterýkoli typ uvedený v její deklaraci, např. třída nemůže být přístupnější než její předek, metoda nemůže být přístupnější než typ, který vrací: internal class A { … } public class B { public A Metoda(){…}; } // chyba: veřejná metoda nemůže vracet interní typ 5 2. DATOVÉ POLOŽKY Datové položky (atributy) jsou proměnné deklarované na úrovni třídy. Odlišujeme nestatické a statické datové položky. Nestatické datové položky Představují „instanční proměnné“, které obsahuje každý objekt – uchovávají tedy stav objektu. Přistupuje se k nim operátorem tečka se jménem objektu. Jejich deklarace je rozšířena o přístupová oprávnění. Jejich inicializace proběhne po vytvoření objektu. Nejsou-li inicializovány, získají nulovou hodnotu vzhledem ke svému typu (0, 0.0, '\u0000', false, null). public class Zamestnanec { public string jmeno = String.Empty; public string email = String.Empty; private int plat; } internal class Program { static void Main(string[] args) { Zamestnanec z1 = new Zamestnanec(); Zamestnanec z2 = new Zamestnanec(); Console.WriteLine(z1.jmeno); // prázný řetězec Console.WriteLine(z1.plat); // chyba – privátní položka je zde nepřístupná z1.jmeno = "Jan Novák"; // inicializace z1.email = "[email protected]"; Console.WriteLine(z1.jmeno); } } Statické datové položky Představují „třídní proměnné“ – nejsou součástí jednotlivých instancí, ale celé třídy: představují data, k nimž mohou přistupovat všechny instance. Existují od okamžiku spuštění programu, tedy ještě před vytvořením první instance. V jejich deklaraci se užívá klíčové slovo static. V jiných než ve vlastní třídě se ke statickým proměnným přistupuje jménem třídy. public class Zamestnanec { public static int id_organizace = 123; // statická položka public string jmeno = String.Empty; public string email = String.Empty; private int plat; } internal class Program { static void Main(string[] args) { Zamestnanec z1 = new Zamestnanec(); Zamestnanec z2 = new Zamestnanec(); Console.WriteLine(Zamestnanec.id_organizace); // 123 } } 6 Neměnitelné datové položky Modifikátor const deklaruje a současně inicializuje položku, jejíž hodnotu nelze dále měnit. Konstantní položky jsou tedy ve všech objektech stejné a defaultně statické. public class Bod { private int x, y; private const int barva = 0x00FFFFFF; // konstantní (a stejná) barva všech bodů } Není-li hodnota položky známa již při deklaraci, použijeme modifikátor readonly – takovou položku lze inicializovat v konstruktoru nebo při deklaraci, a dál je již neměnná. Položky ke čtení mohou být statické i nestatické – v tomto případě se pak mohou v jednotlivých objektech lišit. public class Bod { private int x, y; private readonly int barva; // neměnná barva každého jednotlivého bodu public Bod(int x, int y, int barva) // „instanční“ konstruktor { this.x = x; this.y = y; this.barva = barva; // inicializace barvy } } internal class Program { public static void Main(string[] args) { Bod b1 = new Bod(5, 8, 0x00FFFFFF); b1.barva = 0xFFFFFFFF; // chyba: položce ke čtení nelze přiřazovat } } public class Bod { private static readonly int implicitníBarva; // neměnná barva všech bodů private int x, y; static Bod() // statický („třídní“) konstruktor { Console.Write("Zadej implicitní barvu"); Bod.implicitníBarva = int.Parse(Console.ReadLine()); } } 7 3. KONSTRUKTORY Konstruktor je speciální metoda, která se volá automaticky v okamžiku vytvoření objektu – slouží k inicializaci datových položek objektu, které nebyly inicializovány přímo v deklaraci. Konstruktor má stejné jméno jako třída, jejíž je součástí, je veřejný a nemá návratový typ. public class Zamestnanec { public string jmeno; // 1. deklarace datových položek public int plat; public Zamestnanec(string _jmeno) // konstruktor { jmeno = _jmeno; // 2. inicializace datových položek } } internal class Program { static void Main(string[] args) { Zamestnanec z1 = new Zamestnanec("Jan Novák"); // volání konstruktoru Console.WriteLine(z1.jmeno); // "Jan Novák" Console.WriteLine(z1.plat); // 0 } } Konstruktor (stejně jako všechny nestatické metody) má k dispozici klíčové slovo this, které v jeho těle odkazuje na aktuální objekt. Pak lze konstruktor – přehledněji – přepsat: public class Zamestnanec { public string jmeno; public int plat; public Zamestnanec(string jmeno) { this.jmeno = jmeno; // uloží parametr do datové položky objektu } } Pokud nevytvoříme vlastní konstruktor, je použit implicitní – bez parametrů a s prázdným tělem. Přetížení konstruktoru Třída může mít více tzv. přetížených (overloaded) konstruktorů, které se liší počtem a/nebo typem parametrů. Při vytváření objektu je pak volán ten, který odpovídá počtem parametrů/typem atributů: public class Zamestnanec { public string jmeno; public int plat; public Zamestnanec(string jmeno) { this.jmeno = jmeno; } public Zamestnanec(string jmeno, int plat) { this.jmeno = jmeno; this.plat = plat; } } 8 Inicializátor Příkaz :this() před tělem konstruktoru slouží k volání jiného konstruktoru téže třídy – tento volaný konstruktor se provede jako první: class Test { public Test(int num1, int num2) { Console.WriteLine("Konstruktor se dvěma parametry."); } public Test(int num) : this(33, 22) { Console.WriteLine("Konstruktor s jedním parametrem."); } public static void Main(String[] args) { Test t = new Test(11); // "Konstruktor se dvěma parametry." } // "Konstruktor s jedním parametrem." } Statický konstruktor Slouží k inicializaci statických („třídních“) datových položek, které nebyly inicializovány již v deklaraci – včetně těch s modifikátorem readonly. Nesmí mít žádné parametry ani specifikaci přístupu. Je volán ještě před vytvořením prvního objektu. public class Bod { private static readonly int implicitníBarva; private int x, y; static Bod() // statický („třídní“) konstruktor { Console.Write("Zadej implicitní barvu"); implicitníBarva = int.Parse(Console.ReadLine()); } public Bod(int x, int y) // „instanční“ konstruktor { this.x = x; this.y = y; } static void Main(string[] args) { Bod bod = new Bod(3, 6); // volání „instančního“ konstruktoru předchází výzva … } // … „Zadej implicitní barvu“ } 9 4. KOMPOZICE OBJEKTŮ Při vytváření nové třídy lze využít existujících – zabudovaných i uživatelských – tříd: objekt odvozené třídy pak může využívat všech jemu přístupných datových položek a metod objektu zanořené třídy. V následujícím případě bude objekt třídy Kostka využívat metody Next() zanořeného objektu třídy Random: public class Kostka { public int početStěn; private Random random; // objekt třídy Random bude součástí objektu třídy Kostka public Kostka() // konstruktor 6ti stěnné kostky { početStěn = 6; // inicializace přiřazením konstanty random = new Random(); // inicializace přiřazením objektu } public Kostka(int početStěn) // konstruktor kostky s libovolným počtem stěn { this.početStěn = početStěn; // inicializace přiřazením hodnoty atributu random = new Random(); } public int Hod() // objekt třídy Kostka … { return random.Next(1, this.početStěn - 1); // … využívá metody objektu třídy Random } } internal class Program { static void Main(string[] args) { Kostka kostka = new Kostka(10); // vytvořením Kostky vytvoříme i objekt Random for (int i = 1; i 0; n--) vysledek *= a; return vysledek; } Console.WriteLine("{0}", Matematika.Umocni(5)); Console.WriteLine("{0}", Matematika.Umocni(5, 3)); Přetížení metod (overloading) je tzv. statickým polymorfismem – proběhne v čase překladu kódu. Lokální proměnné v metodách V metodách (statických i nestatických) lze definovat lokální proměnné; ty nemají specifikaci přístupových práv a nelze je deklarovat jako statické. Vznikají s voláním metody a zanikají při jejím dokončení. Mezi jednotlivými voláními metody se jejich hodnota nezachováná ani nejsou automaticky inicializovány (po vytvoření mají náhodnou hodnotu). 13 Dokumentační komentáře Nápověda v IntelliSense čerpá informace z dokumentačních komentářů uvozených trojlomítkem: /// Třída Bod deklaruje geometrické body v rovině class Bod { private static int početInstancí = 0; private int x, y; /// Metoda VraťSouřadnice zpřístupní souřadnice bodu /// Dvojice souřadnic instance třídy Bod /// Žádné parametry public Tuple VraťSouřadnice() { return Tuple.Create(this.x, this.y); } } XML značka zvýrazní syntaxi jména třídy či jejího členu v kódu, předá souhrnnou informaci o třídě či jejím členu, uvozuje typ návratové hodnoty, uvozuje parametry. Rozšiřující metody Rozšiřující metody (extension methods) umožňují – zdánlivě – přidat do existujícího typu novou instanční metodu. Definují se jako statické ve statické třídě ve vhodném jmenném prostoru – jejich první parametr bude typu, do něhož je chceme přidat, a před jeho specifikaci se přidá klíčové slovo this. Přidejme do typu string metodu Výpis() bez parametrů, která vypíše daný řetězec na konzolu: namespace pomoc { public static class Pomoc { public static void Výpis(this string text) { if (text == null) { text = ""; } Console.WriteLine(text); } } } Pak stačí zpřístupnit jmenný prostor a volat metodu jako instanční: using pomoc; string nápis = "Nápis"; nápis.Výpis(); 14 6. VLASTNOSTI U datových položek požadujeme, abychom měli kontrolu nad jejich změnami, které vyvolávají objekty jiných tříd. Toho dosáhneme tak, že je nastavíme jako privátní a zpřístupníme pomocí veřejných metod. Zapouzdření Princip zapouzdření (encapsulation) deklaruje, že datové položky nejsou z jiných tříd přístupné přímo, ale pouze prostřednictvím metod. Datové položky mají tedy být privátní: public class Uživatel { private int věk; // datové položky jsou přístupné jen z metod třídy private bool plnoletý; private string jméno; public Uživatel(string jméno, int věk) { this.jméno = jméno; this.věk = věk; this.plnoletý = (this.věk >= 18); // nastavení atributu se děje jen v konstruktoru } public void nastavVěk(int věk) { // setter – nastavuje datovou položku this.věk = věk; } public int vraťVěk() { // getter – čte datovou položku return this.věk; } public int vraťPlnoletost() { return this.plnoletý; } } class Program { static void Main() { Uživatel z = new Uživatel("Jan Nový", 20); z.věk = 35; // chyba: položka věk je zde nepřístupná z.nastavVěk(35); Console.Write($"{z.vraťVěk()}"); z.nastavVěk(15); // porušení konzistence: plnoletý zůstává true Console.Write($"{z.vraťPlnoletost()}"); } } Zapouzdření zabraňuje narušení konzistence objektu, např. nastavením nepřípustných hodnot atributů: public void nastavVěk(int věk) { if (věk >= 0 && věk =< 120) // věk lze nastavit jen u určitém intervalu hodnot { this.věk = věk; this.plnoletý = (this.věk >=18); // plnoletost se stává závislou na věku } } 15 Vlastnosti Zapouzdření se realizuje pomocí vlastností (properties), které vně vlastní třídy umožňují čtení a zápis do datových položek provést zdánlivě přímo – ve skutečnosti však voláním getterů a setterů. Nejprve deklarujeme privátní datovou položku (v níž je uložena hodnota), poté veřejnou vlastnost a uvnitř její deklarace dvě metody pro čtení a zápis do datové položky reprezentované klíčovými slovy get a set. Metody get a set nemají parametry, závorky ani typ návratové hodnoty – ten je dán deklarací vlastnosti. Klíčové slovo value reprezentuje hodnotu předávanou metodě set: public class Uživatel { private int věk; // deklarace soukromé datové položky public int Věk { // deklarace veřejné vlastnosti … get { return this.věk; } // … metoda get{} se volá při čtení set { // … metoda set{} se volá při zápisu if (value >= 0 && value = 0 && value , které zkracují zápis metod vynecháním klíčového slova return a závorek {}: private int věk; public int Věk { get => this.věk; private set => this.věk = value; } Lambda výraz lze např. využít pro kontrolu plnoletosti – místo do setteru Věk ji vložit do getteru Plnoletý: public int Věk { get; set; } public bool Plnoletý => this.Věk > 18; 17 Využití vlastností při deklaraci třídy: class Uživatel { public string Jméno { get; init; } // neboť se nebude měnit public bool Plnoletost { get; private set; } // neboť se může změnit se změnou věku private int věk; // ručně, neboť kontrola plnoletosti public int Věk { get { return this.věk; } set { if (value >= 0 && value b) return a; return b; } } int m = Calculate.Max2(100, 200); // … volá se jejím jménem Pokud bychom stejnou metodu definovali jako nestatickou – tím, že v hlavičce vynecháme slovo static – na její spuštění budeme potřebovat objekt třídy, který je nám ovšem úplně k ničemu, neboť nepracujeme s žádnými jeho atributy (neboť ani žádné nemá ). public class Calculate { public int Max2(int a, int b) { // nestatická metoda „patří“ objektům třídy … if (a > b) return a; return b; } } Calculate c = new Calculate(); // … proto se objekt třídy musí nejprve vytvořit … int m = c.Max2(100, 200); // … aby nad ním bylo možné metodu zavolat Takže metody, které nebudou využívat datové položky objektů – pouze dostanou vstupy a vrátí výstupní hodnotu – budeme definovat jako statické. 27 Statické atributy Představují „třídní“ datové složky, které nejsou součástí jednotlivých objektů, ale k nimž mohou všechny objekty přistupovat. Mimo vlastní třídu jsou přístupné jménem třídy. Jazyk C# nepodporuje lokální statické proměnné, tedy deklarované v nějaké metodě. Statické atributy tedy typicky slouží k uložení hodnoty, která má být sdílena všemi objekty třídy nebo která se ve třídě vyskytuje právě jednou: public class Osoba { private static int početOsob = 0; // statické („třídní“) atributy private static int minimálníPlat; private const int id_organizace = 123; // statický a neměnný atribut private string jméno; // nestatické („instanční“) atributy private int pin; public Osoba(string jméno) { this.jméno = jméno; this.id = početOsob++; // nebo: Osoba.početOsob++; } public static int VraťPočetOsob() { // na vrácení statického atributu stačí statická metoda return početOsob; } public string VraťJménoOsoby() { return jméno; } } Osoba jan = new Osoba("Jan"); Osoba anna = new Osoba("Anna"); Osoba.VraťPočetOsob(); // 2 Jako statický se chová také konstantní atribut, deklarovaný modifikátorem const (nikoli static const), jehož hodnota je po inicializace dále neměnná. Statický konstruktor Inicializuje statické atributy, které nebyly inicializovány již v deklaraci – včetně těch s modifikátorem readonly. Nemá žádné parametry ani specifikaci přístupu. Je volán ještě před vytvořením prvního objektu. static Osoba() { minimálníPlat = 20000; } Ve statickém konstruktoru ale lze – stejně jako v instančním konstruktoru – vytvořit instance nějakých tříd a uložit je do statických proměnných. 28 Statické členy: Q & A Lze statickou metodu volat jako nestatickou, tj. nad konkrétním objektem dané třídy? Ano, lze, neboť statická metoda „patří“ všem objektům – ale volání nad třídou je logičtější. Osoba anna = new Osoba("Anna"); anna.VraťPočetOsob(); // OK Lze nestatickou metodu volat jako statickou? Nedává to smysl! Nestatická metoda přece pracuje s konkrétním objektem a jeho atributy – a ty se navíc nastaví až po zavolání jeho konstruktoru. Osoba.VraťJménoOsoby(); // Nelze! Lze ze statické metody přistoupit k nestatickému atributu, tj. k datové položce určitého objektu? Přímo nikoli. Opět – nedává to smysl. public static int VraťPinOsoby() { return anna.pin; // Nelze! } Ale možné to je, pokud je objekt předán statické metodě jako parametr nebo je v ní vytvořen: public static int VraťPinOsoby(Osoba o) { return o.pin; // OK } public static int VraťPinOsoby() { Osoba anna = new Osoba("Anna"); return anna.pin; // OK } Je účelné využívat statické metody? Ano – vždy, když metoda nepracuje s daty objektů, ale „jen“ přijímá vstupní parametry a vrací výstupní hodnotu, tzn. když se chová jako funkce. Je účelné využívat statické atributy? Jejich použití je třeba dobře zdůvodnit: „Opravdu budeme danou hodnotu v dané třídě potřebovat vždy právě jednou?“ Často je vhodnější aplikovat návrhový vzor singleton, tedy definovat třídu, která bude soustřeďovat (jako nestatické atributy) ty údaje, které mají být statické v jiných třídách 29 Singleton Singleton je nestatická třída, která se sama stará o to – pomocí prázdného privátního konstruktoru – aby její instance mohla existovat právě jednou, tedy aby ji nebylo možné z vnějšku vytvářet pomocí new, ale voláním k tomuto účelu určené statické metody. Tím, že je singleton unikátní, může soustřeďovat – jako své nestatické atributy – třídní atributy jiných tříd. public class PočítadloOsob // SINGLETON { private PočítadloOsob() { } // privátní a prázdný konstruktor - brání použití new private static PočítadloOsob počítadlo = null; public static PočítadloOsob VytvořPočítadloOsob() // vytvoří objekt singletonu, … { // … jen pokud ten neexistuje if (počítadlo == null) { počítadlo = new PočítadloOsob(); // volá privátní konstruktor } return počítadlo; } private int početOsob = 0; // reprezentuje sdílený "třídní" atribut pro třídu Osoba public void PřidejOsobu() { this.početOsob++; } public int VraťPočetOsob() { return this.početOsob; } } public class Osoba { private string jméno; public Osoba(string jméno) { this.jméno = jméno; } public string VraťJménoOsoby() { return this.jméno; } } class Program // klientský kód { public static void Main() { PočítadloOsob p = PočítadloOsob.VytvořPočítadloOsob(); Osoba jan = new Osoba("Jan"); p.PřidejOsobu(); Osoba anna = new Osoba("Anna"); p.PřidejOsobu(); Console.WriteLine(p.VraťPočetOsob()); } } 30 Statické třídy Má-li třída obsahovat jen pomocné metody, definujeme ji jako statickou. Statická třída obsahuje pouze statické členy, nelze od ní dědit, nelze – operátorem new – vytvořit její instanci (tzn. obsahuje privátní konstruktor) a k jejím členů se tedy přistupuje pomocí názvu třídy. Příkladem je třída System.Math, jejíž metody provádějí mat. operace bez nutnosti ukládat či načítat data: double d = -3.14; Math m = new Math(); // Nelze vytvořit instanci statické třídy! Console.WriteLine(Math.Abs(d)); // 3.14 Console.WriteLine(Math.Floor(d)); // -4 Console.WriteLine(Math.Round(Math.Abs(d))); // 3 Vytvořme statickou třídu obsahující dvě metody pro vzájemný převod stupňů Celsia a Fahrenheita: public static class PřevodníkTeplot { public static double Celsius2Fahrenheit(string stringCelsius) { double celsius = Double.Parse(stringCelsius); return (celsius * 9 / 5) + 32; } public static double Fahrenheit2Celsius(string stringFahrenheit) { double fahrenheit = Double.Parse(stringFahrenheit); return (fahrenheit - 32) * 5 / 9; } } class Program { static void Main() { Console.WriteLine("Zadej směr převodu:"); Console.WriteLine("1. Celsius -> Fahrenheit"); Console.WriteLine("2. Fahrenheit -> Celsius"); Console.Write(": "); string? volba = Console.ReadLine(); double F, C; switch (volba) { case "1": Console.Write("Zadej stupně Celsia: "); F = PřevodníkTeplot.Celsius2Fahrenheit(Console.ReadLine() ?? "0.0"); Console.WriteLine("Teplota ve stupních Fahrenheita: {0:F2}", F); break; case "2": Console.Write("Zadej stupně Fahrenheita: "); C = PřevodníkTeplot.Fahrenheit2Celsius(Console.ReadLine() ?? "0.0"); Console.WriteLine("Teplota ve stupních Celsia: {0:F2}", C); break; default: Console.WriteLine("Neplatná volba."); break; } Console.ReadKey(); } } 31 17. VÝJIMKY Nastane-li za běhu programu neočekávaná situace („chyba“), je vyvolána výjimka (exception), což je objekt obsahující informaci o důvodu vzniku vyjímečného stavu – aby bylo možné jej ošetřit. Výjimka je instance jedné ze tříd odvozených od třídy SystemException, nebo může být uživatelsky definovaná. string vstup; while (true) { try // chráněný blok { Console.Write("Zadej celé číslo (nebo Enter pro ukončení): "); vstup = Console.ReadLine(); if (vstup == String.Empty) break; int číslo = int.Parse(vstup); // nastane-li výjimka, předá se řízení obsluze Console.WriteLine($"Bylo zadáno číslo {číslo}"); } catch (Exception e) // obsluha výjimky { // vlastnost e.Message nese hlášení o chybě Console.WriteLine("Byla zachycena výjimka {0}", e.Message); } finally // „cleaning block“ { Console.WriteLine("Díky za zadání..."); // provede se vždy } } Kód, v němž může nastat výjimka, se uzavře do chráněného bloku try. Následuje jeden či více bloků obsluhy catch, které ošetří zadaný typ výjimky – provede se vždy ten první, který odpovídá typu výjimky. Je-li jako typ výjimky uveden Exception (nebo – vhodněji – není-li uveden žádný typ), jsou ošetřeny všechny typy. V nepovinné koncovce finally, která se provede vždy, dojde k úklidu prostředků, např. k uzavření souborů či databáze. Nepovinný je rovněž identifikátor výjimky – uvede se, pokud je třeba pracovat s vlastnostmi výjimky, např. Message, která nese textové hlášení o výjimce. Hierarchie tříd výjimek 32 Řazení bloků obsluhy Bloky obsluhy se řadí od specifických typ výjimek k obecnějším, tj. typ potomka předchází typ rodiče (neboť potomek může zastoupit rodiče): Console.WriteLine("Zadej dělence a dělitele: "); try { string vstup_a = Console.ReadLine(); string vstup_b = Console.ReadLine(); int a = int.Parse(vstup_a); int b = int.Parse(vstup_b); int c = a / b; Console.WriteLine($"Celočíselný podíl zadaných čísel: {c}"); } catch (DivideByZeroException) // specifická výjimka (potomek) { Console.WriteLine("Pokus o dělení nulou."); } catch (ArithmeticException e) // obecnější výjimka (rodič) { Console.WriteLine("Jiná aritmetická výjimka: {0}", e); } catch // zachytí všechny výjimky { Console.WriteLine("Oops... Něco se pokazilo..."); } Vyvolání výjimky Příkazem throw se výjimka eskaluje, nebo se vytvoří nová instance výjimky zadaného typu. Nepoviným argumentem lze výjimce předat zrávu Message: string vstup; List známky = new List(); while (true) { Console.WriteLine("Zadej známku 1 až 5, nebo Enter: "); try { vstup = Console.ReadLine(); if (vstup == String.Empty) break; int známka = int.Parse(vstup); if (známka < 1 || známka > 5) throw new IndexOutOfRangeException("Neplatná známka"); známky.Add(známka); Console.WriteLine($"Uložena známka {známka}"); } catch (IndexOutOfRangeException e) { Console.WriteLine("Výjimka: {0}", e.Message); } catch (FormatException e) { Console.WriteLine("Výjimka: {0}", e.Message); } } 33 Šíření výjimky Vznikne-li výjimka mimo blok try nebo neexistuje-li pro ni obsluha, přejde výjimka do volající metody. Není-li nalezena obsluha ani v metodě Main(), je výjimka předána prostředí.NET a program je ukončen („Neošetřená výjimka.“) static float Vydel(int a, int b) { if (b == 0) throw new DivideByZeroException("Pokus o dělení nulou"); return a / b; } static void FunkceVolajiciVydel() { try { float vysledek = Vydel(4, 2); Console.WriteLine($"Vysledek: {vysledek}"); } catch (DivideByZeroException e) { Console.WriteLine($"{e.Message}"); } } FunkceVolajiciVydel(); 34 18. SOUBORY C# rozlišuje soubory (file) a proudy (stream). Soubor je množina dat, s níž zacházíme jako s logickým celkem (nezávisle na běhu programu), proud si vytváří program jako nástroj pro čtení a zápis do souboru. Třídy pro práci se soubory a proudy Nástroje pro práci se soubory i proudy najdeme ve jmenném prostoru System.IO. Třídy odvozené od abstraktní třídy Stream slouží pro čtení/zápis do souboru. Tvoří podkladové proudy pro další třídy, neboť umějí zapsat pouze jednotlivé bajty (pole bajtů). Jejich služeb využívají tři skupiny tříd: - třídy odvozené od abstraktní třídy TextWriter (sloužící pro zápis textových dat) - třídy odvozené od abstraktní třídy TextReader (sloužící pro čtení textových dat) - třídy BinaryReader a BinaryWriter (sloužící pro zápis a čtení binárních dat) Třídy File, FileInfo, Directory a DirectoryInfo slouží k maniplaci se soubory a adresáři. Třída Path představuje sadu nástrojů pro manipulaci se jmény souborů a adresářů. Práce se soubory Třída File nabízí statické metody Create(), Delete(), Copy(), Move(). Metoda Exists() typu bool testuje existenci zadaného souboru. Metoda Open() otevře soubor a vrátí instanci třídy FileStream ke čtení nebo zápisu do něj. Metody OpenWrite() a OpenRead() otevřou proud pouze pro čtení, resp. zápis. Soubor se zadá jako řetězec i s cestou. using System.IO; string jménoSouboru = @"C:\Temp\Test.txt"; // znak @ escapuje celý text try { if (File.Exists(jménoSouboru)) { Console.WriteLine("Soubor existuje... Bude smazán"); File.Delete(jménoSouboru); } else { Console.WriteLine("Soubor neexistuje... Bude vytvořen"); File.Create(jménoSouboru); } } catch (Exception e) { Console.WriteLine(e.ToString()); } 35 Třída FileInfo souží k reprezentaci konkrétního souboru. Její metody Create(), Delete(), CopyTo(), MoveTo() jsou nestatické tj. pracují se souborem, který daná instance reprezentuje. Vlastnost Exists testuje existenci souboru, vlastnost Name vrací jméno souboru, vlastnost FullName vrací jméno včetně cesty. using System.IO; FileInfo soubor = new FileInfo(@"C:\Temp\Test.txt"); try { if (soubor.Exists) { // vlastnost Console.WriteLine("Soubor existuje... Bude smazán"); soubor.Delete(); } else { Console.WriteLine("Soubor neexistuje... Bude vytvořen"); soubor.Create(); } } catch (Exception e) { Console.WriteLine(e.ToString()); } Při kopírování a přesunu souboru druhý, nepovinný logický parametr umožní přepsat původní soubor, pokud ten existuje: using System.IO; FileInfo file = new FileInfo(@"C:\Temp\Test.txt"); if (file.Exists) { Console.WriteLine("Soubor bude nakopírován... "); file.CopyTo(@"C:\Temp\Test2.txt", true); // ex.-li, bude přepsán } else { Console.WriteLine("Soubor neexistuje... Bude vytvořen"); file.Create(); } 36 Práce s adresáři Třída Directory nabízí statické metody GetFiles() a GetDirectories(), které vrací pole řetězců jmen souborů, resp. adresářů obsažených v daném adresáři. Metoda GetCurrentDirectory() vrátí a metoda SetCurrentDirectory() nastaví aktuální adresář. Třída DirectoryInfo nabízí analogické nestatické metody, které pracují s instancí určitého adresáře. using System.IO; // vypíše obsah zadaného adresáře static void VypišAdresář(DirectoryInfo adresář) { Console.WriteLine("Výpis adresáře " + adresář.FullName); DirectoryInfo[] podadresáře = adresář.GetDirectories(); FileInfo[] soubory = adresář.GetFiles(); if (soubory.Length > 0) { Console.WriteLine("Soubory: "); foreach (FileInfo soubor in soubory) { Console.WriteLine($"\t{soubor.Name} \t({soubor.Length} B)"); } } if (podadresáře.Length > 0) { Console.WriteLine("Podadresáře: "); foreach(DirectoryInfo podadresář in podadresáře) { Console.WriteLine($"\t{podadresář.Name}"); } } } string cesta = @"c:\temp"; DirectoryInfo dir = new DirectoryInfo(cesta); VypišAdresář(dir); 37 Vstupy a výstupy Při operacích čtení a zápisu se používají podkladové třídy odvozené od třídy Stream – pro práci se soubory jde o třídu FileStream. Konstruktor FileStream() má tři parametry: – řetězec s názvem souboru – FileMode určující, zda požadujeme otevření existujícího souboru nebo vytvoření nového (Append, Create, CreateNew, Open, OpenOrCreate, Truncate) – FileAccess určující, zda chceme data číst a/nebo zapisovat (Read, Write, ReadWrite). K tomuto proudu se dále připojí „filtr“ pro práci s určitým typem dat – BinaryWriter a BinaryReader pro binární data, StreamReader a StreamWriter pro textová data. Operace s proudem budeme uzavírat do příkazu using, který udržuje kontext a automaticky proud uzavře. Čtení a zápis binárních dat Zápis binárních dat provede metoda Write(), která přijme atomické typy dat, string a pole bajtů – v režimu FileMode.Create vytvoří nový (příp. přepíše existující) soubor, v režimu FileMode.Append připíše data na konec existujícího souboru. using System.IO; // zápis binárních dat static void ZapišBinárně(string soubor) { using (BinaryWriter zapisovač = new BinaryWriter( new FileStream(soubor, FileMode.Create, FileAccess.Write))) // resp. FileMode.Append { for (int i = 0; i < 10; i++) { zapisovač.Write(i); } } } ZapišBinárně(@"c:\temp\test3.dat"); Při čtení binárních dat je třeba testovat konec souboru – při pokusu o čtení za koncem souboru by byla vrácena výjimka EndOfFileException. Odkaz na aktuální pozici čteného souboru nese vlastnost Position. K vlastnímu čtení dat slouží řada metod, jako ReadInt32(), ReadChar(), aj. using System.IO; // čtení binárních dat static void ČtiBinárně(string soubor) { using (FileStream podklad = new FileStream(soubor, FileMode.Open, FileAccess.Read)) using (BinaryReader čtečka = new BinaryReader(podklad)) { while (podklad.Position < podklad.Length) // test konce souboru { int i = čtečka.ReadInt32(); Console.WriteLine(i); } } } ČtiBinárně(@"c:\temp\test3.dat"); 38 Čtení a zápis textových dat Zápis textových dat povede metoda Write() či WriteLine() filtru StreamWriter, který otevře nový (či přepíše existující) soubor. Chceme-li připsat data na konec existujícího souboru, zavoláme konstruktor filtru s druhým parametrem true. Nakonec je nutno metodou Flush() postarat se o vypráznění bufferu. using System.IO; // zápis textových dat static void ZapišTextově(string soubor) { using (StreamWriter zapisovač = new StreamWriter(soubor)) { // příp. StreamWriter(soubor, true) zapisovač.WriteLine("První řádek"); zapisovač.WriteLine("Druhý řádek"); zapisovač.Flush(); } } ZapišTextově(@"c:\temp\test3.txt"); Čtení textových dat provede metoda ReadLine() filtru StreamReader, která vrací řádek textu ze souboru a zároveň se přesune na řádek následující. Testování konce souboru se děje pomocí kontroly, zda proběhlo přiřazení nové řádky do proměnné. using System.IO; // čtení textových dat static void ČtiTextově(string soubor) { using (StreamReader čtečka = new StreamReader(soubor)) { string s; while ((s = čtečka.ReadLine()) != null) { Console.WriteLine(s); } } } ČtiTextově(@"c:\temp\test3.txt"); 39