Práce s dotazem integrovaným jazykem (LINQ)

Úvod

V tomto kurzu se naučíte funkce v .NET Core a jazyce C#. Co se naučíte:

  • Generování sekvencí pomocí LINQ
  • Metody zápisu, které lze snadno použít v dotazech LINQ.
  • Rozlišovat mezi dychtivým a opožděným vyhodnocením

Tyto techniky se naučíte vytvořením aplikace, která předvádí jednu ze základních dovedností každého kouzelníka: faerské náhodné prohazování. Stručně řečeno, faerní shuffle je technika, kde rozdělíte balíček karet přesně do poloviny, pak prolíná každou kartu z každé poloviny, aby se původní balíček znovu sestavil.

Kouzelníci používají tuto techniku, protože každá karta je ve známém místě po každém náhodném prohazování a pořadí je opakující se vzor.

Pro vaše účely je to lehký pohled na manipulaci s posloupnostmi dat. Aplikace, kterou sestavíte, sestaví balíček karet a pak provede posloupnost náhodného náhodného prohazování a pokaždé zapíše sekvenci. Porovnáte také aktualizovanou objednávku s původní objednávkou.

Tento kurz obsahuje několik kroků. Po každém kroku můžete aplikaci spustit a sledovat průběh. Dokončenou ukázku si můžete prohlédnout také v úložišti Dotnet/samples na GitHubu. Pokyny ke stažení najdete v tématu Ukázky a kurzy.

Požadavky

Budete muset nastavit počítač tak, aby běžel .NET Core. Pokyny k instalaci najdete na stránce pro stažení .NET Core. Tuto aplikaci můžete spustit ve Windows, Ubuntu Linuxu nebo OS X nebo v kontejneru Dockeru. Budete muset nainstalovat svůj oblíbený editor kódu. Níže uvedené popisy používají Visual Studio Code , což je opensourcový editor pro různé platformy. Můžete ale použít libovolné nástroje, se kterými jste spokojení.

Vytvoření aplikace

Prvním krokem je vytvoření nové aplikace. Otevřete příkazový řádek a vytvořte nový adresář pro vaši aplikaci. Nastavte aktuální adresář. Na příkazovém řádku zadejte příkaz dotnet new console . Tím se vytvoří počáteční soubory pro základní aplikaci Hello World.

Pokud jste jazyk C# ještě nikdy nepoužívali, tento kurz vysvětluje strukturu programu jazyka C#. Můžete si to přečíst a vrátit se sem, abyste se dozvěděli více o LINQ.

Vytvoření datové sady

Než začnete, ujistěte se, že následující řádky jsou v horní části Program.cs souboru vygenerované dotnet new console:

// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;

Pokud tyto tři řádky (using příkazy) nejsou v horní části souboru, náš program nebude kompilovat.

Teď, když máte všechny odkazy, které budete potřebovat, zvažte, co představuje balíček karet. Balíček hracích karet má obvykle čtyři obleky a každý oblek má třináct hodnot. Za normálních okolností můžete zvážit vytvoření Card třídy přímo od baterky a naplnění kolekce Card objektů ručně. S LINQ můžete být stručnější než obvyklý způsob práce s vytvářením balíčku karet. Místo vytváření Card třídy můžete vytvořit dvě sekvence, které představují obleky a pořadí. Vytvoříte opravdu jednoduchou dvojici metod iterátoru, která vygeneruje pořadí a obleky jako IEnumerable<T>řetězce:

// Program.cs
// The Main() method

static IEnumerable<string> Suits()
{
    yield return "clubs";
    yield return "diamonds";
    yield return "hearts";
    yield return "spades";
}

static IEnumerable<string> Ranks()
{
    yield return "two";
    yield return "three";
    yield return "four";
    yield return "five";
    yield return "six";
    yield return "seven";
    yield return "eight";
    yield return "nine";
    yield return "ten";
    yield return "jack";
    yield return "queen";
    yield return "king";
    yield return "ace";
}

Umístěte Program.cs je pod metodu Main do souboru. Obě tyto dvě metody využívají yield return syntaxi k vytvoření sekvence při jejich spuštění. Kompilátor sestaví objekt, který implementuje IEnumerable<T> a generuje sekvenci řetězců, jak jsou požadovány.

Teď pomocí těchto metod iterátoru vytvořte balíček karet. Dotaz LINQ umístíte do naší Main metody. Podívejte se na ni:

// Program.cs
static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    // Display each card that we've generated and placed in startingDeck in the console
    foreach (var card in startingDeck)
    {
        Console.WriteLine(card);
    }
}

from Více klauzulí vytvoří SelectMany, který vytvoří jednu sekvenci z kombinování každého prvku v první sekvenci s každým prvkem ve druhé sekvenci. Pořadí je pro naše účely důležité. První prvek v první zdrojové sekvenci (Suits) je kombinován s každým prvkem ve druhé sekvenci (Ranks). To vytvoří všech třináct karet prvního obleku. Tento proces se opakuje s každým prvkem v první sekvenci (Suits). Konečným výsledkem je balíček karet seřazených podle obleků následovaných hodnotami.

Je důležité mít na paměti, že pokud se rozhodnete psát LINQ v syntaxi dotazu použité výše, nebo místo toho použít syntaxi metody, je vždy možné přejít z jedné formy syntaxe na druhou. Výše uvedený dotaz napsaný v syntaxi dotazu lze zapsat v syntaxi metody jako:

var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { Suit = suit, Rank = rank }));

Kompilátor přeloží příkazy LINQ napsané pomocí syntaxe dotazu do ekvivalentní syntaxe volání metody. Proto bez ohledu na vaši volbu syntaxe vytvoří dvě verze dotazu stejný výsledek. Zvolte, která syntaxe je pro vaši situaci nejvhodnější: pokud například pracujete v týmu, kde někteří členové mají s syntaxí metody potíže, zkuste raději použít syntaxi dotazu.

Pokračujte a spusťte ukázku, kterou jste vytvořili v tomto okamžiku. Zobrazí se v balíčku všech 52 karet. Může být velmi užitečné spustit tuto ukázku v ladicím programu, abyste mohli sledovat, jak se Suits() tyto metody provádějí Ranks() . Jasně vidíte, že každý řetězec v každé sekvenci je generován pouze podle potřeby.

A console window showing the app writing out 52 cards.

Manipulace s pořadím

Dále se zaměřte na to, jak budete karty v balíčku prohazovat. Prvním krokem v každém dobrém náhodném prohazování je rozdělit palubu do dvou. Metody Take , Skip které jsou součástí rozhraní LINQ API, poskytují tuto funkci za vás. Umístěte je pod smyčku foreach :

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    // 52 cards in a deck, so 52 / 2 = 26
    var top = startingDeck.Take(26);
    var bottom = startingDeck.Skip(26);
}

Neexistuje ale žádná metoda náhodného náhodného prohazování, která by využívala standardní knihovnu, takže budete muset napsat vlastní. Metoda náhodného prohazování, kterou vytvoříte, ilustruje několik technik, které budete používat s programy založenými na LINQ, takže každá část tohoto procesu bude vysvětlena v krocích.

Abyste mohli přidat některé funkce k tomu, jak budete pracovat s IEnumerable<T> dotazy LINQ, budete muset napsat některé speciální druhy metod označovaných jako rozšiřující metody. Stručně řečeno, rozšiřující metoda je speciální statická metoda , která přidává nové funkce do již existujícího typu, aniž byste museli upravovat původní typ, do kterého chcete přidat funkce.

Dejte metodám rozšíření nový domov přidáním nového souboru statické třídy do vašeho programu volaný Extensions.csa pak začněte vytvářet první metodu rozšíření:

// Extensions.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace LinqFaroShuffle
{
    public static class Extensions
    {
        public static IEnumerable<T> InterleaveSequenceWith<T>(this IEnumerable<T> first, IEnumerable<T> second)
        {
            // Your implementation will go here soon enough
        }
    }
}

Podívejte se na podpis metody na chvíli, konkrétně parametry:

public static IEnumerable<T> InterleaveSequenceWith<T> (this IEnumerable<T> first, IEnumerable<T> second)

Přidání modifikátoru this můžete vidět u prvního argumentu metody. To znamená, že voláte metodu, jako by byla členová metoda typu prvního argumentu. Tato deklarace metody také následuje standardní idiom, kde vstupní a výstupní typy jsou IEnumerable<T>. Tento postup umožňuje zřetězeným metodám LINQ provádět složitější dotazy.

