Variable scope v lambda expressions v C# 3.0

V C# 2.0 byly zavedeny anonymní metody, což ušetřilo hromadu psaní – když chtěl člověk předat nějakého delegáta, tak nemusel vytvářet metodu, ale mohl přiřadit přímo kus kódu a kompilátor se už sám postaral o to, že vytvořil metodu, do které fláknul ten kus kódu. Se C# 3.0 pak přišly tzv. lambda expressions, což je technicky to samé, jen je syntaxe méně košatá.

Používání lambda expressions, příp. anonymních metod, je docela návykové, ale je třeba je používat s rozmyslem, protože zdánlivě jednoduchý kód může dělat nečekané věci. Např. tento jednoduchý prográmek:

class Program
{
    static Func<string>[] Test(IEnumerable objects)
    {
        if (objects == null)
        {
            throw new ArgumentNullException("objects");
        }
        var res = new List<Func<string>>();
        foreach (var o in objects)
        {
            res.Add(() => o == null ? "null" : o.ToString());
        }
        return res.ToArray();
    }
    
    static void Main(string[] args)
    {
        foreach (var f in Test(new[] { 1, 2, 3 }))
        {
            Console.WriteLine(f());
        }
    }
} 

Metoda Test vrací lambda expressions, které vrací string, který odpovídá textovým reprezentacím předaných objektů. Očekávaným výsledkem by tedy byla čísla 1, 2, 3 vypsaná pod sebou. Omyl – vypíšou se tři trojky! Problémem tohoto příkladu je právě ono fláknutí kusu kódu do nové metody prováděné kompilátorem. V tom kusu kódu totiž můžeme používat vše, co je dostupné (je ve variable scope) v metodě, kde lambda expression vytváříme (metoda Test). A kompilátor si nějak musí poradit s tím, že při přesunu kódu do samostatné metody musí zůstat tyto proměnné dostupné.
S tím si poradí tak, že pro každý statement (blok kódu s vlastními lokálními proměnnými, typicky uzavřený v chlupatých závorkách) vytvoří třídu, která obsahuje jeho lokální proměnné, a všude se pak nepracuje s lokálními proměnnými, ale s členy této třídy – tedy lokální proměnné se přesunou ze zásobníku do instance pomocné třídy (velmi zjednodušeně řečeno – přesný popis by vyžadoval hlubší znalosti CIL). V naší metodě Test máme jen jeden statement – celou metodu. Zjednodušeně si to můžeme dovodit tak, že všechny lokální proměnné můžeme nadeklarovat a nainicializovat (!) na začátku metody (přičemž parametry metody jsou zvláštní případ lokálních proměnných). V našem případě to znamená, že se vytvoří jediná třída s lokálními proměnnými a od té se vytvoří jediná instance. Důsledkem jsou tedy tři trojky – po doběhnutí metody Test totiž v proměnné o (ve skutečnosti uložené jako člen pomocné třídy) zůstane poslední použitá hodnota – tedy trojka – na kterou si pak sahají všechny vytvořené lambda expressions.
V tomto případě jsme dopadli ještě dobře – kdybychom použili klasický for cyklus s indexováním kolekce, měla by iterační proměnná i po doběhnutí cyklu hodnotu mimo hranice kolekce – a hups – výjimka.
Jak tedy z toho zapeklitého problému ven? Musíme donutit překladač vytvářet novou instanci pomocné třídy pro každou iteraci cyklu, tedy vytvořit statement pro tělo cyklu. Toho dosáhneme jednoduše tím, že v těle cyklu nadeklarujeme a nainicializujeme (!) lokální proměnnou. Funkční příklad tedy vypadá takto:

class Program
{
    static Func<string>[] Test(IEnumerable objects)
    {
        if (objects == null)
        {
            throw new ArgumentNullException("objects");
        }
        var res = new List<Func<string>>();
        foreach (var o in objects)
        {
            var localO = o;
            res.Add(() => localO == null ? "null" : localO.ToString());
        }
        return res.ToArray();
    }
    
    static void Main(string[] args)
    {
        foreach (var f in Test(new[] {1, 2, 3}))
        {
            Console.WriteLine(f());
        }
    }
} 

Nyní se vytvoří instance pomocné třídy s lokálními proměnnými v každé iteraci cyklu (tedy při každé nové inicializaci lokální proměnné) a vše funguje jak má.

Kdybychom navíc pracovali s parametry metody nebo „globálními“ proměnnými (globálními v rámci metody), došlo by k tomu, že by se vytvořily dvě pomocné třídy – jedna by reprezentovala „lokální“ statement a kromě svých lokálních proměnných by obsahovala referenci na instanci druhé pomocné třídy, která by reprezentovala „globální“ statement (obsahovala by parametry metody a „globální“ proměnné). Měly bychom tak tři instance „lokální“ pomocné třídy, které by ukazovaly na jedinou instanci „globální“ pomocné třídy.
Celá situace jde samozřejmě dále zobecnit a vnořených statementů může být libovolně – jednoduše se vytvoří hierarchie pomocných tříd.

Při vytváření lambda expressions tedy doporučuji dávat si pozor na scope všech používaných proměnných. Občas to může být docela problém dobře odhadnout – i např. Reflector v aktuální verzi s tím má trošku problém – vypisuje před lokální proměnné přesunuté do pomocné třídy prefix „base.“ (příp. „MyBase.“ ve Visual Basic .NET), což dokáže pěkně zmást…

Pro milovníky funkcionálního programování uvedu ještě jiné řešení problému:

class Program
{
    static Func<string>[] Test(IEnumerable objects)
    {
        if (objects == null)
        {
            throw new ArgumentNullException("objects");
        }
        return objects.OfType<object>().Select<object, Func<string>>(o => () => o == null ? "null" : o.ToString()).ToArray();
    }
    
    static void Main(string[] args)
    {
        foreach (var f in Test(new[] { 1, 2, 3 }))
        {
            Console.WriteLine(f());
        }
    }
} 

Velmi zajímavý je výsledný CIL kód. Je totiž vytvořena nová metoda s poetickým názvem <Test>b_0, která představuje tělo vnější lambda funkce – má tedy parametr o typu object a vrací Func<string>. Teprve v této metodě se pak vytvoří pomocná třída, což prakticky odpovídá vytváření na začátku cyklu z původního kódu.

1 thought on “Variable scope v lambda expressions v C# 3.0

  1. Oni autoři funkcionálních jazyků dobře vědí, proč hodnoty „proměnných“ nelze měnit a proč ty měnitelné mají odlišnou syntaxi (a případně jsou zavřené do monád). Podobně to řeší i Java, u které je z anonymních tříd definovaným uvnitř těla metody možné přistupovat jen k proměnným označeným jako final (readonly v C#).

    O „objektovém“ řešení v C# jsem nevěděl, ale po přečtení musím říct, že se mi vůbec nelíbí – je to úžasný způsob, jak do programu zanést vazby, které ve zdrojáku vůbec nejsou vidět a na které ani překladač neupozorní.

    To se mi líbí

Zanechat odpověď

Vyplňte detaily níže nebo klikněte na ikonu pro přihlášení:

Logo WordPress.com

Komentujete pomocí vašeho WordPress.com účtu. Odhlásit /  Změnit )

Twitter picture

Komentujete pomocí vašeho Twitter účtu. Odhlásit /  Změnit )

Facebook photo

Komentujete pomocí vašeho Facebook účtu. Odhlásit /  Změnit )

Připojování k %s

Tento web používá Akismet na redukci spamu. Zjistěte více o tom, jak jsou data z komentářů zpracovávána.