Vánoční C# hádanka

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:

https://gist.github.com/MartinZikmund/be1af722d0318eacb17ba89100e3fad5

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

https://gist.github.com/MartinZikmund/26d10387beea283cd5d1dda0bdbafcc7

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

https://gist.github.com/MartinZikmund/0bc84d4369247f67dcdd1ab2b45218bf

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!

https://gist.github.com/MartinZikmund/cac7f16c2850e43e79e2259155191146

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:

https://gist.github.com/MartinZikmund/374614ca26f920411fa762b26daa0e64

Vypíše:

https://gist.github.com/MartinZikmund/64b9a163c79c88c58a8c5607783d939a

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

https://gist.github.com/MartinZikmund/8dc47821080ebe245be75484ffdf8a8a

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

https://gist.github.com/MartinZikmund/c60d698bc1db7d68979935cd9dca1582

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

https://gist.github.com/MartinZikmund/acb495a65e562d1d1af8de4d807737c3

Se zkompiluje na:

https://gist.github.com/MartinZikmund/8319776b7734ae093d798575167ef759

Nyní to porovnejme s:

https://gist.github.com/MartinZikmund/fcee23123bc68c385bea4c4ff9f04d3c

Které se zkompiluje na:

https://gist.github.com/MartinZikmund/407bdf00cb610ccea80a1a21b1fd9c19

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.

Buy me a coffeeBuy me a coffee

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.