[C#] Tutorial „Lucrul cu LINQ”

Tradus din această pagină oficială de documentație Microsoft.

Introducere

Acest tutorial vă învață câteva facilități din .NET Core și limbajul C#. Dvs. veți învăța:
  • Cum să generați secvențe cu LINQ
  • Cum să scrieți metode care pot fi ușor folosite în interogări LINQ.
  • Cum să deosebiți între evaluarea imediată și evaluarea leneșă.
Veți învăța aceste tehnici construind o aplicație care demonstrează una dintre abilitățile de bază ale oricărui magician: faro shuffle. Pe scurt, un faro shuffle este o tehnică în care dvs. împărțiți un pachet de cărți exact în jumătate, apoi amestecul îmbină fiecare singură carte din fiecare jumătate să reconstruiască pachetul inițial.

Magicienii folosesc această tehnică deoarece fiecare carte este într-un loc cunoscut după fiecare amestec, și ordinea se repetă.

Pentru scopurile dvs., ea este o privire superficială la manipularea secvențelor de date. Aplicația pe care o veți construi va construi un pachet de cărți, și apoi va face o serie de amestecuri, afișând secvența de fiecare dată. Dvs. de asemenea veți compara ordinea actualizată cu ordinea originală.

Acest tutorial are pași multipli. După fiecare pas, dvs. puteți rula aplicația și vedea progresul. Dvs. puteți de asemenea vedea exemplul completat în depozitul GitHub dotnet/samples. Pentru instrucțiuni de descărcare, vedeți Samples and Tutorials.

Cerințe preliminare

Dvs. va trebui să vă configurați mașina să ruleze .NET core. Puteți găsi instrucțiunile de instalare pe pagina .NET Core. Puteți rula această aplicație pe Windows, Ubuntu Linux, OS X sau într-un container Docker. Dvs. va trebui să instalați editorul de cod favorit al dvs. Descrierile de mai jos folossc Visual Studio Code care este un editor cu sursă deschisă, cross-platform. Totuși, dvs. puteți folosi oricare unelte cu care sunteți confortabili.

Creați aplicația

Primul pas este să creați o nouă aplicație. Deschideți o linie de comandă și creați un director nou pentru aplicația dvs. Faceți-l directorul curent. Tastați comanda dotnet new console la linia de comandă. Aceasta creează fișierele de începere pentru o aplicație de bază „Hello World”.

Dacă nu ați folosit niciodată C# înainte, acest tutorial explică structura unui program C#. Dvs. puteți învăța acela și apoi să vă întoarceți aici să învățați mai multe despre LINQ.

Crearea setului de date

Inainte de a începe, asigurați-vă că următoarele linii sunt la începutul fișierului Program.cs generat de dotnet new console:

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


Dacă aceste trei linii (instrucțiuni using) nu sunt la începutul fișierului, programul nostru nu va compila.

Acum că aveți toate referințele de care aveți nevoie, considerați ce constituie un pachet de cărți. De obicei, un pachet de cărți de joc au patru seturi, și fiecare set are treisprezece valori. In mod normal, dvs. ați considera crearea unei clase Card (Carte) imediat și popularea unei colecții de obiecte Card manual. Cu LINQ, dvs. puteți fi mai concis decât modul obișnuit de a crea un pachet de cărți. In schimbul creării unei clase Card, dvs. puteți crea două secvențe care să reprezinte seturile și respectiv rangurile. Dvs. veți crea o pereche foarte simplă de metode iterator care vor genera rangurile și seturile ca IEnumerable<T>-uri de șiruri:

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


Plasați acestea în interiorul metodei Main în fișierul dvs. Program.cs. Ambele aceste metode utilizează sintaxa yield return pentru a produce secvențe pe măsură ce rulează. Compilatorul construiește un obiect care implementează IEnumerable<T> și generează secvențele de șiruri pe măsură ce sunt cerute.

Acum, utilizați aceste metode iterator pentru a crea pachetul de cărți de joc. Veți plasa interogarea LINQ în metoda noastră Main. Iată o privire la ea:

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


Clauzele multiple from produc un SelectMany, care creează o singură secvență din combinarea fiecărui element din prima secvență cu fiecare element din a doua secvență. Ordinea este importantă pentru scopurile noastre. Primul element din prima secvență sursă  (Suits) este combinat cu fiecare lement din secvența a doua (Ranks). Aceasta produce toate cele treisprezece cărți din primul set. Acest proces este repetat cu fiecare element din prima secvență (Suits). Rezultatul final este un pachet de cărți de joc ordonate după seturi, apoi după valori.

Este important să ținem minte că fie că dvs. alegeți să scrieți LINQ-ul dvs. în sintaxa de interogare folosită mai sus sau să folosiți sintaxa metodelor în schimb, este întotdeauna posibil să treceți de la o formă de sintaxă la alta. Interogarea de mai sus scrisă în sintaxă de interogare poate fi scrisă în sintaxa metodelor ca:

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

Compilatorul traduce instrucțiunile LINQ scrise cu sintaxă de interogare în sintaxa de apeluri de metode echivalentă. Ca urmare, indiferent de alegerea dvs. de sintaxă, cele două versiuni ale interogării produc același rezultat. Alegeți care sintaxă merge mai bine pentru situația dvs.: de exemplu, dacă dvs. lucrați într-o echipă în care unii din membri au dificultate cu sintaxa metodelor, încercați să preferați folosirea sintaxei de interogare.

Dați-i drumul și rulați exemplul pe care l-ați construit până la acest punct. El va afișa toate cele 52 de cărți din pachet. S-ar putea să găsiți foarte ajutător să rulați acest exemplu sub un depanator să observați cum metodele Suits() și Ranks() se execută. Dvs. puteți să vedeți clar că fiecare șir din fiecare secvență este generat doar când este necesar.


Manipularea ordinii

In continuare, concentrați-vă pe cum veți amesteca aceste cărți din pachet. Primul pas în oricare amestecare bună este să împărțiți pachetul în două. Metodele Take și Skip care sunt parte din API-urile LINQ furnizează această facilitate pentru dvs. Plasați-le sub ciclul 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);
}

Cu toate acestea, nu există nici o metodă de amestecare de care să profităm în biblioteca standard, deci dvs. va trebui să vă scrieți propria metodă. Metoda de amestecare pe care o veți fi creând ilustrează câteva tehnici pe care le veți folosi cu programele bazate pe LINQ, deci fiecare parte a acestui proces va fi explicat în pași.

Pentru a adăuga ceva funcționalitate la cum dvs. interacționați cu IEnumerable<T> veți face un pas înapoi de la interogări LINQ, dvs. va trebui să scrieți un fel special de metode numite metode extensii. Pe scurt, o metodă extensie este o metodă statică cu scop special care adaugă funcționalitate nouă la un tip deja existent fără a trebui să modificați tipul original  la care doriți să adăugați funcționalitate.

Dați metodelor extensii ale dvs. o casă nouă adăugând un fișier cu o nouă clasă statică programului dvs. numit Extensions.cs, și apoi începeți să construiți prima metodă extensie:

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

Priviți pentru un moment semnătura metodei, mai exact parametrii:

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

Dvs. puteți vedea adăugarea modificatorului this pe primul argument către metodă. Aceasta înseamnă că dvs. apelați metoda ca și cum ar fi o metodă membru a tipului primului argument. Această declarație de metodă urmează de asemenea un idiom standard în care tipurile de intrare și ieșire sunt IEnumerable<T>. Această practică dă voie metodelor LINQ să fie înlănțuite împreună pentru a realiza interogări mai complexe.

In mod natural, fiindcă dvs. ați împărțit pachetul în două jumătăți, va trebui să uniți aceste două jumătăți împreună. In cod, aceasta înseamnă că dvs. veți fi enumerând ambele secvențe pe care le-ați obținut prin Take și Skip deodată, intercalând elementele (en. interleaving), și creând o singură secvență: pachetul dvs. acum amestecat de cărți. Scrierea uni metode LINQ care lucrează cu două secvențe necesită să înțelegeți cum funcționează IEnumerable<T>.

Interfața IEnumerable<T> are o singură metodă: GetEnumerator. Obiectul întors de GetEnumerator are o metodă să mute la următorul element, și o proprietate care întoarce elementul curent al secvenței. Dvs. veți folosi acești doi membri pentru a enumera colecția și a întoarce elementele. Această metodă de intercalare va fi o metodă iterator, deci în schimbul construirii unei colecții și întoarcerii colecției, dvs. veți folosi sintaxa yield return arătată mai sus.

Aici este implementarea acelei metode:

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


Acum că ați scris această metodă, mergeți înapoi la metoda Main și amestecați pachetul o dată:

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

Comparări

Câte amestecuri sunt necesare pentru a pune pachetul în ordinea lui originală? Pentru a afla, dvs. va trebui să scrieți o metodă care determină dacă două secvențe sunt egale. După ce aveți această metodă, dvs. va trebui să plasați codul care amestecă pachetul într-un ciclu, și să verificați când pachetul este înapoi în ordine.

Scrierea unei metode să determinați dacă cele două secvențe sunt egale ar trebui să fie simplu. Este o structură similară cu metoda pe care ați scris-o să amestece pachetul. Dar de această dată, în schimbul yield return-ării fiecărui element, dvs. veți compara elementele potrivite din fiecare secvență. Când întreaga secvență a fost enumerată, dacă fiecare element se potrivește, secvențele sunt aceleași:

Extensions.cs

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


    while (firstIter.MoveNext() && secondIter.MoveNext())
    {
        if (!firstIter.Current.Equals(secondIter.Current))
        {
            return false;
        }
    }


    return true;
}


Aceasta arată un al doilea idiom LINQ: metodele terminale. Ele primesc o secvență ca intrare (sau în acest caz, două secvențe), și întorc o singură valoare scalară. Când folosiți metode terminale, ele sunt întotdeauna metoda finală într-un lanț de metode pentru o interogare LINQ, de aici numele „terminal”.

Dvs. puteți vedea acesta în acțiune când dvs. îl folosiți să aflați dacă pachetul este înapoi în ordinea lui originală. Puneți codul de amestecare într-un ciclu, și opriți când secvența este înapoi în ordinea lui originală aplicând metoda SequenceEquals(). Dvs. puteți vedea că ea ar fi întotdeauna metoda finală în oricare interogare, deoarece întoarce o singură valoare în schimbul unei secvențe:

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


Rulați codul pe care îl aveți până acum și observați cum pachetul se rearanjează la fiecare amestec. După 8 amestecuri (iterații ale ciclului do-while), pachetul se întoarce la configurația originală în care era când l-ați creat prima dată din interogarea LINQ de începere.

Optimizări

Exemplul pe care l-ați construit până acum realizează un out shuffle, în care cărțile de sus și de jos rămân aceleași la fiecare rulare. Haideți să facem o schimbare: vom folosi un in shuffle în schimb, în care toate cele 52 de cărți își schimbă poziția. Pentru un in shuffle, dvs. intercalați pachetul astfel încât prima carte a jumătății de jos devine prima carte din pachet. Aceasta înseamnă că ultima carte din prima jumătate devine cartea de jos. Aceasta este o simplă schimbare la o singură linie de cod. Actualizați interogarea curentă de amestecare inversând pozițiile lui Take și Skip. Aceasta va schimba ordinea jumătății de sus și a jumătății de jos ale pachetului:

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

Rulați programul din nou, și veți vedea că sunt necesare 52 de iterații pentru ca pachetul să se reordoneze. Dvs. de asemenea veți începe să observați câteva degradări serioase ale performanței pe măsură ce programul continuă să ruleze.

Există câteva motive pentru aceasta. Puteți aborda una dintre cauzele majore ale acestei căderi a performanței: folosirea ineficientă a evaluării leneșe.

Pe scurt, evaluarea leneșă face că evaluarea unei instrucțiuni să nu fie realizată până valoarea ei este necesară. Interogările LINQ sunt instrucțiuni care sunt evaluate leneș. Secvențele sunt generate doar pe măsură ce elementele sunt cerute. In mod obișnuit, acesta este un avantaj major al LINQ. Cu toate acestea, într-o utilizare cum este acest program, aceasta acesta cauzează creștere exponențială în timpul de execuție.

Țineți minte că noi am generat pachetul original folosind o interogare LINQ. Fiecare amestec este generat făcând trei interogări LINQ pe pachetul anterior. Toate acestea sunt realizate leneș. Aceasta înseamnă de asemenea că ele sunt realizate din nou de fiecare dată când secvența este cerută. In momentul în care ajungeți la a 52-a iterație, dvs. veți fi regenerând pachetul original de multe, multe ori. Haideți să scriem un jurnal (en. log) să demonstrăm acest comportament. Apoi, dvs. îl veți corecta.

Iată aici o metodă jurnal care poate fi pusă la sfârșitul oricărei interogări să marcheze că interogarea a fost executată.

public static IEnumerable<T> LogQuery<T>
    (this IEnumerable<T> sequence, string tag)
{
    using (var writer = File.AppendText("debug.log"))
    {
        writer.WriteLine($"Executing Query {tag}");
    }

    return sequence;
}

Acum, instrumentați definiția fiecărei interogări cu un mesaj de jurnal:

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

Notați că dvs. nu înregistrați (en. log) de fiecare dată când accesați o interogare. Dvs. înregistrați doar când creați interogarea originală. Programul încă ia mult timp să ruleze, dar acum puteți vedea de ce. Dacă rămâneți fără răbdare rulând in shuffle-ul cu jurnalizarea pornită, treceți înapoi la out shuffle. Dvs. încă veți simți efectele evaluării leneșe. Intr-o rulare, ea execută 2592 de interogări, incluzând toate generările de valoare și set.

Puteți îmbunătăți performanța codului aici pentru a reduce numărul de execuții pe care le faceți. O simplă reparație pe care o puteți face este să cache-uiți rezultatele interogării LINQ originale care construiește pachetul de cărți. In prezent, dvs. executați interogările iar și iar de fiecare dată când ciclul do-while trece printr-o iterație, reconstruind pachetul de cărți și reamestecându-l de fiecare dată. Pentru a cache-ui pachetul de cărți, puteți profita de metodele LINQ ToArray și ToList; când le puneți la sfârșitul interogărilor, ele vor executa aceleași acțiuni pe care le-ați spus să le execute, dar acum ele vor stoca rezultatul într-un tablou (array) sau o listă, depinzând de care metodă alegeți să apelați. Adăugați metoda LINQ ToArray la sfârșitul ambelor interogări și rulați programul din nou:

public static void Main(string[] args)
{
    var startingDeck = (from s in Suits().LogQuery("Suit Generation")
                        from r in Ranks().LogQuery("Value Generation")
                        select new PlayingCard(s, 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);
}


Acum amestecul out shuffle este jos la 30 de interogări. Rulați din nou cu in shuffle-ul și veți vedea îmbunătățiri similare: el execută acum 162 de interogări.

Vă rugăm să notați că acest exemplu este proiectat să sublinieze cazurile de utilizare în care evaluarea leneșă poate cauza dificultăți de performanță. In timp ce este important să vedeți unde evaluarea leneșă poate lovi performanța codului, este egal de important să înțelegeți că nu toate interogările ar trebui să ruleze imediat. Lovitura de performanță pe care o întâmpinați fără să folosiți ToArray este deoarece fiecare nou aranjament al pachetului de cărți este construit din aranjamentul anterior. Folosirea evaluării leneșe înseamnă că fiecare nouă configurație a pachetului este construită din pachetul original, chiar executând codul care a construit startingDeck. Aceasta cauzează o mare cantitate de lucru în plus.

In practică, unii algoritmi rulează bine folosind evaluarea imediată. Pentru utilizarea zilnică, evaluarea leneșă este de obicei o alegere mai bună când sursa de date este un proces separat, ca un motor de bază de date. Pentru bazele de date, evaluarea leneșă permite interogări mai complexe să se execute doar cu un singur drum către procesul bazei de date și înapoi la restul codului dvs. LINQ este flexibil fie că alegeți să folosiți evaluare leneșă fie imediată, deci măsurați-vă procesele și alegeți oricare fel de evaluare vă dă cea mai bună performanță.

Concluzie

In acest proiect, dvs. ați acoperit:
  • folosirea interogărilor LINQ pentru a agrega date într-o secvență semnificativă
  • scrierea de metode extensii pentru a adăuga propria noastră funcționalitate personalizată la interogări LINQ
  • localizarea zonelor din codul nostru în care interogările noastre LINQ ar putea ajunge în probleme de performanță ca viteză degradată
  • evaluarea leneșă și imediată în legătură cu interogările LINQ și implicațiile pe care ele le pot avea asupra performanței interogărilor
Pe lângă LINQ, ați învățat puțin despre o tehnică pe care magicienii o folosesc pentru trucuri cu cărți de joc. Magicienii folosesc amestecul Faro (Faro shuffle) deoarece ei pot controla unde fiecare carte se mișcă în pachet. Acum că știți, nu o stricați pentru toți ceilalți!

Pentru mai multe informații despre LINQ, vedeți:

Tradus din această pagină oficială de documentație Microsoft.

Niciun comentariu:

Trimiteți un comentariu