Werken met Language-Integrated Query (LINQ)

Inleiding

In deze zelfstudie leert u functies in .NET Core en de C#-taal. U leert het volgende:

  • Reeksen genereren met LINQ.
  • Schrijfmethoden die eenvoudig kunnen worden gebruikt in LINQ-query's.
  • Onderscheid maken tussen gretige en luie evaluatie.

U leert deze technieken door een toepassing te bouwen die een van de basisvaardigheden van elke tovenaar demonstreert: de faro shuffle. Kort gezegd, een faro shuffle is een techniek waarbij je een kaartdek precies in de helft splitst, vervolgens de willekeurige interleaven van elke kaart van elke helft om het oorspronkelijke dek te herbouwen.

Magicians gebruiken deze techniek omdat elke kaart zich na elke willekeurige willekeurige volgorde op een bekende locatie bevindt en de volgorde een herhalend patroon is.

Voor uw doeleinden is het een licht hartige blik op het manipuleren van reeksen gegevens. De toepassing die u bouwt, bouwt een kaartserie en voert vervolgens elke keer een reeks willekeurige volgordes uit en schrijft de reeks uit. U vergelijkt ook de bijgewerkte bestelling met de oorspronkelijke bestelling.

Deze zelfstudie heeft meerdere stappen. Na elke stap kunt u de toepassing uitvoeren en de voortgang bekijken. U kunt het voltooide voorbeeld ook zien in de GitHub-opslagplaats dotnet/samples. Zie Voorbeelden en zelfstudies voor downloadinstructies.

Vereisten

U moet uw computer instellen voor het uitvoeren van .NET Core. U vindt de installatie-instructies op de downloadpagina van .NET Core . U kunt deze toepassing uitvoeren op Windows, Ubuntu Linux of OS X, of in een Docker-container. U moet uw favoriete code-editor installeren. In de onderstaande beschrijvingen wordt Visual Studio Code gebruikt. Dit is een open source, platformoverschrijdende editor. U kunt echter ook de hulpprogramma's gebruiken waarmee u vertrouwd bent.

De toepassing maken

De eerste stap bestaat uit het maken van een nieuwe toepassing. Open een opdrachtprompt en maak een nieuwe map voor uw toepassing. Maak hiervan de huidige map. Typ de opdracht dotnet new console bij de opdrachtprompt. Hiermee maakt u de startersbestanden voor een eenvoudige 'Hallo wereld'-toepassing.

Als u C# nog nooit eerder hebt gebruikt, wordt in deze zelfstudie de structuur van een C#-programma uitgelegd. U kunt dat lezen en vervolgens hier teruggaan voor meer informatie over LINQ.

De gegevensset maken

Voordat u begint, moet u ervoor zorgen dat de volgende regels boven aan het Program.cs bestand staan dat wordt gegenereerd door dotnet new console:

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

Als deze drie regels (using instructies) zich niet boven aan het bestand bevinden, wordt het programma niet gecompileerd.

Nu u alle verwijzingen hebt die u nodig hebt, kunt u overwegen wat een stapel kaarten is. Meestal heeft een stapel speelkaarten vier pakken en elk pak heeft dertien waarden. Normaal gesproken kunt u overwegen om direct een Card klasse te maken van de vleermuis en een verzameling Card objecten met de hand in te vullen. Met LINQ kunt u beknopter zijn dan de gebruikelijke manier om een stapel kaarten te maken. In plaats van een Card klasse te maken, kunt u twee reeksen maken om respectievelijk pakken en rangschikkingen weer te geven. U maakt een heel eenvoudig paar iteratormethoden waarmee de rangschikkingen en pakken worden gegenereerd als IEnumerable<T>tekenreeksen:

// 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";
}

Plaats deze onder de Main methode in uw Program.cs bestand. Deze twee methoden maken beide gebruik van de yield return syntaxis om een reeks te produceren terwijl ze worden uitgevoerd. De compiler bouwt een object dat de reeks tekenreeksen implementeert IEnumerable<T> en genereert wanneer deze worden aangevraagd.

Gebruik nu deze iterator-methoden om het deck met kaarten te maken. U plaatst de LINQ-query in onze Main methode. Hier volgt een kijkje:

// 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);
    }
}

De meerdere from componenten produceren een SelectMany, waarmee één reeks wordt gemaakt van het combineren van elk element in de eerste reeks met elk element in de tweede reeks. De volgorde is belangrijk voor onze doeleinden. Het eerste element in de eerste bronreeks (Suits) wordt gecombineerd met elk element in de tweede reeks (Ranks). Dit produceert alle dertien kaarten van het eerste pak. Dat proces wordt herhaald met elk element in de eerste reeks (Suits). Het eindresultaat is een stapel kaarten gesorteerd op pakken, gevolgd door waarden.

Het is belangrijk om in gedachten te houden of u ervoor kiest om uw LINQ te schrijven in de bovenstaande querysyntaxis of de syntaxis van de methode te gebruiken, het is altijd mogelijk om van de ene vorm van syntaxis naar de andere te gaan. De bovenstaande query die in de querysyntaxis is geschreven, kan worden geschreven in de syntaxis van de methode als:

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

De compiler vertaalt LINQ-instructies die zijn geschreven met de querysyntaxis in de equivalente aanroepsyntaxis van de methode. Daarom produceren de twee versies van de query hetzelfde resultaat, ongeacht uw syntaxiskeuze. Kies welke syntaxis het beste werkt voor uw situatie: als u bijvoorbeeld werkt in een team waarin sommige leden moeite hebben met de syntaxis van de methode, probeert u de voorkeur te geven aan het gebruik van querysyntaxis.

Voer het voorbeeld uit dat u op dit moment hebt gemaakt. Alle 52 kaarten worden weergegeven in het dek. Het is misschien erg handig om dit voorbeeld uit te voeren onder een foutopsporingsprogramma om te zien hoe de Suits() en Ranks() methoden worden uitgevoerd. U kunt duidelijk zien dat elke tekenreeks in elke reeks alleen wordt gegenereerd wanneer deze nodig is.

A console window showing the app writing out 52 cards.

De volgorde bewerken

Vervolgens richt u zich op hoe u de kaarten in de stapel gaat verplaatsen. De eerste stap in een goede shuffle is het splitsen van het dek in tweeën. De Take en Skip methoden die deel uitmaken van de LINQ API's bieden die functie voor u. Plaats ze onder de lus 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);
}

Er is echter geen willekeurige methode om te profiteren van de standaardbibliotheek, dus u moet uw eigen bibliotheek schrijven. De shuffle-methode die u maakt, illustreert verschillende technieken die u gaat gebruiken met LINQ-programma's, dus elk deel van dit proces wordt uitgelegd in stappen.

Als u bepaalde functionaliteit wilt toevoegen aan de manier waarop u communiceert met de IEnumerable<T> query's van LINQ, moet u een aantal speciale soorten methoden schrijven die extensiemethoden worden genoemd. Kort gezegd is een extensiemethode een statische methode voor speciaal doel waarmee nieuwe functionaliteit wordt toegevoegd aan een al bestaand type zonder dat u het oorspronkelijke type waaraan u functionaliteit wilt toevoegen hoeft te wijzigen.

Geef uw extensiemethoden een nieuwe startpagina door een nieuw statisch klassebestand toe te voegen aan uw programma met de naam Extensions.csen begin vervolgens met het bouwen van de eerste extensiemethode:

// 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
        }
    }
}

Bekijk de methodehandtekening even, met name de parameters:

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

U kunt de toevoeging van de this modifier op het eerste argument aan de methode zien. Dit betekent dat u de methode aanroept alsof het een lidmethode is van het type van het eerste argument. Deze methodedeclaratie volgt ook een standaardidioom waarbij de invoer- en uitvoertypen zijn IEnumerable<T>. Met deze procedure kunnen LINQ-methoden worden gekoppeld om complexere query's uit te voeren.

Natuurlijk, omdat je het dek splitst in halven, moet je die helften samenvoegen. In code betekent dit dat u beide reeksen opsommen die u hebt verkregen via Take en Skip in één keer, interleaving de elementen en het maken van één reeks: uw nu geordend stapel kaarten. Als u een LINQ-methode schrijft die met twee reeksen werkt, moet u begrijpen hoe IEnumerable<T> het werkt.

De IEnumerable<T> interface heeft één methode: GetEnumerator. Het object dat wordt geretourneerd door GetEnumerator , heeft een methode om naar het volgende element te gaan en een eigenschap waarmee het huidige element in de reeks wordt opgehaald. U gebruikt deze twee leden om de verzameling op te sommen en de elementen te retourneren. Deze interleave-methode is een iteratormethode, dus in plaats van een verzameling te bouwen en de verzameling te retourneren, gebruikt u de yield return bovenstaande syntaxis.

Dit is de implementatie van die methode:

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;
    }
}

Nu u deze methode hebt geschreven, gaat u terug naar de Main methode en schuift u het dek eenmaal in willekeurige volgorde:

// 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);
    }
}

Vergelijkingen

Hoeveel shuffles het kost om het dek terug te zetten op de oorspronkelijke volgorde? Om erachter te komen, moet u een methode schrijven die bepaalt of twee reeksen gelijk zijn. Nadat u deze methode hebt, moet u de code die het dek in een lus plaatst, plaatsen en controleren wanneer de dek weer in orde is.

Het schrijven van een methode om te bepalen of de twee reeksen gelijk zijn, moet eenvoudig zijn. Het is een vergelijkbare structuur als de methode die u hebt geschreven om het dek te schuiven. Alleen deze keer vergelijkt u de overeenkomende elementen van elke reeks in plaats van yield returnelk element. Wanneer de hele reeks is geïnventariseerd, als elk element overeenkomt, zijn de reeksen hetzelfde:

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;
}

Hier ziet u een tweede LINQ-idiom: terminalmethoden. Ze nemen een reeks als invoer (of in dit geval twee reeksen) en retourneren één scalaire waarde. Wanneer u terminalmethoden gebruikt, zijn ze altijd de laatste methode in een keten van methoden voor een LINQ-query, vandaar de naam 'terminal'.

U kunt dit in actie zien wanneer u het gebruikt om te bepalen wanneer het dek weer in de oorspronkelijke volgorde staat. Plaats de willekeurige code in een lus en stop wanneer de reeks weer in de oorspronkelijke volgorde staat door de methode toe te SequenceEquals() passen. U kunt zien dat dit altijd de laatste methode is in elke query, omdat deze één waarde retourneert in plaats van een reeks:

// 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);
}

Voer de code uit die u tot nu toe hebt en noteer hoe de dek voor elke willekeurige volgorde opnieuw wordt gerangschikt. Na 8 willekeurige volgordes (iteraties van de do-while-lus) keert de deck terug naar de oorspronkelijke configuratie waarin deze zich bevond toen u deze voor het eerst maakte vanaf de beginnende LINQ-query.

Optimalisaties

Het voorbeeld dat u tot nu toe hebt gemaakt, voert een uit willekeurige volgorde uit, waarbij de bovenste en onderste kaarten bij elke uitvoering hetzelfde blijven. Laten we een wijziging aanbrengen: in plaats daarvan gebruiken we een in willekeurige volgorde , waarbij alle 52 kaarten de positie wijzigen. Voor een in willekeurige volgorde interleave je het dek zodat de eerste kaart in de onderste helft de eerste kaart in het dek wordt. Dat betekent dat de laatste kaart in de bovenste helft de onderste kaart wordt. Dit is een eenvoudige wijziging in een enkele coderegel. Werk de huidige shuffle-query bij door de posities van Take en Skip. Hierdoor verandert de volgorde van de bovenste en onderste helften van het dek:

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

Voer het programma opnieuw uit en u ziet dat het 52 iteraties duurt voordat het dek opnieuw ordenen. U zult ook zien dat er ernstige prestatieverminderingen optreden terwijl het programma blijft werken.

Er zijn hier een aantal redenen voor. U kunt een van de belangrijkste oorzaken van deze prestatievermindering aanpakken: inefficiënt gebruik van luie evaluatie.

Kortom, luie evaluatie geeft aan dat de evaluatie van een instructie pas wordt uitgevoerd als de waarde ervan nodig is. LINQ-query's zijn instructies die lazily worden geëvalueerd. De reeksen worden alleen gegenereerd wanneer de elementen worden aangevraagd. Meestal is dat een groot voordeel van LINQ. In een gebruik zoals dit programma zorgt dit echter voor exponentiële groei in uitvoeringstijd.

Houd er rekening mee dat we de oorspronkelijke deck hebben gegenereerd met behulp van een LINQ-query. Elke willekeurige volgorde wordt gegenereerd door drie LINQ-query's uit te voeren op de vorige deck. Al deze worden lazily uitgevoerd. Dat betekent ook dat ze opnieuw worden uitgevoerd telkens wanneer de reeks wordt aangevraagd. Tegen de tijd dat u bij de 52e iteratie komt, hernieuwt u het oorspronkelijke dek vaak, vaak. Laten we een logboek schrijven om dit gedrag te demonstreren. Dan lost u het op.

Typ of kopieer de onderstaande methode in uw Extensions.cs bestand. Met deze extensiemethode maakt u een nieuw bestand dat wordt aangeroepen debug.log in de projectmap en registreert u welke query momenteel wordt uitgevoerd in het logboekbestand. Deze extensiemethode kan worden toegevoegd aan elke query om aan te geven dat de query is uitgevoerd.

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;
}

U ziet een rode kronkel onder File, wat betekent dat deze niet bestaat. De compiler wordt niet gecompileerd, omdat de compiler niet weet wat File het is. Om dit probleem op te lossen, moet u de volgende coderegel toevoegen onder de eerste regel in Extensions.cs:

using System.IO;

Hiermee lost u het probleem op en verdwijnt de rode fout.

Instrumenteer vervolgens de definitie van elke query met een logboekbericht:

// 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);
}

U ziet dat u zich niet telkens aanmeldt wanneer u een query opent. U meldt zich alleen aan wanneer u de oorspronkelijke query maakt. Het programma duurt nog steeds lang om te worden uitgevoerd, maar nu kunt u zien waarom. Als u niet meer geduld hebt om de in willekeurige volgorde uit te voeren terwijl logboekregistratie is ingeschakeld, schakelt u terug naar de out shuffle. U ziet nog steeds de luie evaluatie-effecten. In één uitvoering worden 2592 query's uitgevoerd, inclusief alle waarden en het genereren van pak.

U kunt de prestaties van de code hier verbeteren om het aantal uitvoeringen te verminderen dat u maakt. Een eenvoudige oplossing die u kunt maken, is het opslaan van de resultaten van de oorspronkelijke LINQ-query waarmee de stapel kaarten wordt samengesteld. Op dit moment voert u de query's steeds opnieuw uit wanneer de do-while-lus een iteratie doorloopt, de stapel kaarten opnieuw samenstellen en elke keer opnieuw toewijzen. Als u de stapel kaarten in de cache wilt opslaan, kunt u gebruikmaken van de LINQ-methoden ToArray en ToList; wanneer u ze toevoegt aan de query's, voeren ze dezelfde acties uit die u ze hebt verteld, maar nu slaan ze de resultaten op in een matrix of een lijst, afhankelijk van de methode die u kiest om aan te roepen. Voeg de LINQ-methode ToArray toe aan beide query's en voer het programma opnieuw uit:

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);
}

De out shuffle is nu 30 query's. Voer opnieuw uit met de in willekeurige volgorde en u ziet vergelijkbare verbeteringen: er worden nu 162 query's uitgevoerd.

Houd er rekening mee dat dit voorbeeld is ontworpen om de gebruiksvoorbeelden te benadrukken waarbij luie evaluatie prestatieproblemen kan veroorzaken. Hoewel het belangrijk is om te zien waar luie evaluatie van invloed kan zijn op de codeprestaties, is het even belangrijk om te begrijpen dat niet alle query's gretig moeten worden uitgevoerd. De prestatietreffer die u zonder gebruik ToArray maakt, is omdat elke nieuwe rangschikking van het kaartspel is gebouwd op basis van de vorige rangschikking. Het gebruik van luie evaluatie betekent dat elke nieuwe dekconfiguratie is gebouwd op basis van het oorspronkelijke deck, zelfs het uitvoeren van de code die de startingDeck. Dat zorgt voor een grote hoeveelheid extra werk.

In de praktijk worden sommige algoritmen goed uitgevoerd met behulp van gretige evaluatie en andere goed met behulp van luie evaluatie. Voor dagelijks gebruik is luie evaluatie meestal een betere keuze wanneer de gegevensbron een afzonderlijk proces is, zoals een database-engine. Voor databases kan luie evaluatie complexere query's slechts één retour naar het databaseproces uitvoeren en teruggaan naar de rest van uw code. LINQ is flexibel, ongeacht of u ervoor kiest om luie of gretige evaluatie te gebruiken, dus meet uw processen en kies welke soort evaluatie u de beste prestaties geeft.

Conclusie

In dit project hebt u het volgende behandeld:

  • LINQ-query's gebruiken om gegevens samen te voegen in een zinvolle reeks
  • extensiemethoden schrijven om onze eigen aangepaste functionaliteit toe te voegen aan LINQ-query's
  • gebieden in onze code zoeken waar onze LINQ-query's prestatieproblemen kunnen ondervinden, zoals verminderde snelheid
  • luie en gretige evaluatie met betrekking tot LINQ-query's en de gevolgen die ze kunnen hebben voor queryprestaties

Afgezien van LINQ hebt u wat geleerd over technieken die magicians gebruiken voor kaart trucs. Goochelaars gebruiken de Faro shuffle omdat ze kunnen bepalen waar elke kaart in het dek beweegt. Nu je het weet, verwen het niet voor iedereen!

Zie voor meer informatie over LINQ: