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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
static void Main(string[] args) | |
{ | |
var valuesArray = new ValueHolder[] { new ValueHolder() }; | |
valuesArray[0].SetValue(5); | |
var valuesList = new List<ValueHolder>() { new ValueHolder() }; | |
valuesList[0].SetValue(5); | |
Console.WriteLine(valuesList[0].Value == valuesArray[0].Value); | |
} |
Metoda ValueHolder.SetValue
je implementována následovně:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public void SetValue(int value) => Value = value; |
A vlastnost ValueHolder.Value
je jednoduchá auto-property s privátním setterem:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public int Value { get; private set; } |
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?

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
!
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public struct ValueHolder | |
{ | |
public int Value { get; private set; } | |
public void SetValue(int value) => Value = value; | |
} |
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:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var a = new ValueHolder(); | |
var b = a; | |
b.SetValue(5); | |
Console.WriteLine($"a == {a.Value}"); | |
Console.WriteLine($"b == {b.Value}"); |
Vypíše:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
a == 0 | |
b == 5 |
Co se tedy děje v naší původní ukázce? V případě pole dostaneme očekávaný výstup 5:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var valuesArray = new ValueHolder[] { new ValueHolder() }; | |
valuesArray[0].SetValue(5) | |
Console.WriteLine(valuesArray[0].Value); //5 |
Ale v případě List<int>
, je to 0:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var valuesList = new List<ValueHolder>() { new ValueHolder() }; | |
valuesList[0].SetValue(5); | |
Console.WriteLine(valuesList[0].Value); //0 |
Abychom pochopili, proč stejná operace (indexování) má rozdílné chování, podívejme se do IL kódu. Následující řádek:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
valuesArray[0].SetValue(5); |
Se zkompiluje na:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ldloc.0 //loads the local variable at index 0 (which is the array instance) onto stack | |
ldc.i4.0 //loads the value of 0 onto stack | |
ldelema Riddle.ValueHolder //loads the address of the array element at index 0 as a managed pointer onto stack | |
ldc.i4.5 //loads the value of 5 onto stack | |
call instance void Riddle.ValueHolder::SetValue(int32) //calls the SetValue method on the ValueHolder instance |
Nyní to porovnejme s:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
valuesList[0].SetValue(5); |
Které se zkompiluje na:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
ldloc.1 //loads the local variable at index 1 (which is the list instance) onto stack | |
ldc.i4.0 //loads the value of 0 onto stack | |
//calls the get_Item method on the list and pushes the return values onto the stack | |
callvirt instance !0 class [System.Collections]System.Collections.Generic.List`1<valuetype Riddle.ValueHolder>::get_Item(int32) | |
stloc.2 //pops the value off the stack and stores it as a local variable at index 2 | |
ldloca.s 2 //loads the address of local variable at index 2 | |
ldc.i4.5 //pushes the value 5 onto stack | |
call instance void Riddle.ValueHolder::SetValue(int32) //calls the SetValue method on the ValueHolder instance |
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.