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:


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);
}

view raw

RiddleTest.cs

hosted with ❤ by GitHub

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


public void SetValue(int value) => Value = value;

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


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?

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!


public struct ValueHolder
{
public int Value { get; private set; }
public void SetValue(int value) => Value = value;
}

view raw

ValueHolder.cs

hosted with ❤ by GitHub

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:


var a = new ValueHolder();
var b = a;
b.SetValue(5);
Console.WriteLine($"a == {a.Value}");
Console.WriteLine($"b == {b.Value}");

view raw

AssignStruct.cs

hosted with ❤ by GitHub

Vypíše:


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:


var valuesArray = new ValueHolder[] { new ValueHolder() };
valuesArray[0].SetValue(5)
Console.WriteLine(valuesArray[0].Value); //5

view raw

ArrayValues.cs

hosted with ❤ by GitHub

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


var valuesList = new List<ValueHolder>() { new ValueHolder() };
valuesList[0].SetValue(5);
Console.WriteLine(valuesList[0].Value); //0

view raw

ListValues.cs

hosted with ❤ by GitHub

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


valuesArray[0].SetValue(5);

Se zkompiluje na:


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

view raw

ArrayAccess.il

hosted with ❤ by GitHub

Nyní to porovnejme s:


valuesList[0].SetValue(5);

Které se zkompiluje na:


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

view raw

ListAccess.il

hosted with ❤ by GitHub

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.