pátek 25. dubna 2014

Anonymous classes, Extension methods, Lambda Expressions, ...

Nemoha se dočkati studentíkova dokončení zadaného prográmku v C#, i jal jsem se programovati jej taktéž, však hnán snahou o maximální kompaktnost stvořeného kódu.

A tak vzniklo devatero zastavení na jejich konci jeden řádek jest.

Zadání


Vytvořte konzolovou aplikaci, která vypíše pozice a počet výskytů hledaného znaku v textu. Text i znak je zadán obsluhou z konzole.

Drobný úvod

...aneb, abyste si nemysleli, že jsem debil. 
Parciální třídy a parciální metody, Extension methods, Lambda expressions, Automatic properties, Anonymní typy, atd., atp. jsou už s námi dosti dlouhou dobu, ale zatím se mi je nějak nepodařilo strávit a začít rutině používat. 

V poslední době mi pomohlo několikanásobné shlédnutí Michalova videa (viz. Další informace) a pořízení ReSharperu od Jet BRAINS, který, mimo jiné, nabízí převod klasických konstrukcí for-next na lambda výrazy. Stačí jen kliknout.

Takže takto vybaven jsem se jal upravovat klasický program ze Zadání.

Malá poznámka

Ne zcela důvěřuji SyntaxHighlighteru, který používám na této stránce pro zobrazení zdrojových kódů, tak pokud máte pochybnosti, stáhněte si z odkazu na konci článku testovací projekt, který je prověřen.

Generace 1

Zde je, dá se říct, program klasické konstrukce. Pěkně se porovnáváme znak za znakem a v případě shody něco vypíšeme na konzoli a zvýšíme počítadlo výskytů.
private static void Gen1(string text, string znak)
{
    int pocet = 0; //počítadlo výskytů

    for (int pozice = 0; pozice < text.Length; pozice++)
        if (text.Substring(pozice, 1) == znak)
        {
            Console.WriteLine("Výskyt na pozici: {0}", pozice);
            pocet++;
        }

    Console.WriteLine("Počet výskytů: {0}\n", pocet);
}

Generace 2

V této generaci se počet výskytu znaků stanoví pomocí LINQ (poslední řádek kódu). Tohle dám ještě z hlavy a je to celkem pochopitelné.
private static void Gen2(string text, char znak)
{
    for (int pozice = 0; pozice < text.Length; pozice++)
        if (text[pozice] == znak)
            Console.WriteLine("Výskyt na pozici: {0}", pozice);

    Console.WriteLine("Počet výskytů: {0}\n", text.Count(x => x == znak));
}

Generace 3

Tady jsem vytvořil strukturu ZnakApozice, kdy struktura nese pozici znaku a znak, a celý text vložil do typového Listu. V návaznosti na to jsem upravil procházení kódu. Zatím to tedy kyne, ale očekávám lepší zítřky.
Poznámka: tu hrůzu ve špičatých závorkách na konci zdrojáku tam dodal SyntaxHighlighter, který si nedokáže poradit s definicí generického typu ZnakApozice a tak vyplodil na konec ukončovací značku.
private static void Gen3(string text, char znak)
{
    var temp = new List();

    for (var pozice = 0; pozice < text.Length; pozice++)
        temp.Add(new ZnakApozice { Znak = text[pozice], Pozice = pozice });

    foreach (var znakApozice in temp)
        if (znakApozice.Znak == znak)
            Console.WriteLine("Výskyt na pozici: {0}", znakApozice.Pozice);

    Console.WriteLine("Počet výskytů: {0}\n", text.Count(x => x == znak));
}

private struct ZnakApozice
{
    public char Znak;
    public int Pozice;
}

Generace 4

Stále máme typový List, ale ten už nevzniká přes for-each, ale už pomocí lambda výrazu. Tohle navrhl ReSHARPER.
private static void Gen4(string text, char znak)
{
    var temp = text.Select((t, pozice) => new ZnakApozice { Znak = t, Pozice = pozice }).ToList();

    foreach (var znakApozice in temp)
        if (znakApozice.Znak == znak)
            Console.WriteLine("Výskyt na pozici: {0}", znakApozice.Pozice);

    Console.WriteLine("Počet výskytů: {0}\n", text.Count(x => x == znak));
}

private struct ZnakApozice
{
    public char Znak;
    public int Pozice;
}

Generace 5

Teď už nevypisuji pozice postupně, ale vytvářím si vypis, který pošlu na konzolu až nakonec. Tuším, že mi ReSHARPER opět napoví nějaké pěkné řešení.
private static void Gen5(string text, char znak)
{
    var temp = text.Select((t, pozice) => new ZnakApozice { Znak = t, Pozice = pozice }).ToList();

    var vypis = String.Empty;

    foreach (var znakApozice in temp)
        if (znakApozice.Znak == znak)
            vypis += String.Format("Výskyt na pozici: {0}\n", znakApozice.Pozice);

    Console.WriteLine(vypis);
    Console.WriteLine("Počet výskytů: {0}\n", text.Count(x => x == znak));
}

private struct ZnakApozice
{
    public char Znak;
    public int Pozice;
}

Generace 6

Takže vypis už vzniká lambda výrazem.
private static void Gen6(string text, char znak)
{
    var temp = text.Select((t, pozice) => new ZnakApozice { Znak = t, Pozice = pozice }).ToList();

    var vypis = temp.Where(znakApozice => znakApozice.Znak == znak)
        .Aggregate(String.Empty, (current, znakApozice) => 
            current + String.Format("Výskyt na pozici: {0}\n", znakApozice.Pozice));

    Console.WriteLine(vypis);
    Console.WriteLine("Počet výskytů: {0}\n", text.Count(x => x == znak));
}

private struct ZnakApozice
{
    public char Znak;
    public int Pozice;
}

Generace 7

Teď jsem odstranil proměnnou temp a její lambda výraz vložil přímo do vypisu.
private static void Gen7(string text, char znak)
{
    var vypis = text.Select((t, pozice) => new ZnakApozice { Znak = t, Pozice = pozice }).ToList()
        .Where(znakApozice => znakApozice.Znak == znak)
        .Aggregate(String.Empty, (current, znakApozice) =>
            current + String.Format("Výskyt na pozici: {0}\n", znakApozice.Pozice));

    Console.WriteLine(vypis);
    Console.WriteLine("Počet výskytů: {0}\n", text.Count(x => x == znak));
}

private struct ZnakApozice
{
    public char Znak;
    public int Pozice;
}

Generace 8

Vzpomněl jsem si na Michalovo video a nahradil strukturu ZnakApozice anonymní třídou (konstrukce new {Znak=t, Pozice=pozice}) a taky jsem k tomu rovnou připočetl řetězec s počtem výskytů.
private static void Gen8(string text, char znak)
{
    var vypis = text.Select((t, pozice) => new { Znak = t, Pozice = pozice })
        .Where(znakApozice => znakApozice.Znak == znak)
        .Aggregate(String.Empty, (current, znakApozice) => current
        + String.Format("Výskyt na pozici: {0}\n", znakApozice.Pozice))
        + String.Format("Počet výskytů: {0}\n", text.Count(x => x == znak));

    Console.WriteLine(vypis);
}

Generace 9

Metodu finalizuji tak, že vše vkládám do Console.WriteLine(), čímž se zbavuji proměnné vypis a mám vše v jednom příkazu. Slovy klasika: Kód o kterém většina programátorů nebude nevěřit, že to ten kompilátor vůbec zkompiluje, natož aby to pak něco dělalo.
private static void Gen9(string text, char znak)
{
    Console.WriteLine(text.Select((t, pozice) => new { Znak = t, Pozice = pozice })
        .Where(zp => zp.Znak == znak)
        .Aggregate(String.Empty, (current, zp) => current
        + String.Format("Výskyt na pozici: {0}\n", zp.Pozice))
        + String.Format("Počet výskytů: {0}\n", text.Count(x => x == znak))
        );
}

Další generace

Teď do toho čučím, snažím se kód pochopit a hlavně se dostat do stavu, kdy bych byl schopen něco takového zplodit sám. A případně to ještě zestručnit.

Porovnání

Z programátorského hlediska je použití lambda výrazů & spol. nesporně čitelnější, než nějaké for-each konstrukce, ale jak je to s náročností na procesorový čas?

Abych získal odpověď na tuto otázku, mírně jsem upravil program tak, aby jej nebrzdil výpis na konzolu pro metodu Gen1 a Gen9 prohnal 100 000 až 1 000 000 iteracemi a pro délky řetězců 23, 46, 92, 184 a 368, abych odhalil případné nelineární závislosti.

Generace 1

Z grafu je patrná lineární závislost klasického algoritmu na délce analyzovaného textu.


Generace 2

Je opět patrná lineární závislost kombinace klasického a lambda algoritmu na délce analyzovaného textu.


Generace 9

Stejně jako u předchozích grafů je patrná lineární závislost lambda algoritmu na délce analyzovaného textu.




Porovnání Generace 1 & 2 & 9

Z grafu je patrné, že lambda algoritmu je o cca 60% pomalejší než klasické algoritmy, a to prakticky bez ohledu na délku analyzovaného řetězce nebo na počet iterací.


Počty analyzovaných znaků za jednotku času 

Zajímavé rozpouštění režie algoritmu do počtu analyzovaných znaků.




Další informace

  • Testovací projekt je ke stažení ZDE
  • Pěkné Michalovo VIDEO na téma Extension methods, lambda expressions a LINQ v C#
  • ReSharper od Jet BRAINS ZDE

Žádné komentáře:

Okomentovat