Samozřejmě, protože rozdělíte palubu na poloviny, budete muset spojit ty poloviny dohromady. V kódu to znamená, že budete vytvářet výčet obou sekvencí, které jste získali, Take a Skip najednou, interleaving prvků a vytvoření jedné sekvence: nyní prohazovaného balíčku karet. Vytvoření metody LINQ, která funguje se dvěma sekvencemi, vyžaduje, abyste pochopili, jak IEnumerable<T> funguje.

Rozhraní IEnumerable<T> má jednu metodu: GetEnumerator. Objekt vrácený GetEnumerator má metodu pro přechod na další prvek a vlastnost, která načte aktuální prvek v sekvenci. Tyto dva členy použijete k vytvoření výčtu kolekce a vrácení prvků. Tato metoda prokládání bude metodou iterátoru, takže místo sestavení kolekce a vrácení kolekce použijete yield return syntaxi uvedenou výše.

Tady je implementace této metody:

public static IEnumerable<T> InterleaveSequenceWith<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while (firstIter.MoveNext() && secondIter.MoveNext())
    {
        yield return firstIter.Current;
        yield return secondIter.Current;
    }
}

Teď, když jste napsali tuto metodu Main , se vraťte k metodě a jednou zamícháte balíček:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = from s in Suits()
                       from r in Ranks()
                       select new { Suit = s, Rank = r };

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    var top = startingDeck.Take(26);
    var bottom = startingDeck.Skip(26);
    var shuffle = top.InterleaveSequenceWith(bottom);

    foreach (var c in shuffle)
    {
        Console.WriteLine(c);
    }
}

Porovnání

Kolik náhodného prohazu trvá nastavit palubu zpět do původního pořadí? Abyste zjistili, budete muset napsat metodu, která určuje, jestli jsou dvě sekvence stejné. Jakmile budete mít tuto metodu, budete muset umístit kód, který zamíchá balíček do smyčky, a zkontrolovat, kdy je balíček v pořádku.

Napsání metody, která určí, jestli jsou dvě sekvence stejné, by měly být jednoduché. Je to podobná struktura jako metoda, kterou jste napsali pro náhodné prohazování balíčku. Pouze tentokrát místo yield returnkaždého prvku porovnáte odpovídající prvky jednotlivých sekvencí. Pokud se celá sekvence vyčíslí, pokud se všechny prvky shodují, jsou sekvence stejné:

public static bool SequenceEquals<T>
    (this IEnumerable<T> first, IEnumerable<T> second)
{
    var firstIter = first.GetEnumerator();
    var secondIter = second.GetEnumerator();

    while ((firstIter?.MoveNext() == true) && secondIter.MoveNext())
    {
        if ((firstIter.Current is not null) && !firstIter.Current.Equals(secondIter.Current))
        {
            return false;
        }
    }

    return true;
}

Zobrazí se druhý idiom LINQ: terminálové metody. Jako vstup přebírají sekvenci (nebo v tomto případě dvě sekvence) a vrátí jednu skalární hodnotu. Při použití terminálových metod jsou vždy konečnou metodou v řetězu metod pro dotaz LINQ, a proto název "terminal".

Tuto akci uvidíte, když ji použijete k určení, kdy se balíček vrátí v původním pořadí. Umístěte kód náhodného prohazování do smyčky a zastavte, když je sekvence zpět v původním pořadí použitím SequenceEquals() metody. V libovolném dotazu se vždy zobrazí konečná metoda, protože místo sekvence vrátí jednu hodnotu:

// Program.cs
static void Main(string[] args)
{
    // Query for building the deck

    // Shuffling using InterleaveSequenceWith<T>();

    var times = 0;
    // We can re-use the shuffle variable from earlier, or you can make a new one
    shuffle = startingDeck;
    do
    {
        shuffle = shuffle.Take(26).InterleaveSequenceWith(shuffle.Skip(26));

        foreach (var card in shuffle)
        {
            Console.WriteLine(card);
        }
        Console.WriteLine();
        times++;

    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Spusťte kód, který jste zatím získali, a poznamenejte si, jak se při každém náhodném prohazování přeuspořádá prezentace. Po 8 prohazování (iterace smyčky do-while) se balíček vrátí k původní konfiguraci, ve které byl při prvním vytvoření z počátečního dotazu LINQ.

Optimalizace

Ukázka, kterou jste zatím vytvořili, provede náhodné prohazování, kde horní a dolní karty zůstanou na každém spuštění stejné. Pojďme udělat jednu změnu: místo toho použijeme prohazovací tlačítko, kde se změní pozice všech 52 karet. V případě náhodného prohazování prokládáte balíček tak, aby první karta v dolní polovině byla první kartou v balíčku. To znamená, že poslední karta v horní polovině se stane dolní kartou. Jedná se o jednoduchou změnu na jediný řádek kódu. Aktualizujte aktuální dotaz náhodného náhodného prohazování přepnutím pozic Take a Skip. Tím se změní pořadí horní a dolní poloviny paluby:

shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));

