Delen via


Werken met Language-Integrated Query (LINQ)

Introductie

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

  • Reeksen genereren met LINQ.
  • Schrijfmethoden die u eenvoudig kunt gebruiken 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 goochelaar demonstreert: de faro shuffle. Een faro shuffle is een techniek waarbij u een stapel kaarten exact in tweeën splitst en vervolgens elke kaart van elke helft in elkaar laat overvloeien om de oorspronkelijke stapel opnieuw te bouwen.

Goochelaars gebruiken deze techniek omdat elke kaart zich, na elke schudbeurt, op een bekende locatie bevindt en de volgorde een herhalend patroon volgt.

Deze zelfstudie biedt een speelse kijk op het bewerken van reeksen gegevens. De toepassing maakt een kaartspel, voert een reeks schuddingen uit en schrijft de reeks telkens uit. Ook wordt de bijgewerkte volgorde vergeleken met de oorspronkelijke volgorde.

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 zelfstudiesvoor downloadinstructies.

Vereiste voorwaarden

De toepassing maken

Maak een nieuwe toepassing. Open een opdrachtprompt en maak een nieuwe map voor uw toepassing. Maak het de huidige directory. Typ de opdracht dotnet new console -o LinqFaroShuffle bij de opdrachtprompt. Met deze opdracht worden de startersbestanden gemaakt 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

Aanbeveling

Voor deze zelfstudie kunt u uw code ordenen in een naamruimte die wordt aangeroepen LinqFaroShuffle om overeen te komen met de voorbeeldcode, of u kunt de standaard algemene naamruimte gebruiken. Als u ervoor kiest om een naamruimte te gebruiken, moet u ervoor zorgen dat al uw klassen en methoden consistent binnen dezelfde naamruimte zijn, of voeg indien nodig de juiste using instructies toe.

Bedenk wat een stapel kaarten is. Een speelkaart heeft vier pakken en elk pak heeft 13 waarden. Normaal gesproken kunt u overwegen om direct een Card klasse te maken 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, maakt u twee reeksen om pakken en rangschikkingen weer te geven. Maak een paar iteratormethoden die de rangen en soorten genereren als IEnumerable<T>s van strings:

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 methoden onder de Console.WriteLine instructie in uw Program.cs bestand. Deze twee methoden gebruiken beide de yield return syntaxis om een reeks te produceren terwijl ze worden uitgevoerd. De compiler bouwt een object dat IEnumerable<T> implementeert en genereert de tekenreeksvolgorde wanneer deze wordt opgevraagd.

Gebruik nu deze iterator-methoden om het deck met kaarten te maken. Plaats de LINQ-query boven aan het Program.cs bestand. Dit ziet er als volgt uit:

var startingDeck = from s in Suits()
                   from r in Ranks()
                   select (Suit: s, Rank: r);

// Display each card that's generated and placed in startingDeck
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 dit voorbeeld. Het eerste element in de eerste bronreeks (Suits) wordt gecombineerd met elk element in de tweede reeks (Ranks). Dit proces produceert alle 13 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 soort en vervolgens op waarde.

Houd er rekening mee dat, ongeacht of u uw LINQ schrijft in de querysyntaxis die in het voorgaande voorbeeld wordt gebruikt of dat u in plaats daarvan de syntaxis van de methode gebruikt, altijd van de ene vorm van syntaxis naar de andere kunt gaan. De voorgaande 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 => (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 de syntaxis die het beste werkt voor uw situatie. Als u bijvoorbeeld in een team werkt waarbij sommige leden moeite hebben met de syntaxis van de methode, kunt u beter querysyntaxis gebruiken.

Voer het voorbeeld uit dat u op dit moment hebt gemaakt. Het toont alle 52 kaarten in het dek. Het kan handig zijn 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 volgorde alleen wordt gegenereerd wanneer dat nodig is.

Een consolevenster waarin de app 52 kaarten aan het schrijven is.

De volgorde bewerken

Richt u vervolgens op hoe u de kaarten in het dek verschuift. 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. Plaats ze volgens de foreach lus:

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

U kunt echter geen gebruik maken van een shuffle-methode in de standaardbibliotheek, dus moet u uw eigen methode schrijven. De methode shuffle die u maakt illustreert verschillende technieken die u gebruikt met LINQ-programma's, dus elk deel van dit proces wordt uitgelegd in stappen.

Als u functionaliteit wilt toevoegen aan de interactie met de IEnumerable<T> resultaten van LINQ-query's, schrijft u een aantal speciale soorten methoden die extensiemethoden worden genoemd. Een extensiemethode is een statische methode voor speciale doeleinden 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:

public static class CardExtensions
{
    extension<T>(IEnumerable<T> sequence)
    {
        public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
        {
            // Your implementation goes here
            return default;
        }
    }
}

Opmerking

Als u een andere editor gebruikt dan Visual Studio (zoals Visual Studio Code), moet u mogelijk boven aan het using LinqFaroShuffle; toevoegen om de extensiemethoden toegankelijk te maken. Visual Studio voegt deze instructie automatisch toe, maar andere editors doen dat mogelijk niet.

De extension container specificeert het type dat wordt uitgebreid. Het extension knooppunt declareert het type en de naam van de ontvangerparameter voor alle leden in de extension container. In dit voorbeeld gaat u uitbreiden IEnumerable<T>en krijgt de parameter de naam sequence.

Uitbreidingsliddeclaraties worden weergegeven alsof ze lid zijn van het type ontvanger:

public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)

U roept de methode aan alsof het een lidmethode van het uitgebreide type is. 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.

Omdat je de stapel in tweeën hebt gesplitst, 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, waarbij u de elementen interleaseert en één reeks maakt: uw nu georderdelde 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 syntaxis die in de voorgaande code wordt weergegeven.

Dit is de implementatie van die methode:

public IEnumerable<T> InterleaveSequenceWith(IEnumerable<T> second)
{
    var firstIter = sequence.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 schudt de stapel eenmaal.

var shuffledDeck = top.InterleaveSequenceWith(bottom);

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

Vergelijkingen

Bepaal hoeveel keer schudden nodig is om het kaartendeck terug te brengen naar de oorspronkelijke volgorde. Om erachter te komen, schrijft u een methode die bepaalt of twee reeksen gelijk zijn. Nadat u die methode hebt, plaatst u de code die het kaartspel schudt in een lus en controleert u wanneer het kaartspel 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. Deze keer vergelijkt u echter de overeenkomende elementen van elke reeks in plaats van voor elk element te gebruiken yield return . Wanneer de hele reeks wordt geïnventariseerd, als elk element overeenkomt, zijn de reeksen hetzelfde:

public bool SequenceEquals(IEnumerable<T> second)
{
    var firstIter = sequence.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;
}

Deze methode toont 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.

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

var startingDeck = from s in Suits()
                   from r in Ranks()
                   select (Suit: s, Rank: r);

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

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

var shuffledDeck = top.InterleaveSequenceWith(bottom);

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

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

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

Console.WriteLine(times);

Voer de code uit die je tot nu toe hebt gemaakt en zie hoe het deck opnieuw wordt gerangschikt bij elke schudbeurt. Na 8 keer schudden (iteraties van de do-while-lus) keert de stapel terug naar de oorspronkelijke configuratie waarin deze was toen u deze voor het eerst maakte vanuit de oorspronkelijke 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 één wijziging aanbrengen: gebruik in plaats daarvan een in-shuffle, waarbij alle 52 kaarten worden geschud en van positie veranderen. 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. Voor deze wijziging is één regel code vereist. Werk de huidige shuffle-query bij door de posities van Take en Skip om te wisselen. Deze wijziging verandert de volgorde van de bovenste en onderste helften van het dek:

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

Voer het programma opnieuw uit en je ziet dat het 52 iteraties duurt voordat het dek zich opnieuw ordent. U ziet ook een ernstige prestatievermindering omdat het programma blijft draaien.

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

Luie evaluatie geeft aan dat de evaluatie van een expressie pas wordt uitgevoerd als de waarde 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 programma zoals dit programma veroorzaakt luie evaluatie echter exponentieel groei in uitvoeringstijd.

Houd er rekening mee dat u de oorspronkelijke deck hebt 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 query's worden lazily uitgevoerd. Dat betekent ook dat ze opnieuw worden uitgevoerd telkens wanneer de volgorde wordt aangevraagd. Tegen de tijd dat u bij de 52e iteratie bent, regenereert u de oorspronkelijke kaartenset veelvuldig. Schrijf een logboek om dit gedrag te demonstreren. Zodra u gegevens hebt verzameld, kunt u de prestaties verbeteren.

Vul in of kopieer de methode uit het volgende codevoorbeeld 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. Voeg deze extensiemethode toe aan een query om te markeren dat de query is uitgevoerd.

public IEnumerable<T> LogQuery(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;
}

Instrumenteer vervolgens de definitie van elke query met een logboekbericht:

var startingDeck = (from s in Suits().LogQuery("Suit Generation")
                    from r in Ranks().LogQuery("Rank Generation")
                    select (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 merkt op dat u niet elke keer logt 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 geen geduld meer hebt om de in shuffle uit te voeren terwijl logboekregistratie is ingeschakeld, schakelt u terug naar de out shuffle. U ziet nog steeds de effecten van luie evaluatie. In één uitvoering worden 2592 query's uitgevoerd, inclusief het genereren van waarden en kleuren.

U kunt de prestaties van de code verbeteren om het aantal uitvoeringen te verminderen dat u maakt. Een eenvoudige oplossing 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 herhaaldelijk uit telkens wanneer de do-while-lus een iteratie voltooit, waarbij de stapel kaarten opnieuw wordt opgebouwd en elke keer opnieuw geschud. Als u de stapel kaarten in de cache wilt opslaan, past u de LINQ-methoden ToArray toe en ToList. Wanneer u ze toevoegt aan de query's, voeren ze dezelfde acties uit die u hen 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:

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 teruggebracht tot 30 queries. Voer het opnieuw uit met de shuffle-optie en zie vergelijkbare verbeteringen: er worden nu 162 query's uitgevoerd.

Dit voorbeeld is ontworpen om de gebruiksvoorbeelden te markeren 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 houdt in dat elke nieuwe deckconfiguratie uit de oorspronkelijke stapel wordt opgebouwd, zelfs de code wordt uitgevoerd die de startingDeck bouwde. 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 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 aangepaste functionaliteit toe te voegen aan LINQ-query's.
  • Zoeken naar gebieden in code waar LINQ-query's prestatieproblemen kunnen ondervinden, zoals verminderde snelheid.
  • Luie en gretige evaluatie in LINQ-query's en de gevolgen die ze kunnen hebben voor queryprestaties.

Afgezien van LINQ, hebt u geleerd over een techniek 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: