Vánoční C# hádanka

Visual Studio General .NET Core Uncategorized

5 years ago

Tento článek je součástí třetího ročníku C# Advent, velké díky za organizaci patří Matthewovi D. Grovesovi! Jako svůj příspěvek do C# Advent jsem si připravil malou hádanku pro C# vývojáře. Mějme následující kód:

Metoda ValueHolder.SetValue je implementována následovně:

A vlastnost ValueHolder.Value je jednoduchá auto-property s privátním setterem:

Value a SetValue jsou jedinými položkami ValueHolder a na pozadí neběží žádný externí kód nebo jiné vlákno, které by výsledky jakkoliv ovlivnilo. Intuitivně by výstupem programu mělo být True. Co však program vypíše doopravdy?

False?

False?

Dokážete vysvětlit, proč program vypsal False? Jakmila budete mít odpověď připravenou, či se budete chtít podívat na řešení, odscrollujte prosím níže!       Připraveni na řešení?       Pojďme se na to podívat!

Řešení

Ačkoliv se může zdát, že manipulace s List<int> a int[] je naprosto identická, je tu klíčový detail, který jsem o typu ValueHolder neprozradil - ValueHolder je ve skutečnosti struct!

Přestože se může zdát, že jde o malý rozdíl, má opravdu velký význam! Jedním ze základních faktů o struct v C# je to, že jde o hodnotové typy. To znamená, že když strukturu přiřadíme do nové proměnné, vznikne nová instance, která obsahuje kopii všech jejích členů. Například:

Vypíše:

Co se tedy děje v naší původní ukázce? V případě pole dostaneme očekávaný výstup 5:

Ale v případě List<int>, je to 0:

Abychom pochopili, proč stejná operace (indexování) má rozdílné chování, podívejme se do IL kódu. Následující řádek:

Se zkompiluje na:

Nyní to porovnejme s:

Které se zkompiluje na:

Když se podíváme na IL výstup práce s polem, vidíme, že indexer vrací managed pointer na existující instanci struktury ValueHolder a volá metodu SetValue přímo na ní. Takže v našem příkladě jsme opravdu změnili hodnotu vlastnosti na původní instanci v poli. Situace se liší při práci s listem. List<T> je třída, která nemá "zabudovaný" koncept indexování. Má implementovaný C# indexer, který lze vytvořit pro kteroukoliv třídu. A indexer je pouze syntaktický cukr, který generuje dvojici metod - get_Item pro čtení hodnoty a set_Item pro modifikaci hodnoty na indexu. Toto je hlavní příčinou našeho problému - protože metoda get_Item je obyčejná metoda, která vrací hodnotu na daném indxu, ve skutečnosti vytváří novou kopii struktury. Kód pak modifikuje hodnotu vlastnosti na této nové kopii, která je však hned po vykonání volání zahozena a list nadále obsahuje původní instanci struktury, která je nedotčena. Je dobré podotknout, že kompilátor se snaží podobným problémům zabránit. Pokud bychom se pokusili modifikovat vlastnost přímo, nebylo by nám to povoleno. V případě volání metody ale kompilátor nemůže rozpoznat, že dochází k vedlejším účinkům, které mění vnitřní stav instance.

Shrnutí

Hlavním poselstvím této malé hádanky je, že struktury v C# bychom se měli snažit ponechat neměnnými (immutable). Tímto způsobem si můžeme být vždy jisti, že vnitřní stav instance je neměnný a vždy budeme jasně vědět, kdy vytváříme nové instance. Namísto metody SetValue v našem příkladě bychom mohli mít metodu WithValue, která by vracela novou instanci obsahující hodnotu předanou jako parametr. Uživateli (vývojáři) by bylo pak zřejmé, že API nemodifikuje existující instanci a musí pracovat s návratovou hodnotou metody.

Zdrojový kód

Ukázkový zdrojový kód k tomuto článku je k dispozici na mém GitHubu.