Spusťte program znovu a uvidíte, že přeuspořádání balíčku trvá 52 iterací. Začnete si také všimnout některých závažných snížení výkonu, protože program pokračuje ve spuštění.

Existuje několik důvodů. Můžete řešit jednu z hlavních příčin tohoto poklesu výkonu: neefektivní využití opožděného hodnocení.

Stručně řečeno, opožděné vyhodnocení uvádí, že vyhodnocení příkazu se neprovádí, dokud nebude potřeba jeho hodnota. Dotazy LINQ jsou příkazy, které se vyhodnocují opožděně. Sekvence se generují pouze vygenerování prvků, které jsou požadovány. Obvykle je to hlavní výhoda LINQ. Při použití, jako je například tento program, to však způsobuje exponenciální růst doby provádění.

Mějte na paměti, že jsme vygenerovali původní balíček pomocí dotazu LINQ. Každý shuffle se generuje provedením tří dotazů LINQ v předchozí prezentaci. Všechny tyto jsou prováděny lazily. To také znamená, že se provádějí znovu při každém vyžádání sekvence. V době, kdy se dostanete k 52. iteraci, vygenerujete původní palubu mnohokrát, mnohokrát. Pojďme napsat protokol, který toto chování předvede. Pak to opravíte.

Do souboru zadejte nebo zkopírujte Extensions.cs následující metodu. Tato metoda rozšíření vytvoří nový soubor volaný debug.log v adresáři projektu a zaznamenává, jaký dotaz se právě spouští do souboru protokolu. Tuto metodu rozšíření lze připojit k libovolnému dotazu, který označí, že se dotaz spustil.

public static IEnumerable<T> LogQuery<T>
    (this IEnumerable<T> sequence, string tag)
{
    // File.AppendText creates a new file if the file doesn't exist.
    using (var writer = File.AppendText("debug.log"))
    {
        writer.WriteLine($"Executing Query {tag}");
    }

    return sequence;
}

Uvidíte červenou vlnovku File, což znamená, že neexistuje. Nebude se kompilovat, protože kompilátor neví, co File je. Chcete-li tento problém vyřešit, nezapomeňte přidat následující řádek kódu pod úplně první řádek v Extensions.cs:

using System.IO;

Tím by se měl problém vyřešit a červená chyba zmizí.

Dále instrumentujte definici každého dotazu se zprávou protokolu:

