# Programování v C\# - zkouška bude podobná jako u Principů - principy Pythonu - přiřazení do proměnné vytvoří objekt na garbage-collectované haldě s daty, jejich délkou a overheadem (obsahuje ref. count a datový typ) - odkaz (reference) na tento objekt se uloží do lokální paměti na místo vyhrazené dané proměnné - při provádění operací pomocí přetížených operátorů se kontrolují datové typy proměnných (uvnitř objektů na haldě) a podle toho se provede vhodná operace - v C/C++ - ukládá se rovnou hodnota, nevytváří se žádný objekt ani reference – proměnné se vytvářejí rovnou na místě (v lokální paměti) - objekty se taky ukládají do lokální paměti - pointery se chovají dost podobně – obsahují adresu proměnné - halda - na haldě se objekty alokují pomocí klíčového slova `new` - alokátor si udržuje overhead - dealokace se provádí explicitně pomocí `delete` - rozdíl mezi třídou a strukturou je ten, že obsah struktury je defaultně public - ochranu přístupu (public/private) kontroluje překladač, jinak se nikam neukládá - C# - .NET - běhové prostředí (runtime), mj. alokátor proměti - standardní knihovny - v dotnetu může (kromě C#) běžet víc jazyků – např. Visual Basic .NET, F#, … - jsou věci, které platí pro C#, a věci, které platí pro .NET ## Typy - základní dělení všech typů - pointery – jsou ošklivé, nebudeme se o nich bavit - hodnotové typy – alokované na místě (s výjimkami) - enums - structures - simple types (Int32, Int64, Double, Boolean, Char, …) - nullables - user defined structures (struct) - referenční typy – alokované na spravované (managed) haldě - classes (e.g. strings) - interfaces - arrays - delegates - halda je garbage-collectovaná (GC), funguje chytřeji než v Pythonu, není potřeba reference counter, ale používá se graf dosažitelnosti - overhead u každého objektu na haldě (má typicky 16 B) - syncblock – kvůli práci s více vlákny (u jednovláknových programů zbytečný), má 8 B - pointer na typ (zjednodušeně řečeno) - třída System.Type, má instance na GC haldě - každý datový typ odpovídá jedné instanci třídy - na referenčních proměnných jde volat `.GetType()` - `new` na hodnotovém typu (např. u typu struct) nealokuje nikde nic - dotnetový typ `System.Int16` → C# klíčové slovo `short` - pokud klíčové slovo existuje, používáme ho - v Javě se hodnotové typy označují malým písmenem, referenční velkým – v C# nic takového neplatí - dotnet se vyvíjí rychleji než C#, takže např. System.Half ještě nemá klíčové slovo v C# - odbočka: CLS compliant = všechny jazyky dotnetu musí tenhle typ podporovat (problém je hlavně s Javou, když se používá v dotnetu, všechny ostatní jazyky dotnetu nejspíš podporují všechny dotnetí typy) - při přiřazování se v C# kopíruje obsah proměnné nezávisle na typu (takže u referenčních typů se kopíruje odkaz do GC haldy, u hodnotových typů samotná hodnota) - třídy a struktury můžeme anotovat slovem `record` - např. `record class C {}` - při běhu se takové třídy/struktury chovají klasicky - umožní nám to psát méně boilerplate kódu, protože nám C# překladač vytvoří nějaké chytré metody (např. lepší ToString) - místo `record class` se dá napsat jenom `record` - jak rozlišit, kdy použít referenční a kdy hodnotový typ - syntaktický cukr – `var` a `new()` - fields a ochrana přístupu (public, readonly, …) - properties – gettery, settery - auto-implemented props - automaticky se vytvoří backing field - props bez setteru jsou readonly - getter a setter můžou mít různou viditelnost (celku se nastaví nějaká viditelnost, u getteru nebo setteru se pak napíše jiná viditelnost) - do readonly věcí můžu zapisovat v konstruktoru - výchozí hodnoty typů - za `new X()` můžu do složených závorek napsat do složených závorek nastavení hodnot fieldů (ale je to syntaktický cukr pro nastavení hodnot fieldů po skončení běhu konstruktoru, úroveň přístupu odpovídá úrovni okolního kódu – takže to funguje podobně, jako bychom přiřazení provedli až dodatečně na dalším řádku; jediný rozdíl spočívá v klíčovém slově `init`) - ve složených závorkách za `new X()` je inicializér - klíčové slovo `init` se používá podobně jako `set` (nedají se použít zároveň), akorát `init` umožňuje přiřazení pouze v konstruktoru nebo v inicializéru - klíčové slovo required – danému fieldu/property musím nastavit hodnotu do konce běhu inicializéru - u bezparametrických konstruktorů nemusím psát závorky, takže stačí `new X` - primary constructors – v C# 12, fieldy píšu do závorky za `class C` - můžu definovat property vycházející z nějakého fieldu - C# 10 umožňuje u record class automaticky vygenerovat vlastnosti tímto způsobem - u record class (v C# 11) jsou vlastnosti immutable (mají get, init) - u record struct jsou vlastnosti mutable (mají get, set) - u readonly record struct jsou vlastnosti immutable (mají get, init) - klíčové slovo record se hodí na vytváření jednoduchých „datonosných“ tříd - nullable hodnotové typy – pomocí otazníku, vytvoří se generická struktura Nullable, je tam boolean vlastnost HasValue - lze přetěžovat porovnání, takže někdy může být problém porovnávat s nullem → může se hodit použít operátor `is` nebo `is not` - `null` původně vychází z referenčních typů - `null` = neplatná reference - `null` se dá běžně používat u referenčních typů - proměnná jakoby odkazuje na adresu 0 (což je ale v operačním systému neplatné mapování) - runtimu dotnetu se říká CLR (Common Language Runtime) - CLR u dereferencí proměnných (vlastností apod.) uvnitř proměnných referenčních typů kontroluje, jestli referenční proměnné nejsou null (tedy např. `x = null; Console.WriteLine(x.y);` vyhodí NullReferenceException) - pokud se pokusíme vypsat null, tak Console.WriteLine automaticky vypíše prázdný řetězec a nevolá ToString (kdybychom ToString zavolali ručně, tak se vyhodí NullReferenceException) - trik s rychlým překomentováním pomocí `/**/` a `/*/` - nová sémantika referenčních typů od C# 8 - v ReCodExu je vypnutá, ve Visual Studiu je obvykle zapnutá - když do zdrojáku napíšu `#nullable enable`, tak se zapne nová sémantika - jakmile se zapne, referenční typy nejsou nullable, je potřeba to zapínat otazníkem u každé proměnné - pak se můžu spolehnout na invariant, že daná proměnná nikdy nebude null - pokud do non-nullable referenční proměnné přiřadím null, tak to vypíše warning - statická analýza neumí říct stoprocentně správně, jestli je přiřazení nullu v danou chvíli špatně, takže nevrací error, ale jenom warning ## Kontrakt a rozhraní (interfaces) - koncepční rozdíl mezi C# a Pythonem - Python – duck typing - ke kachní poště nepotřebujme kachnu, ale něco, co je kachně dostatečně podobné v těch důležitých vlastnostech - implicitně kódem sděluju, co potřebuju od dané proměnné - můžu danému kódu přiřadit cokoliv, co dané vlastnosti splňuje (kontroluje se to až za runtimu) - duck typing funguje i v C++ u generických typů (ale nejčastěji se objevuje v dynamicky typovaných jazycích) - C# - definuju si kontrakt - někdo ho implemenuje (slibuje, že má minimálně metody předepsané kontraktem) - někdo ho využívá (slib, že používá maximálně metody z kontraktu) - lze to vynucovat více způsoby, ale nejtypičtější jsou interfacy - interface – referenční typ - konvence psát na začátek názvu `I` - dovnitř se píšou metody / method-like věci (třeba props), nesmí tam být fields - jeden řádek může být např. `public int m(int a, int b);` - dříve muselo být všechno public, dneska je dobré tam to public explicitně psát (i když defaultně je pořád všechno public) - názvy parametrů metod v interfacu slouží k dokumentaci - nemůže existovat instance interfacu, pouze proměnná s typem interfacu (ta může být nullable) - `class Kachna : IPostovniZvire {…}` - `IPostovniZvire zvire1 = new Kachna();` - třída může implementovat víc interfaců - přístup CLR k implementaci metod - synovská třída dědí implementaci metody u rodiče - každý interface u třídy vytvoří jakoby tabulku metod, ta se zaplní reálnými implementacemi (respektive ukazateli na implementace) - obecně může nastat, že rodič interface neimplementuje, ale syn ano - tabulky jsou uloženy v instanci třídy Type (v overheadu daného objektu na haldě) - typ proměnné rozhoduje, jakou metodu volám - je to důležité při zakrývání metod pomocí `new` - pokud je to interfacová metoda, tak záleží na tom, kam ukazuje tabulka, což vyplývá z toho, která třída implementuje interface - pokud chci s nějakým objektem (typicky z interfacu nebo taky u syna rodičovské třídy) zacházet speciálně podle konkrétní třídy, jejíž je instancí, můžu použít operátor `is` a pak ho castnout - klasický zápis: `if (a is B) {B b = (b)a; …}` - zkrácený zápis: `if (a is B b) {…}` - pokud C dědí od B, přičemž B dědí od A, pak instanci třídy C lze takto castnout do B i C (a samozřejmě i do A) – tzn. pro všechny tyto typy vrací operátor `is` hodnotu `true` ## Překlad a distribuce programů - C++ - zdrojáky .cpp, jeden po druhém je překládáme, vznikají object fily .obj (nebo .o), tam je přímo strojový kód - ty se linkerem zpracují do jednoho .exe souboru - strojový kód typicky cílí na konkrétní platformu - kdybych chtěl, aby program běžel na jiné platformě, vezmu zdrojový kód a celý ho znova přeložím - 64bitové Windows podporují 32bitové programy - uživatelé musí vědět, jakou verzi programu si mají vybrat - knihovny - překladač programu používá hlavičkové soubory knihoven - Apple - Apple 6502 → Motorola 68000 (32bit) → IBM PowerPC (32bit) → Intel x86 (32bit) → Intel x64 (64bit) → Apple M1/M2 (ARM64, 64bit) - vymysleli fat binary (universal executable) – jeden spustitelný soubor, v něm je více verzí programu najednou - řeší to problém zpětné kompatibility, ne dopředné - C\# - soubory .cs - .csproj, předává se build systému dotnetu, ten pustí C# compiler csc.exe (dnes css.dll) - vypadne z toho executable, uvnitř je CIL kód (common intermediate language) - ke spuštění programu potřebujeme CLR (common language runtime), obvykle je tam JIT (just-in-time) překladač, ten zajišťuje překlad a spuštění pro konkrétní platformu - CLR … virtual machine (VM) - CIL kód … managed/řízený kód - dotnet executable soubor … assembly - Java to má podobně - Java intermediate language = Java bytecode - všechny programovací jazyky dotnetu se překládají do CIL kódu - optimalizace dělá až JIT - ale má na to omezený čas, aby se program spustil dostatečně rychle - JIT používá překlad po metodách - ale dá se použít AOT (ahead of time) překlad – takže pak v .exe souboru je nejen assembly (ten se použije pro nepodporované platformy), ale taky přímo strojový kód, který tak může být efektivnější - některé věci tam nefungují - knihovna se překládá velmi podobně - .csproj + .cs soubory - csc přeloží na .dll soubor - rozdíl mezi .exe a .dll není skoro žádný, akorát .exe má entry point (dneska může být i executable .dll s entry pointem, takže podle přípony to není poznat) - v .exe/.dll souboru je vždy CIL kód a metadata - když v programu používám knihovnu, odkazuju se při překladu přímo na její .dll soubor (metadata uvnitř) - v metadatech musí být v zásadě celá hlavička metody (včetně názvů parametrů – kvůli dokumentaci a explicitnímu přiřazení) - od dotnetu 5 - a.exe soubor … Windows loader - spustí správný soubor clr.dll - jeho pomocí spoustí a.dll soubor - nástroj dotnet - obsahuje SDK toolchain - provádí nalezení správného souboru clr.dll a spuštění a.dll souboru (to je užitečné hlavně mimo Windows) - stačí napsat `dotnet a.dll` - `dotnet a.exe` nefunguje, protože exe soubor se nedá spustit jako dotnetí - pomocí nástroje ildasm.exe se můžeme podívat dovnitř metadat a CIL kódu - ILSpy je dekompiler, který zkouší obnovit původní kód - kromě dll a exe souboru se k programu obvykle generuje i .pdb soubor s informacemi k debugování (typicky názvy lokálních proměnných) - ten pak používá i ILSpy, pokud je k dispozici ## Dědičnost - dědičnost/inheritance - class A - class B : A - dědění (kopírování dat) probíhá až v JIT - v B je všechno, co bylo v A, kromě konstruktoru - instanční - fields - methods - statické - fields - methods - instanční metoda má implicitní parametr this, který je typu dané třídy - u rodiče se někdy hodí klíčové slovo abstract, to zakazuje vytváření instancí dané třídy - třída může implementovat libovolné množství interfaců - každá třída má právě jednoho předka (pokud jsme žádného nedefinovali, tak je to Object) - typy v C# (kromě pointerů) tvoří strom - konstruktory se nedědí - konstruktor můžu chápat jako instanční metodu - volání rodičovského konstruktoru zajišťuje klíčové slovo base - `public B(params1):base(params2) {` - není to optional, rodičovský konstruktor se volá pokaždé (defaultně bez parametrů) - konstruktor předka se vždy volá před konstruktorem potomka - `a is B` vs. `a.GetType() == typeof(B)` - první varianta se ptá, jestli to, co je v `a`, je přiřaditelné do typu `B` - druhá varianta se ptá, jestli typy přesně odpovídají - druhá varianta je mírně rychlejší - `is` za runtimu vyhodnocuje, zda instance splňuje nějakou podmínku - `a is PODMINKA-NA-INSTANCI` - `a is null` - skládání podmínek se provádí pomocí „slovních“ operátorů `and`, `or`, `not` - pro nullable typ `Person?` můžu provést kontrolu pomocí `is not null` nebo `is Person person` - operátor `.` vs. null-forgiving operátor `?.` - `a.Length` vs. `a?.Length` - obdobně `a[index]` vs. `a?[index]` - typ výsledku se liší v nullabilitě (pokud by `a.Length` bylo typu `X`, `a?.Length` bude typu `X?`) - příklad použití ke zpřehlednění kódu - zadání: nemá platit, že x je menší než 4 - implementace 1: `x >= 4` - implementace 2: `!(x < 4)` - implementace 3: `x is not < 4` - skládání: `x is not null and not < 4` - pokud by `x` byla vlastnost (property), tak se getter zavolá pouze jednou (pro klasické boolovské výrazy by se volal dvakrát: `x != null && x >= 4`) - dá se použít syntaktická zkratka `person is { FirstName: "Vít", LastName: "Kološ" }` - občas se místo not null používá `is {}`, ale není to moc praktické - dá se to složit na `person is Employee { Salary: > 500, LastName: "Kološ" } employee` - zároveň můžu rovnou přiřadit do proměnné `person is Employee { Salary: > 500, Salary: var salary, LastName: "Kološ" } employee` ## Konstanty, konstruktory, výčty - občas je praktické uložit nějakou hodnotu jako konstantu - konstanty uvnitř tříd - `readonly int x1;` – ukládá se u každé instance, což se nám nehodí - `static readonly int x2;` - je uložený pouze jednou, ale pracuje se s ním jako s proměnnou (ve strojovém kódu se vykonává load z paměti…) - `const int x3 = 5;` - je to konstanta pro celou třídu, nezabírá to místo, hodnota je uložena přímo v metadatech, překladač optimalizuje počítání s konstantami (takže předem provede některé výpočty; této optimalizaci se říká constant folding) - do takové konstanty lze uložit jen základní hodnotové typy a stringy – pokud nám to nestačí, tak musíme použít static readonly - pokud konstanty používáme v knihovnách, tak se může stát, že vydáme novou verzi knihovny, někdo si ji stáhne, ale přitom bude mít starou verzi programu, v níž bude „zakompilovaná“ stará konstanta ze staré verze knihovny - class constructor - metoda bez parametrů, volá ji automaticky CLR - zavolá se před prvním použitím toho typu jako takového - není úplně jasné, kdy přesně se zavolá - pokud se daný typ používá a je možné, že jeho class constructor ještě nebyl zavolán, provede se kontrola tohoto zavolání a následně se případně zavolá - může nastat situace, že by se tato kontrola prováděla při každém volání funkce, kterou je ale potřeba volat mnohokrát – v takovém případě může být vhodné této kontrole zabránit např. vytvořením zbytečné (prázdné) instance objektu daného typu někdy na začátku programu - class konstruktor se generuje automaticky, ale dá se napsat ručně – název metody odpovídá názvu třídy, použije se klíčové slovo static, viditelnost se neuvádí - i když to vypadá, že se volá víc konstruktorů u dané třídy (typicky pomocí `: this(…)`), tak na úrovni CIL kódu tu instanci inicializuje jenom jeden z těch konstruktorů – ten, který se volá jako první (tedy ten `this(…)`) - dovnitř se automaticky generují inicializace fieldů u objektu - kód ve složených závorkách za this() se provede až po volání tohoto konstruktoru - nejdřív se inicializují fieldy a volají se funkce potřebné k jejich inicializaci → pak se volá this() nebo base() → pak se provádí kód ve složených závorkách - tohle je vlastnost C#, ne CLR - když zkusíme během inicializace fieldů přistoupit k neinicializovanému fieldu, tak tam bude nula (tzn. 0, null nebo false) - konstruktor se v CLR označuje jako `.ctor`, class konstruktor jako `.cctor` - namespaces jsou syntaktická zkratka C#, CLR zná třídy pouze pod jejich celými jmény (tzn. namespace.třída), přičemž pro něj tečka nemá žádný speciální význam (pro C# samozřejmě ano) - dalším výrazem, jehož hodnota se generuje za překladu, je nameof(proměnná) - typické použití k uvedení jména argumentu při vyhození ArgumentException - nameof(x) je ekvivalentní se zápisem "x", ale když proměnnou x přejmenuju, tak na "x" snadno zapomenu - když píšu číslo a chci ho zpřehlednit, tak můžu použít podtržítko - např. `int speed = 300_000;` - výčtové typy (enum) - `enum Season { Spring, Summer, Autumn, Winter }` - hodnotový typ - předává se jako int (nebo jiný základní typ) - s proměnnými můžu provádět aritmetické operace, ale musím hlídat, jestli se nedostanu mimo definované hodnoty (pak se s proměnnou zachází jako s klasickým číslem) - metoda ToString defaultně vrací název dané hodnoty v enumu (pro nedefinovanou hodnotu vrátí číslo) - lze ručně nastavit hodnoty, pokud danou hodnotu nenastavím, tak se vezme předchozí hodnota a zvýší se o jedna - hodnoty můžou být i stejné – pak se tyto hodnoty při porovnání rovnají, ToString vypíše tu, která je v seznamu uvedena jako první - někdy se enum používá jako bitová maska – takže jako hodnoty přiřadíme mocniny dvojky, s označením `[Flags]` tento přístup podporuje i ToString (vypíše seznam „zvolených hodnot“) - existuje *hodně pomalá* metoda Enum.Parse, která pro zadaný název hodnoty v enumu dokáže najít číslo (funguje i pro Flags) - používá koncept reflection - klíčové slovo using umožňuje nastavit alias pro název typu (např. `using X = Console;`), funguje i global using - má to smysl jen ve výjimečných situacích (pokud má něco krkolomný název) - situace: chceme mít uložený řádek a sloupec tabulky a chceme předejít tomu, abychom ta dvě čísla zaměnili - je vhodné řešení pomocí struktur (takže budu mít např. `Row.Value`) - není až tak rozumné použít enum - balíčkovací systém pro distribuci knihoven – NuGet (nuget.org) - balíček je tam nahraný ve formě souboru .nupkg, je to v podstatě ZIP soubor, jsou tam i metadata k balíčku, je tam nějaké verzování ## Měření rychlosti - benchmark - na celý program nebo konkrétní metodu - microbenchmark – zkouší malý kus kódu - framework BenchmarkDotNet – distribuuje se jako nugetový balíček - poznámka: hranaté závorky nad názvem metody/třídy označují atribut (odpovídá tomu třída; do metadat té metody se zapíše, že má daný atribut) - obvykle používám debug build – jakmile dělám benchmark, tak chci použít release build, aby JIT udělal optimalizace - benchmark se spustí několikát a statisticky se vyhodnotí - metody se označují atributem `[Benchmark]` - kdybych chtěl měřit spotřebu paměti, celé třídě s benchmarky můžu dát atribut `[MemoryDiagnoser]` (ale pak to může ovlivnit naměřené časy apod.) - měření rychlostí sčítání - `public static int a = 1;` - `public static int b = 2;` - `[Benchmark]` - `public int Add() { return a + b; }` - pozor, je potřeba, aby benchmarkovací funce vracela výsledek operací, které měřím, aby JIT nezahodil operace jako takové - pozor, kdybych používal private nebo readonly hodnoty, tak by to JIT mohl optimalizovat do konstant, takže by operace neprováděl - běžně se provádí constant-folding, tzn. sčítání konstant 1+2 se přeloží jako 3, tudíž by něco podobného mohlo nastat i tady - každá funkce má ve strojovém kódu prolog, tělo a epilog - pokud funkci volám opakovaně (v cyklu), většinu času zabere běh prologu a epilogu – řešením by bylo to tělo funkce zkopírovat přímo do cyklu, což by ale bylo v rozporu s guidelines - JIT dělá tuhle optimalizaci za nás (**method inlining**) – ale jen tam, kde to dává smysl - kdybychom funkce moc inlinovali, tak by procesor musel pro instrukce sahat do nižší úrovně cache, takže by se program řádově zpomalil - když má CIL kód méně než 20 bajtů, tak je to kandidát pro inlinování - např. rekurzivní funkce nebo metody na instancích s určitým interfacem se nedají inlinovat - pokud chci ručně ovlivnit inlining, tak můžu použít `System.Runtime.CompilerServices` a jeho atributy `[MethodImpl(…)]`, kde parametr může být `MethodImplOptions.AggressiveInlining` nebo taky `MethodImplOptions.NoInlining` (další varianty se týkají třeba optimalizace) - tohle jsou mikrooptimalizace – ty nemáme dělat, pokud k tomu nemáme opravdu pádný důvod ## Virtuální metody - už umíme zakrýt metodu rodiče pomocí klíčového slova `new` - synovská třída pak má dvě metody s daným názvem – jednu svoji a jednu od rodiče - při volání metody pak rozhoduje typ proměnné – pokud je v B zakrytá metoda m(), ale instance B je typu A, tak `inst.m()` volá původní metodu `A.m()` – kdybych chtěl volat `B.m()`, musím instanci nejdříve přetypovat - virtuální metoda `A.f()` - má implementaci v A - pomocí override definujeme implementaci v B - opět se podle typu výrazu rozhoduje první krok – bude se volat metoda `A.f()`, ale na úrovni CIL kóu se použije virtuální volání (callvirt) - na úrovni CIL kódu není poznat, která implementace se bude volat – to se rozhoduje za runtimu - každý typ s virtuálními metodami má tabulku virtuálních metod (VMT), ta se při dědění kopíruje (a prodlužuje), u overridnutých virtuálních metod se změní odkaz na implementaci - takže to, která implementace se reálně zavolá, závisí na třídě, jejíž instanci jsme vytvořili (nehledě na typ) - oba přístupy se dají kombinovat - u zakrytí metody se používá klíčové slovo `new` – bez něj nám překladač vyhodí warning - bez warningů by mohlo dojít k tomu, že by se – po zakrytí metody – kód v jiné části programu začal chovat divně a nevěděli bychom proč - ale error to není, protože se může stát, že se v knihovně objeví metoda, která tam původně nebyla → najednou zakrýváme metodu → museli bychom opravovat kód - abstraktní metoda (klíčové slovo abstract) – je to virtuální metoda bez implementace - nesmíme vyrábět instance třídy s abstraktní metodou, takže třída musí být abstraktní - JIT: call vs. callvirt - `callvirt` → `call [… VMT[…]]` - výkonostně to vychází podobně - ale nevirtuální volání se dá inlinovat (virtuální ne), takže u častého volání krátkých metod může být vhodné používat nevirtuální metody, aby JIT mohl optimalizovat inlinováním - je vhodné defaultně používat nevirtuální metody (a virtuální jen tehdy, kdy to dává smysl) - C++ … podobná syntax jako v C#, jen override u virtuálních metod má hodně způsobů zápisu (nenapíšu nic nebo napíšu virtual nebo v novějších verzích napíšu override) - Java - všechny metody jsou automaticky virtuální - proč je to špatně - když píšeme metody v třídě A, které se navzájem volají, tak obvykle spoléháme na to, jakým způsobem tyto metody ovlivňují vnitřní stav - takže nahrazení nějaké z metod může tu třídu rozbít - označení metody jako virtuální → slibujeme rozšiřitelnost (tedy to, že tu metodu můžeme nahradit jinou implementací a nic se nerozbije) - někdy můžeme chtít tento slib pro virtuální přepsanou metodu zrušit – použijeme klíčové slovo sealed (v Javě final) - někdy dává smysl použít sealed u třídy (respektive obecně u typu), pokud nechceme, aby ji někdo rozšiřoval - pozorování: pokud použijeme sealed metodu, tak ji JIT může v určitých situacích devirtualizovat, nebo dokonce inlinovat - virtuální metody vs. interfaces - každý typ má tabulku virtuálních metod (VMT) - ta obsahuje odkazy na implementace virtuálních metod - každý typ, který implementuje interface (a jeho potomci), má tabulku pro každý interface - ta obsahuje odkazy na metody odpovídající metodám z interfacu - jeden interface může rozšiřovat jiný interface (zápis jako u dědění/implementace) - abstract method vs. interface - vynucení kontraktu – interface ho vynucuje hned - veřejný kontrakt – abstraktní metody můžou být protected - slib rozšiřitelnosti – u virtuální metody je to opt-out (pomocí sealed), u interfaců opt-in (musím znova říct, že implementuju interface) - data – u interfaců nelze (kvůli vícenásobné dědičnosti) - vícenásobná dědičnost – nelze u abstraktních metod - volání jiné implementace virtuální metody - řešením by bylo použít protected metodu a tu volat z public metody, ale to se obvykle nedělá - lepší je použít base.m() - base je jako this, ale přímý předek - pokud metodu voláme jako base.m(), tak se metoda volá nevirtuálně - takže base.m() volá metodu m() přímého předka (nehledě na to, jestli je virtuální) - může nastat problém při volání virtuální metody z konstruktoru – např. když její synovská implementace spoléhá na nějakou vlastnost/field, která se přiřazuje v synovském konstruktoru, který se v danou chvíli ještě nespustil ### Přístup překladače k (ne)virtuálním metodám - ve hře Stardew Valley zasadíme semínko, vyroste strom, ten plodí jablka - kvalita semínka ovlivňuje kvalitu plodů - návrhový vzor Factory - AppleFactory v konstruktoru dostane kvalitu jablek - strom dostane instanci AppleFactory - AppleFactory má metodu Create, která vrátí jablko (instanci třídy Apple) dané kvality - ze stromu můžou padat i zlatá jablka - metoda Eat třídy Apple bude virtuální - GoldenApple je potomek Apple, přepisuje virtuální metodu Eat - problém, když budeme mít jablečnou logiku v knihovně a.dll a jejich jezení v knihovně b.dll - b.dll jsme překládali vůči starší verzi a.dll - takže i zlatá jablka budeme jíst nevirtuálně? - C# překladač dělá trik, že i nevirtuální metody volá virtuálně – tedy se rozhodnutí přesouvá na JIT - tedy i nevirtuální metody mají v CIL kódu instrukci callvirt - fungování virtuálních a nevirtuálních metod to nijak nemění, jen to řeší poměrně častý problém, který nastává při nezávislém vývoji knihoven a kompilaci na nich závislých programů ## Vlastnosti - properties, mají getter a setter - už známe auto-implemented props - často bychom si tělo getteru a setteru chtěli napsat sami - z getteru se v CIL kódu vygeneruje metoda `int get_X()` (pro vlastnost `int X`) - ze setteru se vygeneruje metoda `void set_X(int value)` - value … kontextové klíčové slovo (je klíčovým slovem pouze v setteru vlastnosti) - často dává smysl v setteru kontrolovat podmínky pro omezení hodnot - syntaktická zkratka pro deklaraci jednoduché metody - `void f() => return x;` - ale nepoužívat uvnitř metod – to jsou lambda funkce, což je poměrně složitý koncept - vlastnost s getterem se dá zapsat různě - `int X { get { return x; } }` - `int X { get => x; } }` - `int X => x;` - obecně k vlastnostem přistupujeme jako k ekvivalentu fieldu – takže je potřeba na to při jejich implementaci myslet (gettery a settery by měly běžet rychle, typicky konstantní čas) - když by nějaký getter měl běžet dlouho, tak ho budu implementovat jako metodu, ne jako property - vlastnosti se obvykle pojmenovávají podstatnými jmény, metody slovesy (respektive začínají slovesem – třeba Get) - Length vs. GetLength() - problém s Count a Count() - lepší by bylo Count a GetCount() - boolean vlastnosti obvykle začínají slovesem Has, Can, Is, Should… - vlastnosti můžou být v interfacech - když v interfacu požaduju jen getter, můžu v jeho implementaci přidat i setter - u knihovny může dávat smysl rovnou použít vlastnost, než začít s fieldem a později ho předělat na vlastnost - změna fieldu na vlastnost mění rozhraní knihovny (a je potřeba opravit/překompilovat programy, které ji používají) - property Length u vektoru - spočítám jen jednou, budu cachovat - potřebuju uložit „neplatnout hodnotu“ (abych věděl, že ještě nemám spočítáno) - mohl bych použít null, ale typ double podporuje i hodnotu not a number (NaN), která by tady byla vhodnější (protože nullabilita fieldu zvětšuje velikost celé struktury) - při porovnávání NaN je třeba myslet na to, že existuje víc variant NaN (takže je lepší použít metodu double.IsNaN) - u vlastností X, Y je potřeba přidat settery, které budou kromě nastavení hodnoty backing fieldu invalidovat délku - pokud jsem X, Y definoval jako auto-implemented properties, je potřeba přidat backing fieldy - vlastnosti můžou být i virtuální (nebo abstraktní) - můžu overridovat getter i setter nebo jen jeden z nich - ale když mám virtuální vlastnost s getterem, tak v jejím overridu nemůžu přidat setter - pozor, pokud použiju syntaxi auto-implemented props s výchozí hodnotou, tak se ta hodnota jednou přiřadí do backing fieldu - pokud je výchozí hodnota nějaký field, tak pak pozdější změna hodnoty fieldu neovlivní hodnotu té vlastnosti - pozor na side-effects - vlastnost by neměla mít side-effects (i kdyby byla rychlá) - je vhodnější, aby to byla metoda ## Viditelnost - klíčová slova - public – přístup není omezen - private – přístup je omezen na kód v daném typu - C# podporuje vnořené typy – takže kód ve vnořeném typu má taky přístup k private položkám typu, v němž je vnořen - protected – přístup je omezen na daný typ a typy, které z něj dědí - internal – přístup je omezen na aktuální sestavení - protected internal – přístup je omezen na aktuální sestavení ***nebo*** odvozené typy - private protected – přístup je omezen na aktuální sestavení ***a*** odvozené typy - file – přístup je omezen na aktuální zdroják (v C# 11) - výchozí viditelnost – private ve třídě, public v interfacu - životnost lokální proměnných (scope) - `int a = 5;` se dá rozdělit na `int a;` a `a = 5;` - `int a;` … deklarace proměnné - lokální proměnná vzniká v místě deklarace - nadřazené složené závorky odpovídají životnosti proměnné - takže třeba ve while cyklu se proměnná s daným názvem vytváří v každé iteraci - na začátku cyklu `ALLOC → SUB SP,4` - na konci cyklu `FREE → ADD SP,4` - JIT může udělat optimalizaci, že alokaci a dealokaci vystrčí mimo cyklus - někdy taky dané místo na zásobníku recykluje pro různé proměnné - pokud lokální proměnnou nepotřebuju sdílet mezi různými iteracemi cyklu, nedává smysl se to snažit mikrooptimalizovat vystrčením deklarace mimo cyklus - deklarace lokálních proměnných - některé jazyky umožňují uvnitř bloku deklarovat již deklarovanou proměnnou, která bude nezávislá na té deklarované výše – v C# to nejde - nelze redeklarovat parametr funkce - nelze redeklarovat proměnnou deklarovanou výše ve vnějším bloku - nelze redeklarovat proměnnou deklarovanou výše ve vnitřním bloku - nelze přistupovat k neinicializované proměnné - situace, kdy inicializuju v podmíněném bloku – je potřeba nějak zajistit default inicializaci (ideálně s komentářem, pokud se ta hodnota nikdy nepoužívá; taky je vhodné rozumně umístit vyhození výjimky) - příklad s `if (x is int a)` – chová se jako deklarace uvnitř podmíněného bloku - lze omezit životnost proměnných pomocí složených závorek ## Pointery a reference - co může být v proměnné - hodnota (u hodnotových typů) - velikost odpovídá velikosti datového typu - u struktur lze použít `new`, které nic nealokuje, ale zavolá konstruktor - reference (u referenčních typů) - je tam uložená adresa, takže velikost odpovídá velikosti adres - adresa vede na GC haldu - adresa ukazuje na celý objekt - explicitní alokace pomocí `new` - tu adresu nelze zjistit (zčásti proto, že GC objekty na haldě někdy přesouvá, proto se adresa může měnit) - každý hodnotový typ je potomkem objectu - tedy do proměnné typu object musí jít přiřadit proměnnou hodnotového typu - při takovém přiřazení se provádí boxing – je to implicitní alokace na GC haldě, alokuje se tam instance hodnotového typu (bude tam standardní overhead) - pozor: v Javě jsou dva různé typy, int (hodnotový) a Integer (referenční) – v C# je to ten samý typ System.Int32 - co můžu dělat s proměnnou typu object? - můžu volat ToString() - unboxing – hodnotový typ alokovaný na haldě uložený referencí se uloží jako hodnota - není vhodné používat object pro slabé typování – vlastně takhle implementujeme duck typing - co když máme long, zaboxujeme a chtěli bychom unboxovat do intu - nejde to, unboxing konverzi nelze kombinovat s truncation konverzí - vyhodí to runtime exception - musíme napsat obě konverze: `(int)(long)o` - pokud strukturu přiřadíme do proměnné typu interface, tak se zaboxuje - když potřebujeme hodnotový typ předávat referencí, tak object nedává smysl, vhodnější je použít jednoduchou třídu - boxování a unboxing nullable typů funguje tak, jak bychom čekali - pointery - pointer = adresa - `int* a, b;` – hvězdička se váže k typu, ne k identifikátoru (takže se nepíše `int *a, *b;`) - `a = &c;` - `*a = d;` - `Console.Write(*a);` - žádná omezení – podobně jako v C++ - může ukazovat kamkoliv - hodí se to k low-level programování nebo komunikaci s nějakými nativními funkcemi - pointery nesleduje garbage collector - někdy by se nám hodilo něco mezi referencemi a pointery - tracking reference - GC je sleduje - můžou ukazovat pouze na data – na GC haldu, statické fieldy nebo lokální proměnné - tracking reference = adresa, nelze ji zjistit - automatická dereference - může to vést doprostřed objektu, ale musí to ukazovat na celý field - jazyk C++/CLI - sjednocení všech funkcí dotnetu a C++ - to nejhorší z obou světů - tracking reference se deklarují `int%` - obecné tracking reference jsou pořád příliš nebezpečné na to, aby se s nimi dobře programovalo - nebezpečný scénář – tracking reference má delší životnost než věc, na kterou ukazuje - bezpečný scénář – tracking reference má kratší životnost než věc, na kterou ukazuje - tracking referenci typicky předáváme jako argument funkce - věc, na kterou tracking reference ukazuje může být - static - lokální proměnná - GC halda - garbage collector objekt nesebere, dokud na něj směřuje tracking reference - klíčové slovo `ref` – používá se u parametrů funkcí, uvádí se dvakrát, v hlavičce funkce i při volání - u referenčních typů obvykle nedává smysl používat tracking referenci - výjimečným případem, kdy to smysl dává, je třeba zvětšení pole – vyrobím nové pole, položky překopíruju a do tracking reference přiřadím nové pole - když používám `ref`, tak proměnná musí být inicializovaná (aby se z ní dalo číst) - proto se u výstupních parametrů funkcí používá klíčové slovo `out` – na úrovni CIL kódu je to to samé, ale nevyžaduje to, aby proměnné byly inicializované - uvnitř funkce s výstupními parametry se s nimi zachází jako s neinicializovanými (dá se z nich číst až po prvním zápisu) - před standardním ukončením funkce s výstupními parametry se do každého z nich musí alespoň jednou zapsat - pokud funkce skončí výjimkou, tak se do výstupních parametrů zapsat nemusí, ale to není problém, protože catch blok nemůže spoléhat na inicializaci v try bloku - pokud proměnnou deklaruju nad try/catch, v try bloku ji inicializuji a v catch bloku vyhodím výjimku dál (pomocí `throw;`), tak pod try/catch můžu proměnnou považovat za inicializovanou - pokud napíšu `if (int.Parse(s, out int x))`, tak se proměnná deklaruje před ifem - discard - když mě výstupní hodnota nebo návratová hodnota funkce nezajímá, tak použiju podtržítko jako název lokální proměnné - pozor, pokud je někde v kontextu funkce proměnná/funkce s identifikátorem `_`, tak se vypne discard syntaxe a používá se jako standardní identifikátor proměnné - středník - zakončuje příkaz, což je jeden nebo více výrazů - výraz `a = 1` má hodnotu 1 - `a = b = c = 123;` - středník říká, že se má zahodit hodnota výrazu - pokud volám funkci, která není voidová, tak je vhodné její návratovou hodnotu zahodit pomocí přiřazení do discardu (z dokumentačních důvodů) - klíčové slovo `in` … proměnná uvnitř funkce funguje jako readonly (podobně celá struktura funguje jako readonly) - při volání funkce se dá volat i hodnotově bez klíčového slova - `in` má smysl u výkonnostních optimalizací hodnotových typů (typicky u počítačových her) - místo `in` se dá použít taky `ref readonly` – je to něco velmi podobného - tracking reference se dají používat i jinde (nejen jako parametry funkcí) – je lepší je moc nepoužívat ## Interfaces a objekty podruhé - interfacy nejsou potomky objectu, třídy ano - graf dědičnosti interfaců je nezávislý na grafu dědičnosti tříd - ale `.ToString()` se dá volat na každém objektu - v případě `I i1 = new A();`, kde `I` je interface, tedy funguje `i1.ToString();` - všechny hodnotové typy jsou implicitně sealed - kdybychom měli dva structy, kde by jeden dědil od druhého a jeden by byl větší, tak by nefungovalo přiřazení jednoho do typu druhého (protože by neseděla velikost) - proto je dědičnost structů zakázána - new je u structů jenom volání konstruktoru – nic se nealokuje - metoda v objektu typu C má skrytý parametr this typu C - metoda ve struktuře typu S má skrytý parametr this typu `ref S` - stejný interface může být implementován objektem i strukturou ## Pole - v dotnetu vždy referenčního typu - fixní délka - alokuje se na GC haldě - kdyby se délka měnila, pro GC by to bylo moc komplikované - podobně i u jiných objektů se velikost po alokaci nemění - na haldě je uložen overhead (syncblock + type), délka a jednotlivé hodnoty - v poli můžou být přímo hodnoty u hodnotových typů nebo adresy u referenčních typů - i při zjednodušeném zápisu se vždy nejdříve alokuje vynulované pole a pak se do něj postupně přiřadí hodnoty - na prvek pole lze vytvořit tracking referenci - pozor, u objektů s indexerem tohle nefunguje - každé pole je potomkem typu Array - ale obvykle není dobrý nápad to používat - ale na typu Array existují velmi užitečné statické metody (Copy, Sort, …) - vícerozměrná pole - zubatá (jagged) - pole polí - `int[][] a = new int[2][];` - `a[0] = new int[3];` - `a[1] = new int[4];` - `int x = a[0][1];` - jako v Javě - nevýhoda: každé pole má svůj vlastní overhead - obdélníková (rectangular) - mezi hranaté závorky napíšu jednu nebo více čárek - `int[,] a = new int[2, 3];` - `int x = a[0, 1];` - jako v C/C++ - v dotnetu je s nimi problém kvůli Visual Basicu .NET - Visual Basic umožňuje začínat od libovolného indexu - to by snížilo efektivitu - v dotnetu - jednodimenzionální pole jsou striktně indexovaná od nuly - vícedimenzionální pole umožňují začínat od libovolného indexu - VB.NET je používá i pro jednodimenzionální pole - ale jsou pomalejší – i v C# - závěr - jagged jsou obecně rychlejší (cca 2×) - ale pokud už s každou naindexovanou položkou pole budu provádět nějaký výpočet, tak tam nebude velký rozdíl v rychlosti - obdélníková jsou paměťově úspornější (v určitých situacích), lépe se zapisují - poznámka: `default(T)` vrací výchozí hodnotu pro typ T (dá se používat i zkráceně pomocí `default`) - po inicializaci pole jsou všechny hodnoty nastaveny na default - všechna pole mají runtimový range check - nedá se z nich vytéct ven - JIT se snaží range check optimalizovat – typicky u cyklů - struktury v poli vs. v seznamu (List) - metoda na struktuře má implicitní parametr – tracking referenci na strukturu - když budu mít metodu MultiplyBy, která upravuje hodnoty ve struktuře, tak `points[0].MultiplyBy(3)` se bude chovat různě v případě pole a listu – opět viz problém s tracking referencemi a indexery - proto dává smysl, aby struktury byly immutable - mutable struktury jsou divné - tracking reference se dá vrátit z metody – pokud ukazuje na něco jiného než na lokální proměnnou - tedy by se takhle dal implementovat List – indexer by vracel tracking referenci - C# to takhle nedělá, getter indexeru vrací hodnotu a setter modifikuje hodnotu - ve strukturách nešlo inicializovat fieldy hodnotami kvůli implicitnímu volání bezparametrického konstruktoru nulujícího paměť - když se vytváří pole struktur (nebo se definuje lokální proměnná daného typu), tak se to tváří, jako by se volal implicitní bezparametrický konstruktor struktury (ale ve skutečnosti se nevolá – jen se nuluje paměť) - od C# 9 můžou mít fieldy výchozí hodnotu - jsou tam dva bezparametrické konstruktory – jeden implicitní, druhý můj - výchozí hodnoty se přiřazují jen když volám explicitně konstruktor (ten svůj) - překladač kontroluje inicializaci jednotlivých fieldů struktury - takže není potřeba volat konstruktor - dokud nejsou inicializované všechny fieldy struktury, tak nemůžu používat vlastnosti – protože getteru se předává tracking reference na celou strukturu ## Goto - syntax - `goto skocSem;` - label – `skocSem:` - na úrovni strojového kódu je to JMP - problém: může výrazně snížit přehlednost kódu - pravidla - nesmí se skákat mimo funkce - nesmí se skákat do složených závorek - rozumné použití - break z vnořeného cyklu - skočení zpátky před cyklus - stavový automat - alternativa: příkaz switch - switch (jako void příkaz) - každá větev musí končit příkazem break/return/goto - větev může mít několik `case` labelů - JIT ho může optimalizovat – tedy bývá efektivnější než kaskáda ifů - pomocí goto se dá skočit na konkrétní case (z vnitřku switche) - switch jako výraz - `int result = x switch { > 10 and < 20 => 1000, int b => b + 10, null => -1 };` - místo case se píšou šipky - místo default se píše `_ =>` - není tam žádná magie, přeloží se to do rozumné kaskády ifů a goto - pattern matching - `a is X {Prop1: …, Prop2: …}` - `a is {…}` - `a is X(…, …)` - `int result = p switch { Person("Petr", var lastName) => lastName.Length, Person(var firstName, "Vesely") => firstName.Length, _ => 0 };` - Person musí mít metodu Deconstruct - všechny parametry jsou výstupní - Person(string x, string y) se mapuje na Deconstruct(out string x, out string y) - pokud vhodná metoda Deconstruct neexistuje, tak se to nepřeloží - používá se ducktyping - u record tříd se Deconstruct generuje automaticky - metod Deconstruct může být víc - dekonstruktor by neměl být výpočetně náročný - další použití dekonstruktoru je u tuples – ale ty jsou složitější, takže budou v letním semestru - `a is [< 2, > 1, .. int[] restArray, int x]` - takové pole musí mj. mít aspoň 3 prvky - do restArray se uloží kopie (ta se alokuje na GC haldě), to pole může být i prázdné - pozor na konkrétní kolekci – obecně u IEnumerable nemusíme být schopní efektivně najít poslední prvek - novinka v C# 12 - inicializace kolekcí pomocí hranatých závorek - dají se tam vkládat kopie polí ## Výjimky - používat výjimečně – nejsou efektivní - výjimka = objekt, je potomkem typu Exception - když ji chci vyhodit, tak vyrobím novou výjimku - na konec názvu výjimky je vhodné napsat Exception - v objektu výjimky se obvykle ukládají data - Messsage - StackTrace – popis toho, kde výjimka vznikla - informace o tom, o jakou výjimku se jedná, se dá uložit i v typu - je vhodné používat rozumnou hierarchii typů, aby uživatelé typů mohli výjimky filtrovat - mohlo by nás napadnout předalokovat objekt výjimky - ale to pak může vést k problémům se StackTracem - takže je lepší napsat `throw new …` - vyhozená výjimka se šíří ven - pokud najde try blok, tak hledá vhodný catch blok a finally - catch blok má filtr na typ výjimky - pokud je bezparametrický, tak to je to samé, jako by tam byl typ Exception (v C# 1 to znamenalo něco jiného) - v catch bloku - můžu vyhodit jinou výjimku - můžu opět vyhodit tu samou (existující) výjimku – pomocí `throw;` - šíření chyby se ukončí, pokud se našel vhodný catch blok a ten neobsahuje další throw - vyhození výjimky je drahé, protože se vyplňuje StackTrace a protože se řeší přeskakování společně s dealokacemi (na konci každých složených závorek) - try blok není drahý – drahé jsou jen ty výjimky - metoda int.Parse vyhazuje výjimky, pokud se něco nepovede - metoda int.TryParse nevyhazuje výjimky - metody začínající slovem Try počítají s tím, že neúspěch je velmi pravděpodobný – tedy nepoužívají výjimky a jsou levné - u metody TryParse přijdeme o informaci, co se přesně nepovedlo - pokud provádíme operaci na nullable hodnotových typech a jeden z nich je null, tak je výsledek taky null - někdy se používá přístup, že funkce Try vrací nullable hodnotu – když dopadne neúspěchem, tak vrátí null - v Javě se u funkcí píše, jaké výjimky vyhazují – ale to se ukázalo jako nevhodný přístup - pak se používají code contracts – viz pokročilejší předměty - poznámka k Nežárce - pokud chybové stránky řešíme vyhazováním výjimek, tak to vede k větší náchylnosti na DDoS - ale výjimky nám poskytují informaci o chybě - tedy dává smysl vracet „silně typovanou“ strukturu ViewOrError - ta má jeden field typu object, kde obsahuje View nebo Error - implementujeme vlastnosti, které vrátí správný View (u Erroru speciální chybovou stránku) a Error (u normálního View vrátí null) - měli bychom používat vhodné typy výjimek - některé se chápou jako bug - OutOfRangeEx. - NullReferenceEx. - některé se chápou jako špatné použití funkce - ArgOutOfRangeEx. - ArgNullEx. - někdy je vhodné definovat vlastní výjimky – jako potomky ApplicationException - někdy upravujeme datovou strukturu - před úpravou platí invarianty o datech ve struktuře (např. platnost ID) - po úpravě také - během úprav invarianty neplatí - co když se vyhodí výjimka během úprav? - výsledkem jsou vadná data v datové struktuře - typický příklad - do nákupního košíku přidám knížku, pak jí nastavím ID a počet - pokud selže parsování ID, v seznamu se objeví knížka s nulovým ID a počtem, což porušuje invariant - někdy dává smysl přístup defenzivního programování – zabránit tomu, aby situace vůbec mohla nastat (nejdřív vytvořím knížku, až pak ji přidám do seznamu) - dalším řešením je zrušit akce, které jsme provedli - pomocí catch bloku – tady dává smysl zachytávat úplně všechny výjimky, zrušit provedené akce (UNDO) a pokračovat v šíření výjimky - rethrow – pomocí `throw;` - pozor, `catch (Exception ex)` v kombinaci s `throw ex;` není ekvivalentní, protože se přepíše stack trace - může to dávat smysl, pokud je to server a stack trace by byl vidět na internetu - finally blok - provádí se až po catch bloku (nebo po try bloku) - u streamu typicky používáme buffer, ten se splachuje pomocí Flush – to se hodí mít ve finally bloku - když vypisujeme XML, vypisujeme např. řádky tabulky a nějaký řádek selže, tak chceme uzavřít tabulku – taky ve finally bloku - zavírání souborů - soubory jsou cenné zdroje - file lock (aplikace mají výhradní přístup k souboru) - tedy zavírání souborů bychom chtěli dělat ve finally blocku - každá věc, kterou bychom měli včas zavírat, by měla implementovat rozhraní IDisposable (má metodu Dispose) - protože se try & finally s Dispose používá často, tak můžeme používat using blok, což je syntaktická zkratka - `using (var f1 = new FileStream("...")) { ... }` - alternativa místo using = nullable objekt a klasický try + finally (tohle umožňuje použít i catch blok) - jak řešit několik usingů? - napsat je pod sebe bez středníků a složených závorek – jako vnořené bloky (poslední z nich už má klasicky složené závorky) - using jako deklarace – Dispose se zavolá na konci složených závorek (tzn. tam, kde končí životnost proměnné) - pozor, tohle často svádí k tomu, že se objekt disposuje pozdě - `using var f1 = new FileStream("...");` - k čemu dalšímu se používá using - platí pouze ve zdrojáku (pomocí global using v celém projektu) - `using System;` … namespace - `using C = Console;` … alias pro určitý typ - často vede k menší čitelnosti kódu - v C# 12 lze i pro generické typy (dřív ne) - může to vést k tomu, že vytvoříme dva aliasy pro tu samou třídu – čímž ztrácíme silnou typovanost - implementace IDisposable podle dokumentace (s destruktorem apod.) dává smysl pouze pokud mám pointer na externí unmanaged resource - to obvykle v C# neděláme - tedy stačí implementovat IDisposable pomocí jednoduché metody Dispose - v metodě Dipose je potřeba detekovat, že už Dispose bylo jednou zavoláno – nemá se provést nic - je potřeba počítat s tím, že po zavolání Dispose bude někdo chtít k objektu přistupovat – v takovém případě vracet ObjectDisposedException (asi detekovat pomocí bool fieldu) - když ve třídě používám disposable zdroje, tak může dávat smysl, aby třída byla tranzitivně také disposable a v dispose volat dispose na zdrojích - to dává smysl, pokud je třída vlastníkem toho zdroje - jinak (typicky u writerů) není dobrý nápad soubor implicitně zavírat - kde vznikají výjimky - metody – `new StreamReader`, `int.Parse` - zachytíme konkrétním catch blokem pro danou výjimku - catch bloky se kontrolují postupně shora dolů – vezme se první, který matchuje - rethrow v catch bloku - typicky v catch bloku je UNDO (obnovení invariantů) a nějaké logování - u metody LoginUser máme nějakou implementaci – třeba pomocí souborů, takže když uživatel neexistuje, vyhodí se FileNotFoundException - druh výjimky ale potom souvisí s implementačním detailem - chceme tu výjimku nahradit nějakou hezčí výjimkou - budeme mít try catch blok, který zachytí FileNotFoundException a vyhodí ProfileNotFoundException - přepíše se nám tak ale stack trace - naštěstí má Exception vlastnost InnerException, kterou bychom měli nastavit vždy, když rethrowujeme nějakou jinou výjimku - InnerException se typicky nastavuje v konstruktoru - např. `catch (FileNotFoundException ex) { throw new ProfileNotFoundException(ex); }` - zpracování výjimky v CLR - nejprve se hledá místo ošetření (1. fáze) - výjimky umožňují exception filtry pomocí klíčového slova when - exception filtry můžou obsahovat volání funkcí - tyhle funkce se volají během hledání místa ošetření (v 1. fázi) - kolem metody Main je try s always-true exception filtrem, který obsahuje výpis „unhandled exception“ - ten se liší podle verze dotnetu a operačního systému (v dotnet frameworku se zobrazí dialogové okno – takže se může stát, že se finally vůbec nezavolá) - šíření výjimky (2. fáze) - rozlišování výjimek - zachytím obecnou výjimku, pomocí ifu zkontroluju typ výjimky a případně rethrowuju - pomocí when - dodatek k výjimkám - vstup do catch bloku pausne šíření výjimky - vstup do finally bloku pausne šíření výjimky - CLR udržuje zásobník pausnutých výjimek - při opuštění finally bloku se buď 1) odpausne výjimka pausnutá tímto blokem, nebo se 2) tato výjimka odstraní a místo ní se propaguje nová výjimka (pokud uvnitř finally bloku vznikla nová výjimka) - při opuštění catch bloku se stane jedna z těchto věcí - zruší se výjimka pausnutá tímto blokem (pokud nevznikla nová výjimka) - odpausne se výjimka pausnutá tímto blokem (v případě rethrowování pomocí `throw;`) - odstraní se výjimka pausnutá tímto blokem a místo ní se propaguje nová výjimka (pokud je vyhozena nová výjimka) ## Garbage collector - C-like - malloc/new → free/delete - ownership - problém nemazání (memory leaků) a dvojitého mazání - C++ (volitelně) - unique_ptr - shared_ptr (ref_count) - Rust (povinně) - Rc\ (ref_count) - odlišný přístup: garbage collection (GC) - v managovaném prostředí, kde má runtime velkou kontrolu nad programem - paměť se spravuje efektivněji - GC v dotnetu vytvořila Maoni Stephens – má ráda kočky - Small Object Heap (SOH) - Large Object Heap (LOH) - když má objekt víc než 85 000 B - když mám pole velkých objektů, tak to pole nemusí skončit na LOH, protože pole obsahuje reference - adresový prostor - fyzický adresový prostor (PA) – jedna jednotka je rámec - u virtuálního adresového prostoru (VA) – stránka - proces nějak používá virtuální adresový prostor - operační systém virtuální adresový prostor nějak mapuje na ten fyzický - reserve VA (parametr: počet bajtů) → kernel vrátí volnou adresu ve VA - rezervuju určitou část virtuálního adresového prostoru, abych měl zaručeno, že ji nebudou chtít použít ostatní části mého programu (procesu) - tím způsobem můžu mít jistotu, že budu používat kontinuální rozsah adres (v téhle konkrétní části svého programu) - nesouvisí to s přidělením fyzické paměti (dokonce jí ani nemusí být dostatek) - commit VA (adresa a počet bajtů) = alokace PA - commituju část rezervovaného adresového prostoru - může selhat, když není dostatek fyzické paměti - bloku paměti se v dotnetu říká segment/region - garbage collector vrací paměť operačnímu systému, když ji nepotřebuje - decommit = free - módy: workstation × server - ve workstation módu je halda sdílená, takže když běží garbage collector, tak se všechna vlákna zapauzují - v server módu garbage collector běží ve více vláknech - v některých situacích může garbage collection běžet na pozadí - garbage collector se pouští na základě alokace paměti - Windowsy vysílají broadcast zprávu, když dochází paměť počítači, takže i v této situaci se spouští garbage collection - graf objektů - kde jsou reference - ve statických proměnných - na zásobníku (lokální proměnné a tracking reference) - uvnitř (live) objektů - každá reference odpovídá hraně v grafu - garbage collector označí dosažitelné objekty jako live - kdyby se mazaly mrtvé objekty z haldy, tak by vznikly díry - proto se provádí heap compacting - přeskupí se objekty na haldě - opraví se reference (proto se nedá získat adresa referencí) - heaptop = vrchol haldy (kam se přidává nový objekt) - po heap compactingu se sníží - pomocí GCSettings se dá ovlivnit chování GC - existuje metoda GC.Collect, ale největší chyba je, že ji lidi volají příliš často - udržuje se ukazatel na heaptop, při alokaci se jenom posune o velikost alokace - výhoda tohohle přístupu je to, že je alokace rychlá - akorát se ta paměť při alokaci musí nulovat - GC se pouští, když paměť na haldě „dojde“ - short lived objekty - jsou fajn, protože žádný objekt nepřežije garbage collection, takže může proběhnout rychle a nemusí se dělat heap compacting - generační GC - gen 0 → gen 1 → gen 2 - když objekt vznikne, tak je v generaci 0 - garbage collection se dělá po generacích - nejdřív se dělá collection gen 0, pokud je potřeba další paměť, tak gen 1 a případně nakonec gen 2 - pokud objekt přežije garbage collection, tak se přesune do další generace - invariant: po každé collection je generace 0 prázdná - long living objekt skončí v gen 2 - pokud vede reference z long living objektu do short lived objektu, tak na něj dlouho nepřijdeme, že se může uvolnit - large object heap se collectuje, jen když se collectuje gen 2 u small object heap - memory leaky - když máme long living objekt, co ukazuje na short lived objekt - když zapomenu na nějakou referenci - někdy je vhodné explicitně přiřadit null - příklad: List\, implementujeme metodu Clear - máme hodnotu Count a pole referencí na T - přístup 1: Clear vynuluje Count v O(1) – špatná implementace, vede k memory leaku, zůstanou tam reference na objekty uvnitř listu - přístup 2: v O(n) přiřadit všem objektům null - o každém segmentu se ví, v jaké je generaci ## Stringy - délka a pole charů - escape sekvence se resolvují za překladu - je lepší používat string než String (protože String se dá zadefinovat jinak) - jsou immutable - ToUpper vytvoří nový string - Substring vytvoří nový string - v Javě se použije původní string a ukazuje se na jeho část - StringBuilder … mutable string - StringBuilder dělá podobný trik jako List – alokuje víc místa než potřebuje - ale konkatenace stringů je přehlednější než StringBuilder a má menší overhead, takže pokud jich provádíme málo, tak je to lepší řešení - string i StringBuilder mají spoustu optimalizací - StringBuilder Append má spoustu přetížení na čísla apod. - Appendy se dají řetězit - sb.Append("x").Append("y"); - protože Append vrací instanci StringBuilder - string.Format (pomocí `{0}`, `{1}` apod.) vs. konkatenace - pozor na míchání přístupů – pokud uživatel zadá `{0}`, tak se to může rozbít - interpolace pomocí `$"xyz"` - používá se StringHandler - InterpolatedStringHandler používá duck typing (najde se vhodný handler podle typu, do kterého výsledek interpolace přiřazujeme)