this dir | view | cards | source | edit | dark
top
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í 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?
- 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++
- 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ě)
- 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
- 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<T>, 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)