// Program.cs
public static void Main(string[] args)
{
    var startingDeck = (from s in Suits().LogQuery("Suit Generation")
                        from r in Ranks().LogQuery("Rank Generation")
                        select new { Suit = s, Rank = r }).LogQuery("Starting Deck");

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    Console.WriteLine();
    var times = 0;
    var shuffle = startingDeck;

    do
    {
        // Out shuffle
        /*
        shuffle = shuffle.Take(26)
            .LogQuery("Top Half")
            .InterleaveSequenceWith(shuffle.Skip(26)
            .LogQuery("Bottom Half"))
            .LogQuery("Shuffle");
        */

        // In shuffle
        shuffle = shuffle.Skip(26).LogQuery("Bottom Half")
                .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
                .LogQuery("Shuffle");

        foreach (var c in shuffle)
        {
            Console.WriteLine(c);
        }

        times++;
        Console.WriteLine(times);
    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Všimněte si, že se nepřihlašujete pokaždé, když přistupujete k dotazu. Protokolujete pouze při vytváření původního dotazu. Program stále trvá dlouhou dobu, ale teď můžete zjistit, proč. Pokud vyčerpáte trpělivost při náhodném prohazování se zapnutým protokolováním, přepněte zpátky na náhodné prohazování. Stále uvidíte opožděné účinky vyhodnocení. V jednom spuštění spustí 2592 dotazů, včetně všech hodnot a generování obleků.

Tady můžete zvýšit výkon kódu, abyste snížili počet provádění, které provedete. Jednoduchou opravou, kterou můžete provést, je uložit do mezipaměti výsledky původního dotazu LINQ, který vytváří balíček karet. V současné době provádíte dotazy znovu a znovu pokaždé, když smyčka do-while prochází iterací, znovu sestaví balíček karet a pokaždé ho znovu prohodí. Pokud chcete uložit balíček karet do mezipaměti, můžete využít metody ToArray LINQ a ToList– když je připojíte k dotazům, budou provádět stejné akce, kterým jste je řekli, ale teď budou ukládat výsledky do pole nebo seznamu v závislosti na tom, jakou metodu se rozhodnete volat. Připojte metodu ToArray LINQ k dotazům a spusťte program znovu:

public static void Main(string[] args)
{
    IEnumerable<Suit>? suits = Suits();
    IEnumerable<Rank>? ranks = Ranks();

    if ((suits is null) || (ranks is null))
        return;

    var startingDeck = (from s in suits.LogQuery("Suit Generation")
                        from r in ranks.LogQuery("Value Generation")
                        select new { Suit = s, Rank = r })
                        .LogQuery("Starting Deck")
                        .ToArray();

    foreach (var c in startingDeck)
    {
        Console.WriteLine(c);
    }

    Console.WriteLine();

    var times = 0;
    var shuffle = startingDeck;

    do
    {
        /*
        shuffle = shuffle.Take(26)
            .LogQuery("Top Half")
            .InterleaveSequenceWith(shuffle.Skip(26).LogQuery("Bottom Half"))
            .LogQuery("Shuffle")
            .ToArray();
        */

        shuffle = shuffle.Skip(26)
            .LogQuery("Bottom Half")
            .InterleaveSequenceWith(shuffle.Take(26).LogQuery("Top Half"))
            .LogQuery("Shuffle")
            .ToArray();

        foreach (var c in shuffle)
        {
            Console.WriteLine(c);
        }

        times++;
        Console.WriteLine(times);
    } while (!startingDeck.SequenceEquals(shuffle));

    Console.WriteLine(times);
}

Teď je náhodné prohazy až 30 dotazů. Spusťte znovu příkazem náhodného prohazu a uvidíte podobná vylepšení: teď spouští 162 dotazů.

Upozorňujeme, že tento příklad je navržený tak, aby zvýrazňoval případy použití, kdy opožděné vyhodnocení může způsobit potíže s výkonem. I když je důležité zjistit, kde opožděné vyhodnocení může mít vliv na výkon kódu, je stejně důležité pochopit, že ne všechny dotazy by měly běžet dychtivě. Výkon, který jste dosáhli bez použití ToArray , spočívá v tom, že každé nové uspořádání balíčku karet je sestaveno z předchozího uspořádání. Použití opožděného vyhodnocení znamená, že každá nová konfigurace balíčku je vytvořena z původní prezentace, a to i spuštění kódu, který vytvořil startingDeck. To způsobuje velké množství nadbytečné práce.

V praxi některé algoritmy dobře běží s využitím dychtivých vyhodnocení a jiné běží dobře pomocí opožděné vyhodnocení. V případě denního využití je opožděné vyhodnocení obvykle lepší volbou, pokud je zdrojem dat samostatný proces, jako je databázový stroj. U databází umožňuje opožděné vyhodnocení provádět složitější dotazy pouze jeden proces odezvy do databázového procesu a zpět ke zbytku kódu. LINQ je flexibilní bez ohledu na to, jestli se rozhodnete využít opožděné nebo dychtivé vyhodnocení, takže změřte procesy a vyberte, jaký druh vyhodnocení vám dává nejlepší výkon.

Závěr

V tomto projektu jste se zabývali:

  • Použití dotazů LINQ k agregaci dat do smysluplné sekvence
  • Psaní metod rozšíření pro přidání vlastních funkcí do dotazů LINQ
  • vyhledání oblastí v našem kódu, kde se naše dotazy LINQ můžou narazit na problémy s výkonem, jako je snížení rychlosti
  • opožděné a dychtivé vyhodnocení týkající se dotazů LINQ a dopadů, které můžou mít na výkon dotazů

Kromě LINQ jste se dozvěděli trochu o technice kouzelníci, které používají pro triky karet. Kouzelníci používají faerské náhodné prohazení, protože můžou řídit, kde se každá karta pohybuje v palubě. Teď, když víte, nekazte to všem ostatním!

Další informace o LINQ najdete tady: