Před nedávnem vyšla veřejná beta verze produktů Visual Studio 11, .NET 4.5, C# 5.0 a už je tedy v celku jasné jaké nové funkce v těchto produktech budou a které nebudou. Nová verze jazyka C# 5.0 bude obsahovat kromě “velkých funkcí”, což je samozřejmě async/await a caller info attributes (u kterých se mi mimochodem vůbec nelíbí jak jsou do jazyka “dolepeny” - vlastní užitečnost této funkce ale nezpochybňuji), i nějaké ty menší. Jednou z těch menších je fix sémantiky foreach cyklu.
O co jde? Mějme následující kód:
static void Main(string[] args)
{
var list = new List<Func<int>>();
foreach (int i in Enumerable.Range(1, 3))
{
list.Add(() => i);
}
foreach (var f in list)
{
Console.WriteLine(f());
}
}
Pokud tento kód spustíme v C# 4.0 (nebo starších), bude výstup následující:
3
3
3
Pro ty, kteří by snad shledávali toto chování jako neočekávané , stručné vysvětlení proč tomu tak je:
Funkce () => i, kterou tvoříme uvnitř foreach cyklu “zachytává” lokální proměnou i. Tato technika se nazývá closure a její nejdůležitější vlastností je to, že closure zachytává proměnou nikoliv hodnotu. Proměnná foreach cyklu (*) je pouze jediná (před každou iterací je do ní pouze přiřazena jiná hodnota). To znamená, že opakovaně konstruujeme totožnou funkci, která v době jejího spuštění vrátí v daném čase aktuální hodnotu proměnné i. A aktuální hodnota proměnné foreach cyklu po jeho skončení bude hodnota posledního prvku (**).
(Pozn.: Řešením by bylo deklarovat uvnitř cyklu pomocnou proměnnou: int i2 = i; list.Add(() => i2);)
Podrobněji se tímto zabývá článek zde.
Pravdou ale samozřejmě zůstává to, že pokud vyloženě přímo tento “chyták” neznáte je velmi obtížné neudělat v tomto chybu. A to je také důvod, proč když výše uvedený kód spustíme v C# 5.0 (resp. zatím v beta verzi), budeme již dostávat výstup jiný:
1
2
3
V C# 5.0 je totiž pro každou iteraci foreach cyklu deklarována proměnná nová.
Jedná se samozřejmě o breaking change, ale riziko opravdového rozbití nějakého v praxi existujícího kódu je zde opravdu malé (změna se týká pouze kódu, který buď obsahuje chybu nebo by využíval “chybného” chování) ve srovnáním s přínosem provedení této změny.
POZOR, že sémantika cyklu for zůstává i v C# 5.0 nezměněná.
(*) Stejně je tomu i u for cyklu.
(**) Díky způsobu implementace closure také nebude technicky proměnná i “obyčejnou” lokální proměnnou (ve smyslu jejího interního umístění v paměti), protože kompilátor jí převede na členskou proměnnou třídy, která je vždy alokována na heapu (nikoliv na zásobníku nebo v registru).
(Toto uvádím pro doplnění souvislostí s dříve rozebíraném tématem v této části článku o hodnotových typech.)