Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Introduzione
Questa esercitazione illustra le funzionalità di .NET Core e il linguaggio C#. Verrà descritto come:
- Generare sequenze con LINQ.
- Scrivere metodi che possono essere usati facilmente nelle query LINQ.
- Distinguere tra valutazione anticipata e valutazione ritardata.
Queste tecniche verranno apprese creando un'applicazione che illustra una delle abilità fondamentali di ogni mago: il faro shuffle. In breve, un mescolamento Faro è una tecnica in cui si divide un mazzo di carte esattamente a metà, poi il mescolamento intreccia ogni carta da ogni 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.
Ai tuoi fini, è uno sguardo spensierato sulla manipolazione di sequenze di dati. L'applicazione che creerai costruisce un mazzo di carte e quindi esegue una sequenza di shuffles, scrivendo la sequenza ogni volta. Si confronterà 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
Creare l'applicazione
Il primo passaggio consiste nel 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
al prompt dei comandi. In questo modo vengono creati 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
Prima di iniziare, assicurarsi che le righe seguenti si trovino nella parte superiore del Program.cs
file generato da dotnet new console
:
// Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
Se queste tre righe (using
direttive) non sono nella parte superiore del file, il programma potrebbe non essere compilato.
Ora che hai tutti i riferimenti necessari, prendi in considerazione ciò che costituisce un mazzo di carte. In genere, un mazzo di carte da gioco ha quattro abiti, e ogni abito ha tredici valori. In genere, si potrebbe prendere in considerazione la creazione subito di una Card
classe per poi popolare manualmente una raccolta di oggetti Card
. Con LINQ, è possibile essere più concisi del solito modo di gestire la creazione di un mazzo di carte. Invece di creare una Card
classe, è possibile creare due sequenze per rappresentare rispettivamente i completi e i ranghi. Si creerà una coppia davvero semplice di metodi iteratori che genereranno i ranghi e gli abiti come IEnumerable<T>stringhe:
// 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";
}
Posizionare questi sotto il Main
metodo 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. La query LINQ verrà inserita nel nostro metodo Main
. Ecco un'occhiata:
// 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);
}
}
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 i nostri scopi. Il primo elemento della prima sequenza di origine (Suits) viene combinato con ogni elemento della seconda sequenza (Classifica). Questo produce tutte e tredici 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.
È importante tenere presente che se si sceglie di scrivere LINQ nella sintassi di query usata in precedenza o usare la sintassi del metodo, è sempre possibile passare da una forma di sintassi all'altra. La query precedente scritta nella sintassi delle query può essere scritta nella sintassi del metodo nel modo seguente:
var startingDeck = Suits().SelectMany(suit => Ranks().Select(rank => new { 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 dei metodi, provare a preferire l'uso della sintassi di query.
Procedere ed eseguire l'esempio creato a questo punto. Verranno visualizzate tutte le 52 carte nel mazzo. Può risultare molto 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.
Modificare l'ordine
Successivamente, concentrati su come mescolare 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 sotto il foreach
ciclo:
// 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);
}
Tuttavia, non esiste un metodo di mescolamento da sfruttare nella libreria standard, quindi dovrai scrivere il tuo. Il metodo shuffle che verrà creato illustra diverse tecniche che verranno usate con programmi basati su LINQ, quindi ogni parte di questo processo verrà illustrata nei passaggi.
Per aggiungere alcune funzionalità al modo in cui si interagisce con le IEnumerable<T> che otterrai dalle query LINQ, è necessario scrivere alcuni tipi speciali di metodi denominati metodi di estensione. Brevemente, 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 vuole aggiungere funzionalità.
Assegnare ai metodi di estensione una nuova home page aggiungendo un nuovo file di classe statico al programma denominato Extensions.cs
e quindi iniziare a compilare il primo metodo di estensione:
// 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
}
}
}
Esaminare la firma del metodo per un momento, in particolare i parametri:
public static IEnumerable<T> InterleaveSequenceWith<T> (this IEnumerable<T> first, IEnumerable<T> second)
È possibile visualizzare l'aggiunta del modificatore this
al primo argomento del metodo. Ciò significa che il metodo viene chiamato come se fosse un metodo membro del tipo del primo argomento. 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.
Naturalmente, poiché si divide il mazzo in metà, sarà necessario unirle. Nel codice, questo significa che enumererai entrambe le sequenze ottenute attraverso Take e Skip contemporaneamente, interleaving
gli elementi, e creerai una sola 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 ha un metodo per passare all'elemento successivo e una proprietà che recupera l'elemento corrente nella sequenza. Questi due membri verranno usati per enumerare la raccolta e restituire gli elementi. Questo metodo Interleave sarà un metodo iteratore, quindi invece di compilare una raccolta e restituire la raccolta, si userà la yield return
sintassi illustrata in precedenza.
Ecco l'implementazione di questo metodo:
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;
}
}
Dopo aver scritto questo metodo, tornare al metodo Main
e mescolare il mazzo una volta.
// 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);
}
}
Confronti
Quanti shuffle ci vuole per riportare il mazzo all'ordine originale? Per scoprire, è necessario scrivere un metodo che determina se due sequenze sono uguali. Dopo aver ottenuto questo metodo, è necessario inserire il codice che riordina il mazzo in un ciclo e verificare quando il mazzo è di nuovo 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. Solo questa volta, invece di yield return
modificare ogni elemento, verranno confrontati gli elementi corrispondenti di ogni sequenza. Quando l'intera sequenza è stata enumerata, se ogni elemento corrisponde, le sequenze sono le stesse:
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;
}
Viene illustrato 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, sono sempre il metodo finale in una catena di metodi per una query LINQ, quindi il nome "terminale".
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:
// 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);
}
Esegui il codice che hai finora e osserva come il mazzo viene riorganizzato ad ogni mischiata. 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 uno shuffle out, in cui le schede superiore e inferiore rimangono invariate in ogni esecuzione. Facciamo una modifica: useremo invece un oggetto in shuffle , in cui tutte le 52 schede 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. Si tratta di una semplice modifica a una singola riga di codice. Aggiorna la query casuale corrente invertendone le posizioni di Take e Skip. Questo cambierà l'ordine delle metà superiore e inferiore del mazzo:
shuffle = shuffle.Skip(26).InterleaveSequenceWith(shuffle.Take(26));
Esegui di nuovo il programma e si noterà che sono necessarie 52 iterazioni perché il mazzo si riordini. Si inizierà anche a notare alcune gravi riduzioni delle prestazioni man mano che il programma continua a essere eseguito.
Ci sono diversi motivi per questo. Puoi affrontare una delle principali cause di questo calo delle prestazioni: uso inefficiente della valutazione differita.
Brevemente, la valutazione pigra indica che la valutazione di un'espressione 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 uso come questo programma, questo causa una crescita esponenziale nel tempo di esecuzione.
Ricorda che abbiamo generato il mazzo originale usando una query LINQ. Ogni mescolamento viene generato eseguendo tre query LINQ sul mazzo precedente. Tutti questi vengono eseguiti 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. Quindi, lo correggerai.
Nel file Extensions.cs
, digitare o copiare il metodo seguente. 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. Questo metodo di estensione può essere appeso a qualsiasi query per indicare che la query è stata eseguita.
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;
}
Verrà visualizzata una sottolineatura ondulata rossa sotto File
, che significa che non esiste. Non verrà compilata, perché il compilatore non sa cos'è File
. Per risolvere questo problema, assicurarsi di aggiungere la riga di codice seguente sotto la prima riga di Extensions.cs
:
using System.IO;
Questo dovrebbe risolvere il problema e l'errore rosso scompare.
Instrumentare quindi la definizione di ogni query con un messaggio di log:
// 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);
}
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. Gli effetti della valutazione differita saranno comunque visibili. In un'unica esecuzione vengono eseguite 2592 query, compresa la generazione di tutti i valori e i semi.
È possibile migliorare le prestazioni del codice qui per ridurre il numero di esecuzioni eseguite. Una semplice correzione che è possibile apportare consiste nel memorizzare nella cache i risultati della query LINQ originale che costruisce il mazzo di carte. Attualmente, esegui ripetutamente le query ogni volta che il ciclo do-while attraversa un'iterazione, ricreando e rimescolando il mazzo di carte ogni volta. Per memorizzare nella cache il mazzo di carte, è possibile sfruttare i metodi ToArray LINQ e ToList. Quando vengono accodate alle query, eseguiranno le stesse azioni a cui le hai riportate, ma ora archivieranno i risultati in una matrice o in un elenco, a seconda del metodo a cui scegli di chiamare. Aggiungere il metodo ToArray LINQ a entrambe le query ed eseguire di nuovo il programma:
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);
}
Ora il mescolamento "out" è ridotto a 30 query. Eseguire di nuovo con in shuffle e si noteranno miglioramenti simili: ora esegue 162 query.
Si noti che questo esempio è progettato per evidenziare i casi d'uso in cui la valutazione differita può causare problemi di prestazione. 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 se si sceglie di usare la valutazione pigra o sollecita, quindi bisogna misurare i processi e scegliere il tipo di 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 incontrare problemi di prestazioni come una riduzione della velocità
- valutazione pigra e immediata per quanto riguarda le query LINQ e le implicazioni che potrebbero avere sulle prestazioni delle query
Oltre a LINQ, si è appreso un po' di una tecnica che i magici usano per i trucchi per 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: