Condividi tramite


Lavorare con query Language-Integrated (LINQ)

Introduzione

Questa esercitazione illustra le funzionalità in .NET e nel linguaggio C#. Scopri come:

  • Generare sequenze con LINQ.
  • Scrivere metodi che è possibile usare facilmente nelle query LINQ.
  • Distinguere tra valutazione anticipata e valutazione ritardata.

Queste tecniche vengono apprese creando un'applicazione che dimostra una delle competenze di base di qualsiasi mago: il faro shuffle. Un faro shuffle è una tecnica in cui si divide un mazzo di carte esattamente a metà, e poi la mescolata intercala ogni carta da ciascuna metà per ricostruire il mazzo originale.

I magici usano questa tecnica perché ogni scheda si trova in una posizione nota dopo ogni shuffle e l'ordine è un modello ripetuto.

Questo tutorial offre un approccio leggero alla manipolazione di sequenze di dati. L'applicazione costruisce un mazzo di carte, esegue una sequenza di shuffles e scrive ogni volta la sequenza. Confronta anche l'ordine aggiornato con l'ordine originale.

Questa esercitazione include più passaggi. Dopo ogni passaggio, è possibile eseguire l'applicazione e visualizzare lo stato di avanzamento. È anche possibile visualizzare l'esempio completato nel repository GitHub dotnet/samples. Per istruzioni sul download, vedere esempi ed esercitazioni .

Prerequisiti

  • La versione più recente .NET SDK
  • editor di Visual Studio Code
  • Il DevKit C#

Creare l'applicazione

Creare una nuova applicazione. Aprire un prompt dei comandi e creare una nuova directory per l'applicazione. Rendi questa la directory corrente. Digitare il comando dotnet new console -o LinqFaroShuffle al prompt dei comandi. Questo comando crea i file di avvio per un'applicazione "Hello World" di base.

Se non si è mai usato C# in precedenza, questa esercitazione illustra la struttura di un programma C#. È possibile leggere questo e quindi tornare qui per altre informazioni su LINQ.

Creare il set di dati

Suggerimento

Per questa esercitazione è possibile organizzare il codice in uno spazio dei nomi denominato LinqFaroShuffle in modo che corrisponda al codice di esempio oppure è possibile usare lo spazio dei nomi globale predefinito. Se si sceglie di usare uno spazio dei nomi, assicurarsi che tutte le classi e i metodi siano coerenti nello stesso spazio dei nomi o aggiungere istruzioni appropriate using in base alle esigenze.

Considerare ciò che costituisce un mazzo di carte. Un mazzo di carte da gioco ha quattro abiti e ogni vestito ha 13 valori. In genere, è consigliabile creare subito una Card classe e popolare una raccolta di Card oggetti a mano. Con LINQ, è possibile essere più concisi del solito modo di creare un mazzo di carte. Invece di creare una Card classe, creare due sequenze per rappresentare abiti e ranghi. Creare una coppia di metodi iteratori che generano i ranghi e si adattano come IEnumerable<T>stringhe:

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

Posizionare questi metodi sotto l'istruzione Console.WriteLine nel Program.cs file. Questi due metodi usano entrambi la yield return sintassi per produrre una sequenza durante l'esecuzione. Il compilatore compila un oggetto che implementa IEnumerable<T> e genera la sequenza di stringhe quando vengono richieste.

Ora, usa questi metodi di iterazione per creare il mazzo di carte. Posizionare la query LINQ nella parte superiore del Program.cs file. Ecco come appare:

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

Le molteplici from clausole producono un SelectManyoggetto, che combina ogni elemento della prima sequenza con ognuno della seconda, creando una singola sequenza. L'ordine è importante per questo esempio. Il primo elemento della prima sequenza di origine (Suits) viene combinato con ogni elemento della seconda sequenza (Classifica). Questo processo produce tutte e 13 le carte del primo abito. Questo processo viene ripetuto con ogni elemento nella prima sequenza (Suits). Il risultato finale è un mazzo di carte ordinate in base alle tute, seguite da valori.

Tenere presente che se si scrive LINQ nella sintassi di query usata nell'esempio precedente o si usa la sintassi del metodo, è sempre possibile passare da una forma di sintassi all'altra. La query precedente scritta nella sintassi della query può essere scritta nella sintassi del metodo come segue:

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

Il compilatore converte le istruzioni LINQ scritte con la sintassi di query nella sintassi di chiamata al metodo equivalente. Di conseguenza, indipendentemente dalla sintassi scelta, le due versioni della query producono lo stesso risultato. Scegliere la sintassi più adatta alla situazione. Ad esempio, se si lavora in un team in cui alcuni membri hanno difficoltà con la sintassi del metodo, provare a preferire l'uso della sintassi di query.

Esegui l'esempio costruito a questo punto. Visualizza tutte le 52 carte nel mazzo. Potrebbe essere utile eseguire questo esempio in un debugger per osservare come vengono eseguiti i Suits() metodi e Ranks() . È possibile notare chiaramente che ogni stringa in ogni sequenza viene generata solo in base alle esigenze.

Finestra della console che mostra l'app che scrive 52 schede.

Modificare l'ordine

Successivamente, concentrati su come si mescolano le carte nel mazzo. Il primo passo in qualsiasi buon shuffle è dividere il mazzo in due. I Take metodi e Skip che fanno parte delle API LINQ forniscono tale funzionalità. Posizionarli seguendo il foreach ciclo:

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

Tuttavia, non esiste un metodo di mescolare nella libreria standard, quindi è necessario scrivere il proprio. Il metodo shuffle creato illustra diverse tecniche usate con programmi basati su LINQ, quindi ogni parte di questo processo viene illustrata nei passaggi.

Per aggiungere funzionalità alla modalità di interazione con i IEnumerable<T> risultati delle query LINQ, si scrivono alcuni tipi speciali di metodi denominati metodi di estensione. Un metodo di estensione è un metodo statico per scopi speciali che aggiunge nuove funzionalità a un tipo già esistente senza dover modificare il tipo originale a cui si desidera aggiungere funzionalità.

Assegnare ai metodi di estensione una nuova home page aggiungendo un nuovo file di classe statico al programma denominato Extensions.cse quindi iniziare a compilare il primo metodo di estensione:

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

Annotazioni

Se si usa un editor diverso da Visual Studio (ad esempio Visual Studio Code), potrebbe essere necessario aggiungere using LinqFaroShuffle; all'inizio del file Program.cs per rendere accessibili i metodi di estensione. Visual Studio aggiunge automaticamente questa istruzione using, ma altri editor potrebbero non farlo.

Il extension contenitore specifica il tipo da estendere. Il extension nodo dichiara il tipo e il nome del parametro ricevitore per tutti i membri all'interno del extension contenitore. In questo esempio si estende IEnumerable<T>e il parametro è denominato sequence.

Le dichiarazioni dei membri di estensione vengono visualizzate come se fossero membri del tipo di ricevitore:

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

Chiamare il metodo come se fosse un metodo membro del tipo esteso. Questa dichiarazione di metodo segue anche un linguaggio standard in cui i tipi di input e output sono IEnumerable<T>. Questa pratica consente di concatenare i metodi LINQ per eseguire query più complesse.

Poiché si divide il mazzo in metà, è necessario unire quelle metà insieme. Nel codice, ciò significa che si enumerano entrambe le sequenze acquisite con Take e Skip contemporaneamente, intercalando gli elementi e creando una sequenza: il tuo mazzo di carte ora mescolato. La scrittura di un metodo LINQ che funziona con due sequenze richiede la comprensione del funzionamento IEnumerable<T> .

L'interfaccia IEnumerable<T> ha un metodo: GetEnumerator. L'oggetto restituito da GetEnumerator dispone di un metodo per passare all'elemento successivo e a una proprietà che recupera l'elemento corrente nella sequenza. Questi due membri vengono usati per enumerare la raccolta e restituire gli elementi. Questo metodo Interleave è un metodo iteratore, quindi anziché compilare una raccolta e restituire la raccolta, si usa la yield return sintassi illustrata nel codice precedente.

Ecco l'implementazione di questo metodo:

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

Dopo aver scritto questo metodo, tornare al metodo Main e mescolare il mazzo una volta.

var shuffledDeck = top.InterleaveSequenceWith(bottom);

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

Confronti

Determinare il numero di shuffle necessari per ripristinare l'ordine originale del mazzo. Per scoprire, scrivere un metodo che determina se due sequenze sono uguali. Dopo aver ottenuto questo metodo, posiziona il codice che mescola il mazzo in un ciclo e verifica quando il mazzo è tornato in ordine.

Scrivere un metodo per determinare se le due sequenze sono uguali devono essere semplici. Si tratta di una struttura simile al metodo che hai scritto per mescolare il mazzo. Tuttavia, questa volta, anziché usare yield return per ogni elemento, si confrontano gli elementi corrispondenti di ogni sequenza. Quando viene enumerata l'intera sequenza, se ogni elemento corrisponde, le sequenze sono le stesse:

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

Questo metodo mostra un secondo linguaggio LINQ: metodi terminal. Accettano una sequenza come input (o in questo caso due sequenze) e restituiscono un singolo valore scalare. Quando si usano metodi terminal, questi sono sempre il metodo finale in una catena di metodi per una query LINQ.

Puoi vederlo in azione quando lo usi per determinare quando il mazzo è tornato nell'ordine originale. Inserire il codice di mescolamento all'interno di un ciclo e arrestarlo quando la sequenza torna nell'ordine originale applicando il metodo SequenceEquals(). È possibile vedere che sarebbe sempre il metodo finale in qualsiasi query perché restituisce un singolo valore anziché una sequenza:

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

Esegui il codice che hai creato finora e osserva come il mazzo si riorganizza ogni volta che viene mischiato. Dopo 8 mescolamenti (iterazioni del ciclo do-while), il mazzo torna alla configurazione originale che aveva quando è stato creato per la prima volta dalla query LINQ iniziale.

Ottimizzazioni

L'esempio creato finora esegue un out shuffle, nel quale le carte superiore e inferiore rimangono invariate a ogni esecuzione. Apportiamo una modifica: usiamo invece un mescolamento interno, in cui tutte le 52 carte cambiano posizione. Per un in shuffle, si interleave il mazzo in modo che la prima carta nella metà inferiore diventi la prima carta nel mazzo. Ciò significa che l'ultima carta nella metà superiore diventa quella in fondo. Questa modifica richiede una riga di codice. Aggiorna la query casuale corrente invertendone le posizioni di Take e Skip. Questa modifica cambia l'ordine delle metà superiore e inferiore del mazzo:

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

Esegui di nuovo il programma e noterai che sono necessarie 52 iterazioni per riordinare il mazzo. Si noterà anche una grave riduzione delle prestazioni man mano che il programma continua a essere eseguito.

Ci sono diversi motivi per questo calo delle prestazioni. È possibile affrontare una delle principali cause: uso inefficiente della valutazione differita.

La valutazione differita indica che la valutazione di un'istruzione non viene eseguita fino a quando non è necessario il relativo valore. Le query LINQ sono istruzioni eseguite pigramente. Le sequenze vengono generate solo quando vengono richiesti gli elementi. In genere, questo è un grande vantaggio di LINQ. Tuttavia, in un programma come questo, la valutazione differita causa una crescita esponenziale nel tempo di esecuzione.

Ricorda che hai generato il mazzo originale usando una query LINQ. Ogni mescolamento viene generato eseguendo tre query LINQ sul mazzo precedente. Tutte queste query vengono eseguite pigramente. Ciò significa anche che vengono eseguiti di nuovo ogni volta che viene richiesta la sequenza. Quando si arriva alla 52a iterazione, si rigenera il mazzo originale molte volte. Scrivere un log per illustrare questo comportamento. Dopo aver raccolto i dati, è possibile migliorare le prestazioni.

Nel file Extensions.cs, digita o copia il metodo nel seguente esempio di codice. Questo metodo di estensione crea un nuovo file denominato debug.log all'interno della directory del progetto e registra la query attualmente in esecuzione nel file di log. Aggiungere questo metodo di estensione a qualsiasi query per indicare che la query è stata eseguita.

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

Instrumentare quindi la definizione di ogni query con un messaggio di log:

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

Si tenga presente che non si registra tutte le volte che si accede a una query. Si effettua il log solo quando si crea la query originale. L'esecuzione del programma richiede ancora molto tempo, ma ora è possibile vedere perché. Se esaurisci la pazienza durante l'esecuzione della sequenza casuale in con la registrazione attivata, torna alla sequenza casuale out. Si vedono ancora gli effetti della valutazione pigra. In un'unica esecuzione vengono eseguite 2,592 query, inclusa la generazione di valori e semi.

È possibile migliorare le prestazioni del codice per ridurre il numero di esecuzioni eseguite. Una semplice correzione consiste nel memorizzare nella cache i risultati della query LINQ originale che costruisce il mazzo di carte. Al momento, le query vengono eseguite ripetutamente ogni volta che il ciclo do-while effettua un'iterazione, ricostruendo il mazzo di carte e mescolandolo di nuovo ogni volta. Per memorizzare nella cache il mazzo di carte, applicare i metodi ToArray LINQ e ToList. Quando le aggiungi alle query, eseguono le stesse azioni che gli sono state indicate, ma ora archiviano i risultati in una matrice o in un elenco, a seconda del metodo scelto. Aggiungere il metodo ToArray LINQ a entrambe le query ed eseguire di nuovo il programma:

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

Ora il mescolamento "out" è ridotto a 30 query. Esegui di nuovo con lo shuffle e vengono visualizzati miglioramenti simili: ora esegue 162 query.

Questo esempio è progettato per evidenziare i casi d'uso in cui la valutazione differita può causare problematiche di prestazioni. Anche se è importante vedere dove la valutazione differita può influire sulle prestazioni del codice, è altrettanto importante comprendere che non tutte le query devono essere eseguite con entusiasmo. Il colpo di prestazioni che si verifica senza usare ToArray è perché ogni nuova disposizione del mazzo di carte è costruito dalla disposizione precedente. L'uso della valutazione differita significa che ogni nuova configurazione del mazzo viene costruita dal mazzo originale, anche eseguendo il codice che ha costruito startingDeck. Ciò causa una grande quantità di lavoro aggiuntivo.

In pratica, alcuni algoritmi funzionano bene usando la valutazione anticipata e altri funzionano bene usando la valutazione differita. Per l'uso quotidiano, la valutazione pigra è di solito una scelta migliore quando la fonte dei dati è un processo separato, come un motore di database. Per i database, la valutazione differita consente a query più complesse di effettuare un solo viaggio di andata e ritorno al processo del database, tornando per completare il resto del codice. LINQ è flessibile sia che si scelga di usare la valutazione lazy o eager, quindi misura i tuoi processi e scegli quella valutazione che offre le migliori prestazioni.

Conclusione

In questo progetto sono stati trattati gli argomenti seguenti:

  • Uso di query LINQ per aggregare i dati in una sequenza significativa.
  • Scrittura di metodi di estensione per aggiungere funzionalità personalizzate alle query LINQ.
  • Individuazione di aree nel codice in cui le query LINQ potrebbero presentare problemi di prestazioni come una velocità ridotta.
  • Valutazione lazy e eager nelle query LINQ e le implicazioni che potrebbero avere sulle prestazioni delle query.

Oltre a LINQ, hai imparato una tecnica che i maghi utilizzano per i trucchi con le carte. I magici usano lo shuffle faro perché possono controllare dove ogni carta si muove nel mazzo. Ora che sai, non rovinarlo per tutti gli altri!

Per altre informazioni su LINQ, vedere: