Změna sémantiky cyklu foreach v C# 5.0

Tomáš Holan       19.03.2012       C#       13025 zobrazení

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é Smile, 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.)

 

hodnocení článku

1 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

Menší chyba ve výsledku

Upozornění pěkné, ale přesto ty výsledky jsou špatně.

Deklarace metody je Enumerable.Range(start, count). Takže bude vracet hodnoty 1, 2 a 3. Jenže v Lambda výrazu je + 1.

Takže výsledné hodnoty poté budou:

4, 4, 4 (<= .net 4.0)

2, 3, 4 (> .net 4.0)

nahlásit spamnahlásit spam 0 odpovědětodpovědět

ad "Jenže v Lambda výrazu je + 1" nic takového tam není.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

RE: Změna sémantiky cyklu foreach v C# 5.0

@Tomáš Jecha: jj, je to přesně tak, na Targetovanou verzi FW se vůbec nekouká.

@Jirka: Máš pravdu, v normálním kódu by se to moc potkávat nemělo. Já jsem se s tímto problémem jednou setkal u dost advanced scénaře, kdy jsem v kódu sestavoval nějaký složitější expression.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

RE: Změna sémantiky cyklu foreach v C# 5.0

Ha, tak to je dobrý chyták. Nikdy jsem se s tím ještě nesetkal (bohudík), ale hned zítra to ukážu studentíkům.

Každopádně z mého úhlu pohledu, proč jsem se s tím nikdy nesetkal? Protože rozumný programátor nebude nikdy do lokálního seznamu dávat fukce tak, že si do foreach schová lambda výraz. Jedna fukce má dělat vždy jen jendu věc.

Jirka

nahlásit spamnahlásit spam 0 odpovědětodpovědět

RE: Změna sémantiky cyklu foreach v C# 5.0

Ještě bych zdůraznil, že kompilace .NET 3.5 nebo .NET 4 zdrojáků bude novým kompilátorem ovlivněna také. Je to stejný případ, jako klíčové slovo "var", které šlo také využívat v .NET Frameworku 2, pokud jsme měli kompilátor pro novější verzi jazyka.

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback