Vnořené funkce v C#

Když jsem přecházel před lety z Delphi na C#, trošku mi chyběla možnost mít v metodě vnořenou funkci. Jasně, vždycky se to dá vyřešit tím, že se udělá další metoda, ale to už je další zbytečný kód navíc. Nejen, že si tím člověk zaplevelí třídu privátními metodami, které jsou velmi specifické a volají se jen z jedné metody, ale navíc musí do pomocné metody reprezentující vnořenou funkci předat kontext rodičovské metody, např. nějaké lokální proměnné nebo parametry.

Vezměme si jednoduchoučký příklad vnořené funkce:

private static string VerySpecificFunction(object o, IFormatProvider provider)
{
    return o == null ? "null" : Convert.ToString(o, provider);
}

public static void MyCoolMethod(IFormatProvider provider)
{
    foreach (var o in new object[] { 1, 2, 3, null })
    {
        Console.WriteLine(VerySpecificFunction(o, provider));
    }
    foreach (var o in new object[] { 4, 5, 6, null })
    {
        Console.WriteLine(VerySpecificFunction(o, provider));
    }
} 

Metoda VerySpecificFunction reprezentuje vnořenou funkci v metodě MyCoolMethod. Protože ale C# vnořené funkce nepodporuje, je třeba toto omezení nějak obejít. Výše uvedený kód ukazuje typický postup založený na vytvoření další pomocné metody. Lze to ale provést i elegantněji – pomocí anonymních metod, resp. lambda expressions.

Jednoduše si vytvoříme v metodě lokální proměnnou typu delegate, který má stejnou signaturu jako vnořená funkce. Do tohoto delegáta přiřadíme anonymní funkci či lambda expression. S typem delegáta většinou problém není a můžeme použít standardní generické typy Action (pro funkce vracející void), Func (pro funkce vracející specifiký typ), příp. Predicate (vrací bool na základě vlastností objektu). Pokud bychom s těmito typy nevystačili, pravděpodobně bychom museli nadeklarovat privátní typ delegáta na úrovni třídy a to by pak už nebylo tak pěkné. Ale troufám si tvrdit, že ve většině případů jsou zmíněné typy dostačující.

Upravený příklad by tedy vypadal takto:

public static void MyCoolMethod(IFormatProvider provider)
{
    var VerySpecificFunction = new Func<object, string>(o =>
        {
            return o == null ? "null" : Convert.ToString(o, provider);
        });

    foreach (var o in new object[] { 1, 2, 3, null })
    {
        Console.WriteLine(VerySpecificFunction(o));
    }
    foreach (var o in new object[] { 4, 5, 6, null })
    {
        Console.WriteLine(VerySpecificFunction(o));
    }
} 

Díky variable scope máme k dispozici parametr rodičovské metody i její lokální proměnné – vnořená funkce pak nemusí mít parametr typu IFormatProvider, protože můžeme použít přímo parametr rodičovské metody. Máme tedy prakticky to samé jako v Delphi – nice!

Jeden malý zádrhel tu ale přeci jen je – co kdybychom potřebovali z vnořené funkce volat samy sebe, tedy rekurzi? Když zkusíte zagooglit, najdete obskurní řešení, které sice funguje v jejich příkladech, ale když si anonymní metoda sahá i mimo své lokální proměnné, tak se řešení stává ještě více obskurnější. Ani toto není plně funkční kód, protože je problém získat aktuální instanci pomocné třídy, ve které jsou uchovány lokální proměnné a parametry:

public static void MyCoolMethod(IFormatProvider provider)
{
    Func<object, string> VerySpecificFunction = null;
    VerySpecificFunction = new Func<object, string>(o =>
        {
            var fr = new StackTrace().GetFrame(0);
            var mi = new StackTrace().GetFrame(0).GetMethod();
            return o == null ? "null" :
                Convert.ToString(o, provider) + (o is string ? string.Empty :
                    mi.Invoke(Activator.CreateInstance(mi.DeclaringType), new [] { "--" }));
        });    foreach (var o in new object[] { 1, 2, 3, null })
    {
        Console.WriteLine(VerySpecificFunction(o));
    }
    foreach (var o in new object[] { 4, 5, 6, null })
    {
        Console.WriteLine(VerySpecificFunction(o));
    }
}

Podle mého na to šli ale zbytečně složitě. Stačí si uvědomit, kdy můžeme zavolat např. i = i + 1 (což je „rekurze na úrovni proměnných„). Jedinou podmínkou je, že proměnná i musí být deklarována a inicializována. A vnořená funkce není přeci nic jiného než lokální proměnná typu delegate, takže můžeme použít zcela průhledný kód ve stejném duchu:

public static void MyCoolMethod(IFormatProvider provider)
{
    Func<object, string> VerySpecificFunction = null;
    VerySpecificFunction = new Func<object, string>(o =>
    {
        return o == null ? "null" :
            Convert.ToString(o, provider) + (o is string ? string.Empty : VerySpecificFunction("--"));
    });

    foreach (var o in new object[] { 1, 2, 3, null })
    {
        Console.WriteLine(VerySpecificFunction(o));
    }
    foreach (var o in new object[] { 4, 5, 6, null })
    {
        Console.WriteLine(VerySpecificFunction(o));
    }
} 

Ve vnořené funkci VerySpecificFunction v klidu volám opět funkci VerySpecificFunction bez jakýchkoliv obskurností – cool!

S těmito znalostmi můžeme používat v C# konstrukt, který je velmi podobný „nested functions“, jak je známe např. z Delphi. Samozřejmě ve finále dojde k vygenerování pomocné třídy a/nebo metody, ale náš zdroják zůstane čistý, protože záležitosti spojené s vnořenou funkcí zůstanou uzavřeny uvnitř rodičovské metody.

7 thoughts on “Vnořené funkce v C#

  1. Pane kolego, to je teda, s prominutím, pěkná prasečina :). Vim, ze je to jen demo příklad, ale nebylo by v tomto konkrétním případě prostě vytáhnout celý foreach do zvláštní metody, která by jako parametry měla IFormatProvider a IEnumerable a tu pak dvakrát v MyCoolMethod zavolat? Dokážu si představit, že se mi v jedné metodě na několika různých místech opakuje specifiký kus kódu, který není pak už nikde jinde v celém programu. Ale ve jménu čitelnosti kódu bych tento kus vytáhl do normální metody. Ona se například ta poslední verze dá napsat taky jako

    VerySpecificFunction = (o => o == null ? „null“ : Convert.ToString(o, provider) + (o is string ? string.Empty : VerySpecificFunction(„–„)));

    a takové konstrukce jsou dle mého názoru zbytečně nepřehledné. Každopádně článek mě vedl k zamyšlení a za to děkuji.

    To se mi líbí

  2. Pane kolego, v tomto konkrétním případě by to jistě šlo, dokonce je to tak uvedeno v prvním příkladu 😉

    Nicméně existují situace, kde se mnou navržené řešení velmi hodí. Teď mám na mysli views v ASP.NET MVC. To jsou totiž aspx stránky, které nemají by default žádný code-behind. Tedy celá stránka je jakoby jedna dlouhá metoda. A pokud máme v jedné stránce nějaký specifický kus kódu (např. jednou na začátku stránky, podruhé na konci), tak mnou navržené řešení přijde velmi vhod.

    To se mi líbí

  3. V tom případě uznávám, že se to opravdu může někdy hodit. (Teda pokud je takový problém napsat statickou třídu s extension metodami pro helpery ;))

    To se mi líbí

  4. Problém to jistě není, ale pokud je kód opravdu specifický pro jednu metodu (potažmo pro jedno view), jak jsem psal v úvodním odstavci, tak by se tyto extension metody pouze pletly v IntelliSense v ostatních views, kde nemají žádný smysl 😉

    To se mi líbí

  5. Každopádně vnořené funkce s oblibou používám v Delphi (pokud jsem v něm nucen ještě nějaké projekty udržovat), a v C# mi to také chybělo, takže díky za ty dary 😉

    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 )

Google photo

Komentujete pomocí vašeho Google úč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.