Sdílet prostřednictvím


Práce s dotazem Language-Integrated (LINQ)

Úvod

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

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

Tyto techniky se naučíte vytvořením aplikace, která ukazuje jednu ze základních dovedností každého kouzelníka: faro shuffle. Stručně řečeno, faro shuffle je technika, při které rozdělíte balíček karet přesně na poloviny a pak vkládáte karty z jedné poloviny mezi karty z druhé poloviny, aby se znovu vytvořil původní balíček.

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

Pro vaše účely je to odlehčený pohled na zpracování datových sekvencí. Aplikace, kterou sestavíte, vypracuje balíček karet a poté provede posloupnost prohazování a pořadí pokaždé zaznamená. 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. Můžete si také prohlédnout dokončenou ukázku v úložišti dotnet/samples na GitHubu. Pokyny ke stažení najdete ve vzorech a návodech .

Požadavky

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 to jako 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 jsou následující řádky v horní části souboru Program.cs vygenerované dotnet new console:

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

Pokud tyto tři řádky (direktivyusing) nejsou v horní části souboru, nemusí se program 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í třídy Card hned a naplnění kolekce objektů Card 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 vytvoř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é vygenerují pořadí a barvy jako řetězce typu IEnumerable<T>:

// 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 je pod metodu Main do souboru Program.cs. Obě tyto dvě metody využívají syntaxi yield return k vytvoření sekvence při jejich spuštění. Kompilátor sestaví objekt, který implementuje IEnumerable<T> a vygeneruje 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ší metody Main. 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);
    }
}

Více klauzulí from vytvoří SelectMany, což vytvoří jednu sekvenci kombinací 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 (v „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í všech 52 karet v balíčku. Může být velmi užitečné spustit tuto ukázku v ladicím programu, abyste zjistili, jak se metody Suits() a Ranks() provádějí. Jasně vidíte, že každý řetězec v každé sekvenci je generován pouze podle potřeby.

okno konzoly zobrazující aplikaci, která zapisuje 52 karet.

Upravit pořadí

Dále se zaměřte na to, jak budete míchat balíček karet. Prvním krokem v každém dobrém míchání je rozdělit balíček na dvě části. Metody Take a 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 prohazování ve standardní knihovně, takže budete muset napsat vlastní. Metoda zamíchání, kterou vytvoříte, ilustruje několik technik, které budete používat s programy založenými na LINQ, tak bude každý krok tohoto procesu vysvětlen samostatně.

Abyste mohli přidat některé funkce pro způsob interakce s IEnumerable<T>, které získáváte z dotazů LINQ, budete muset napsat speciální druhy metod nazývané rozšiřující metody. Stručně řečeno, rozšiřující metoda je speciální účel 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 statického souboru třídy do programu s názvem 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
        }
    }
}

Na chvíli se podívejte na deklaraci metody, konkrétně na parametry:

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

Můžete vidět přidání modifikátoru this v prvním argumentu metody. To znamená, že voláte metodu, jako kdyby to byla metoda člena typu prvního argumentu. Tato deklarace metody také následuje standardní vzorec, 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 balíček na poloviny, je potřeba je spojit dohromady. V kódu to znamená, že budete vytvářet výčet obou sekvencí, které jste získali prostřednictvím Take a Skip najednou, interleaving prvků a vytvořit jednu sekvenci: nyní prohazovanou sadu 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 posloupnosti. Tyto dva členy použijete k provádění výčtu kolekce a k vracení jejích prvků. Tato metoda interleavingu bude metodou iterátoru, takže místo sestavení kolekce a navrácení kolekce použijete syntaxi yield return 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, vraťte se k metodě Main a jednou zamíchejte 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 zamíchání je potřeba ke srovnání balíčku 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 zpět v původním pořadí.

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 na zamíchání balíčku. Pouze tentokrát místo yield returnjednotlivých prvků 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".

Můžete to vidět v praxi, když ji použijete k určení, kdy se balíček vrátí do původního pořadí. Umístěte kód míchání do smyčky a přestaňte, jakmile se sekvence vrátí do původního pořadí použitím metody SequenceEquals(). 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 balíček při každém prohazování přeuspořádá. Po osmi přeházeních (iteracích smyčky do-while) se balíček vrátí do původní konfigurace, ve které byl, když jste ho poprvé vytvořili z výchozího dotazu LINQ.

Optimalizace

V ukázce, kterou jste zatím vytvořili, se provádí technika míchání karet zvaná, při které horní a dolní karty zůstávají při každém spuštění na stejném místě. Pojďme provést jednu změnu: místo toho použijeme v míchání, kde se změní pozice všech 52 karet. V případě vnitřního míchá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í náhodný dotaz 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 výrazného poklesu výkonu, jak program běží dále.

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

Stručně řečeno, líné vyhodnocení uvádí, že vyhodnocení příkazu se neprovádí, dokud není potřeba jeho hodnota. Dotazy LINQ jsou příkazy, které se vyhodnocují líně. Sekvence se generují pouze tehdy, když jsou prvky 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é zamíchání se generuje provedením tří dotazů LINQ na předchozím balíčku. Všechny tyto jsou prováděny líně. To také znamená, že se provádějí znovu při každém vyžádání sekvence. Až se dostanete k 52. opakování, regenerujete původní balíček karet mnohokrát, mnohokrát. Pojďme napsat záznam, který toto chování ukáže. Pak to opravíte.

Do souboru Extensions.cs zadejte nebo zkopírujte následující metodu. Tato metoda rozšíření vytvoří nový soubor s názvem debug.log v adresáři projektu a zaznamená, 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 pod File, což znamená, že neexistuje. Nepůjde zkompilovat, 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 přidejte k definici každého dotazu logovací zprávu:

// 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. Provádíte záznam pouze při vytváření původního dotazu. Program stále trvá dlouhou dobu, ale teď můžete zjistit, proč. Pokud při míchání v režimu 'in' se zapnutým protokolováním vyčerpáte trpělivost, přepněte zpátky na míchání v režimu 'out'. Stále uvidíte efekty líného vyhodnocování. V jednom spuštění spustí 2592 dotazů, včetně všech hodnot a generování barev.

Tady můžete zvýšit výkon kódu, abyste snížili počet provádění, které provedete. Jednoduchá oprava, kterou můžete provést, je ukládání do mezipaměti výsledků z původního dotazu LINQ, který vytváří balíček karet. V současné době znovu a znovu provádíte dotazy pokaždé, když smyčka do-while prochází iterací, přičemž pokaždé znovu sestavujete balíček karet a znovu ho mícháte. K ukládání balíčku karet do mezipaměti můžete využít metody LINQ ToArray a ToList; když je připojíte k dotazům, budou provádět stejné akce, kterým jste je řekli, ale teď uloží výsledky do pole nebo seznamu v závislosti na tom, jakou metodu zvolíte pro volání. Připojte metodu LINQ ToArray 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 vnější míchání omezeno na 30 dotazů. Spusťte znovu s použitím funkce "in shuffle" a uvidíte podobná vylepšení: nyní spouští 162 dotazů.

Upozorňujeme, že tento příklad je navržený k zdůraznění případů použití, kdy lí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 okamžitě. Bez použití ToArray dochází k dopadu na výkon, protože každé nové uspořádání balíčku karet se vytváří z předchozího uspořádání. Použití opožděného vyhodnocení znamená, že každá nová konfigurace balíčku karet je vytvořena z původního balíčku, dokonce i spuštěním kódu, který vytvořil startingDeck. To způsobuje velké množství nadbytečné práce.

V praxi některé algoritmy běží dobře při využití striktního vyhodnocování, a jiné při využití opožděného vyhodnocování. 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 líné vyhodnocování provádět složitější dotazy pouze s jednou cestou tam a zpět do databázového procesu a zpět k vašemu kódu. LINQ je flexibilní bez ohledu na to, jestli se rozhodnete využít líné nebo horlivé vyhodnocení, takže vyhodnoťte vaše procesy a vyberte, které vyhodnocení poskytuje 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 naše LINQ dotazy mohou narazit na problémy s výkonem, jako je degradace rychlosti
  • opožděné a bezodkladné vyhodnocení týkající se dotazů LINQ a jejich dopadů na výkon dotazů

Kromě LINQ jste se dozvěděli trochu o technice, kterou kouzelníci používají pro karetní triky. Kouzelníci používají Faro shuffle, protože mohou ovládat, kde se každá karta v balíčku pohybuje. Teď, když to víte, nekazte to ostatním!

Další informace o LINQ najdete tady: