
Testy architektury: jak zabetonovat pravidla architektury přímo do kódu
Architektonická pravidla, která existují jen v dokumentaci, se dříve nebo později poruší. Není to otázka jestli, ale kdy.
Přejít na obsah|Přejít k hlavnímu menu|Přejít k vyhledávání
Koncept null prostupuje programovacími jazyky. Setkáváme se s jeho různými podobami: None, 0, NULL, nil, nullptr, undefined. Všechny tyto výrazy označují totéž: neexistující nebo nepřiřazenou hodnotu, na kterou odkazujeme. Tony Hoare (Sir Charles Antony R. Hoare), nositel Turingovy ceny a autor algoritmu Quicksort, na konferenci OOPSLA 2009 označil zavedení null reference za svou "billion‑dollar mistake". Pracoval na typovém systému jazyka ALGOL W v roce 1965 s cílem zajistit bezpečné reference kontrolované za překladu. Přidal však null referenci jako záložní řešení, které se později stalo zdrojem mnoha chyb a zranitelností. Hoare později přiznal, že by to znovu neudělal.
Null se časem dostal do mnoha moderních jazyků. Jazyky s ukazateli (C, C++) jej používají jako speciální hodnotu pro pointery; později ho přijaly i Java, Python a C#, protože usnadňoval interoperabilitu a byl programátory očekáván. Null se stal de facto standardem pro reprezentaci neexistujících hodnot, ale jeho přítomnost přinesla řadu problémů. Z typového hlediska null představuje slabinu: nelze jednoznačně vyjádřit, zda reference může být null. To znamená, že téměř každý odkaz je potenciálně null a je třeba jej před použitím kontrolovat.
null tak vysoká?Hoare mluvil o kumulativních nákladech, které null přinesl softwarovému průmyslu. I v dnešní době (o 17 let později) patří výjimky typu NullReferenceException mezi nejčastější runtime chyby. Dereference null (CWE‑476: NULL Pointer Dereference) se pravidelně objevuje v žebříčcích CWE Top 25 Most Dangerous Software Weaknesses. Každý takový problém stojí čas na debugování, rollback nasazení, řízení incidentu a v nejhorším případě i bezpečnostní riziko. Sečteme‑li náklady napříč desítkami let a miliardy řádků kódu, číslo s devíti nulami najednou nepůsobí jako hyperbola. Částka bude samozřejmě ještě dále růst, protože null se stále používá a bude se používat i nadále.
Příkladem, jak může nekontrolovaný null způsobit rozsáhlé škody, byl globální výpadek Google Cloud z června 2025. Chyba v nově nasazeném kódu vedla k null dereferencím v kritické části kontrolního plánu a výsledkem byly rozsáhlé výpadky služeb po dobu několika hodin. Podrobné analýzy incidentu naleznete v ThousandEyes a Google status reportech.
Existuje několik strategií pro zmírnění dopadů null a v praxi se často kombinují:
null vracíme objekt implementující požadované rozhraní, který bezpečně nic neprovádí. Příklad v C#: public interface ILogger
{
void Log(string message);
}
public class NullLogger : ILogger
{
public void Log(string message)
{
// No operation
}
}Option<T> explicitně vyjadřuje možnost neexistující hodnoty. Tento přístup je běžný v jazycích s ADT (F#, Rust, OCaml). Příklad v F#: type Option<'T> =
| Some of 'T
| NoneNull aware typový systém: jazyk odlišuje na úrovni typů, zda reference může být null. Používá se např. v Kotlin, Swift nebo Dart a od verze 8 i v jazyce C#.V C# komunitě se o ADT mluví minimálně od roku 2017 a různé návrhy na zavedení se objevují pravidelně. Pattern matching přidaný v C# 7.0 a vylepšovaný v dalších verzích umožňuje ADT emulovat, ale stále chybí podpora pro exhaustivní kontrolu: překladač neví, že nejsou ošetřeny všechny možné případy, pokud přidáte nový podtyp. Příklad emulace ADT pomocí dědičnosti recordů v moderním C#:
abstract record Shape;
record Circle(double Radius) : Shape;
record Rectangle(double W, double H) : Shape;
double Area(Shape s) => s switch
{
Circle c => Math.PI * c.Radius * c.Radius,
Rectangle r => r.W * r.H,
_ => throw new UnreachableException()
};Aktuálně jsou ADT v C# ve fázi návrhu.
Kdo nechce čekat na nativní podporu, může sáhnout po komunitních knihovnách. LanguageExt přináší plnohodnotný funkcionální toolkit včetně Option<T>, Either<L, R>, Try<T> a desítek dalších typů inspirovaných funkcionálními jazyky. Optional je naopak minimalistická alternativa zaměřená primárně na Option<T> a Option<T, TException>. Obě knihovny pomáhají vynutit explicitní ošetření obou větví při práci s Option<T> pomocí funkcí Match:
Option<string> name = GetName();
string result = name.Match(
Some: n => $"Hello, {n}",
None: () => "Hello, stranger"
);Cesta defenzivního programování vede k řetězení kontrol na null, což vede k verbóznímu a těžko čitelnému kódu jako je tento:
if (user != null)
if (user.Address != null)
if (user.Address.Country != null)
Console.WriteLine(user.Address.Country.Name);Jako první pokus o mitigaci null problému v C# můžeme považovat null coalescing a null conditional operátory:
Console.WriteLine(user?.Address?.Country?.Name ?? "Unknown");Kód je určitě kratší a lépe se čte. Stále ale neřeší problém, že user nemusí mít vyplněnou adresu (i když tento stav nemusí být platný).
Null anotaceNull anotace (nullable reference types) byly představeny v C# 8.0 a fungují na jednoduchém principu: kompilátor rozdělí referenční typy na dvě kategorie podle toho, zda mohou být null nebo ne. Klíčovým prvkem je ? za názvem typu:
string name = "Alice"; // nesmí být null, kompilátor to ohlídá formou varování
string? nickname = null; // explicitně deklarováno, že může být nullV praxi překladač sleduje tok dat a upozorňuje na místa, kde hrozí null dereference:
string? nickname = GetNickname();
Console.WriteLine(nickname.Length); // Varování CS8602: možná null dereference
if (nickname != null)
Console.WriteLine(nickname.Length); // V pořádku, překladač ví, že nickname není nullPozitivní vedlejší efekt je, že díky flow analysis se eliminuje řada zbytečných kontrol — pokud kompilátor dokáže odvodit, že proměnná nemůže být null, nevyžaduje kontrolu.
Null anotace se dají zapnout na úrovni projektu pomocí <Nullable>enable</Nullable> v .csproj souboru. Anebo na úrovni souboru pomocí #nullable enable direktivy.
Základní analyzér je celkem dobrý, ale stále existují situace, kdy není schopen odvodit bezpečnost referencí správně. Např. když konstruktor deleguje inicializaci referencí do jiné metody:
public class Service
{
private string value;
public Service()
{
InitializeValue();
}
private void InitializeValue()
{
value = "Logger initialized";
}
}Na první pohled vypadají referenční null anotace stejně jako u hodnotových typů (int?, DateTime?), které C# zná od verze 2.0. Unifikace syntaxe je záměrná, sémantika je ale jiná. U hodnotových typů je int? jiný typ (Nullable<int>), u referenčních typů jde pouze o anotaci. Za běhu jsou string a string? totožné.
Dokumentace popisuje dva stavy, které reference může v null aware kontextu mít:
not-null: Výraz je známý jako nenulový.maybe-null: Výraz může být null.Pokud ze svého anotovaného projektu referencujete legacy knihovnu, která není anotovaná, všechny její reference budou překvapivě označeny jako not-null. To vede k přehnané optimističnosti a analýza je v tomto případě bezmocná. Je nutno podotknout, že tato informace není v dokumentaci moc dobře popsána a pojmenování obou kategorií je velmi matoucí. Z určitého pohledu je to další z nešťastných defaultů, kterých v C# z historických důvodů máme mnoho: sealed není default, immutable není default, atp.
Rozhodnutí to jistě nebylo jednoduché a jistě má své důvody. Bohužel také neexistuje ani žádný přepínač, kde bychom jej mohli změnit. V praxi nám tedy zbývá udělat jedno z následujících:
null anotace.null oblivious reference označovat lépe.null anotace mají mezery a používat defenzivní programování vedle nich.Pokud se rozhodneme null anotace používat a učinit z nich náš hlavní způsob mitigace null dereference, je dobré upravit i styl vývoje. V první řadě je dobré zapnout warnings as errors (minimálně) pro všechny relevantní varování:
<PropertyGroup>
<Nullable>enable</Nullable>
<WarningsAsErrors>CS8600;CS8601;CS8602;CS8603;CS8604;CS8605</WarningsAsErrors>
</PropertyGroup>Dalším krokem by mělo být stanovení pravidel pro používání suppression operátoru ! (null-forgiving operator). Ten by měl být používán pouze v případech, kdy jsme si jisti, že reference není null, ale kompilátor to nedokáže odvodit. Zároveň by každé použití ! mělo být doprovázeno komentářem, proč je bezpečné jej použít (! se v kódu snadno přehlédne). Roslyn analyzér Nullable.Extended.Analyzer může při vynucování těchto pravidel pomoci.
Základní analyzér je celkem dobrý, ale stále existují situace, kdy není schopen odvodit bezpečnost referencí správně. Např. když konstruktor deleguje inicializaci referencí do jiné metody:
public class Service
{
private string value;
public Service()
{
InitializeValue();
}
private void InitializeValue()
{
value = "Logger initialized";
}
}Analyzér vyhodnotí jako varování warning CS8618: Non-nullable field 'value' must contain a non-null value when exiting constructor. Řešení je upravit inicializaci tak, aby se skutečně děla v konstruktoru. Výsledkem je spokojený analyzér a čistější kód (můžeme např. označit field jako readonly):
public class Service2
{
private readonly string value;
public Service2()
{
value = InitializeValue();
}
private string InitializeValue()
{
return "Logger initialized";
}
}Dependency injection pomocí property nebo metody není s null anotacemi příliš kompatibilní. Analyzér bude neustále tvrdit, že závislosti mohou být null (a bude mít pravdu). Pokud tedy chcete používat null anotace, je dobré se této formě DI vyhnout a přejít na konstruktorovou injekci tam, kde je to možné.
Totéž platí pro další formy dvojí inicializace typů, kterou je lepší sjednotit do jednoho kroku. Pokud z nějakého důvodu potřebujete inicializovat ve více krocích, je dobré zvážit použití builder patternu. Dosáhneme tak toho, že neinicializované typy nejsou v typovém systému reprezentovatelné.
Prosté zapnutí <Nullable>enable</Nullable> na úrovni projektu může generovat stovky nebo tisíce varování. Lepším přístupem je postupné zapínání anotací na úrovni souborů pomocí #nullable enable. Postupně tak můžete čistit varování v jednotlivých souborech a přidávat anotace tam, kde je to potřeba.
Strategie se může lišit podle typu projektu:
NullReferenceException.Je dobré zvážit použití AI agentů pro automatickou konverzi kódu. Typická iterace může vypadat takto:
1) Vygenerovat seznam varování pro celý projekt s dočasně zapnutým nullable (dotnet build /p:Nullable=enable > log.txt). 2) Začít od souborů s nejmenším počtem varování. Soubory s nulovým počtem varování nevyžadují žádné změny kódu, stačí přidat #nullable enable. U souborů s mírným počtem varování má AI agent méně rozhodnutí a menší šanci udělat systematicky špatnou anotaci. 3) Spustit build, zaznamenat varování a podle výsledků iterovat.
Při kontrole výsledků je dobré se soustředit na časté chyby AI u těchto úloh: přehnané použití !, nebo mechanické přidávání ? bez doménového rozhodnutí.
V moderním C# dopady miliardové chyby rozhodně zmírnit lze. V případě null anotací je nutné dodržovat určitá pravidla a být si vědom omezení, které tento mechanismus má. ADT cesta nabízí větší jistotu, ale současně je nutné udělat mnohem větší změny a wrapovat téměř všechen cizí kód. 100% eliminace null dereference v C# není realistická, museli bychom C# opustit a zbavit se tak obrovské zátěže zpětné kompatibility. Je třeba zvolit strategii, nastavit ochranné mantinely (warnings-as-errors, code review pravidla), migrovat postupně a nespoléhat na to, že tooling zachytí vše. Null není vyřešený problém: je to kompromis, se kterým se každý C# projekt musí vědomě vyrovnat.

Architektonická pravidla, která existují jen v dokumentaci, se dříve nebo později poruší. Není to otázka jestli, ale kdy.

Vývoj software je dynamický obor, ve kterém se neustále objevují nové nástroje a trendy. Pokud ale chceme vyvíjet kvalitní software, nestačí tyto nástroje a trendy jen aplikovat, musíme skutečně porozumět, jak fungují a jaká případná rizika s sebou nesou.
Děkujeme za váš zájem o odběr našeho newsletteru! Pro dokončení registrace je potřeba potvrdit vaše přihlášení. Na zadaný e-mail jsme vám právě zaslali potvrzovací odkaz. Klikněte prosím na tento odkaz, aby bylo vaše přihlášení dokončeno. Pokud e-mail nenajdete, zkontrolujte prosím složku nevyžádané pošty (spam) nebo složku hromadné pošty.
