Introduzione alle query LINQ in C#

Una query è un'espressione che recupera dati da un'origine dati. Origini dati diverse hanno linguaggi di query nativi diversi, ad esempio SQL per database relazionali e XQuery per XML. Gli sviluppatori devono imparare un nuovo linguaggio di query per ogni tipo di origine dati o formato dati supportato. LINQ semplifica questa situazione offrendo un modello di linguaggio C# coerente per tipi di origini dati e formati. In una query LINQ si lavora sempre con oggetti C#. Vengono usati gli stessi criteri di codifica di base per eseguire una query e trasformare i dati in documenti XML, database SQL, raccolte .NET e qualsiasi altro formato per il quale sia disponibile un provider LINQ.

Tre parti di un'operazione di query

Tutte le operazioni di query LINQ sono costituite da tre azioni distinte:

  1. Ottenere l'origine dati.
  2. Creare la query.
  3. Eseguire la query.

Nell'esempio seguente viene illustrato come le tre parti di un'operazione di query vengono espresse nel codice sorgente. Nell'esempio viene usata una matrice di valori interi come origine dati per motivi di praticità. Gli stessi concetti si applicano però anche ad altre origini dati. In questo articolo si fa riferimento sempre a tale esempio.

// The Three Parts of a LINQ Query:
// 1. Data source.
int[] numbers = [ 0, 1, 2, 3, 4, 5, 6 ];

// 2. Query creation.
// numQuery is an IEnumerable<int>
var numQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

// 3. Query execution.
foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

Nella figura seguente viene illustrata l'operazione di query completa. In LINQ, l'esecuzione della query è distinta dalla query stessa. In altre parole, non si recuperano dati creando una variabile di query.

Diagramma di un'operazione di query LINQ completa.

Origine dati

L'origine dati nell'esempio precedente è una matrice che supporta l'interfaccia generica IEnumerable<T>. Ciò significa che è possibile eseguire query con LINQ. Viene eseguita una query in un'istruzione foreach e foreach richiede IEnumerable o IEnumerable<T>. I tipi che supportano IEnumerable<T> o un'interfaccia derivata, ad esempio l'interfaccia generica IQueryable<T> sono denominati tipi queryable.

Un tipo queryable non richiede alcuna modifica o trattamento speciale per essere usato come origine dati LINQ. Se i dati di origine non sono già in memoria come tipi queryable, il provider LINQ deve rappresentarli come tali. Ad esempio, LINQ to XML carica un documento XML in un tipo XElement queryable:

// Create a data source from an XML document.
// using System.Xml.Linq;
XElement contacts = XElement.Load(@"c:\myContactList.xml");

Con EntityFramework si crea un mapping relazionale a oggetti tra le classi C# e lo schema del database. È possibile scrivere le query sugli oggetti e in fase di esecuzione EntityFramework gestisce la comunicazione con il database. Nell'esempio seguente Customers rappresenta una tabella specifica nel database e il tipo del risultato della query, IQueryable<T>, deriva da IEnumerable<T>.

Northwnd db = new Northwnd(@"c:\northwnd.mdf");

// Query for customers in London.
IQueryable<Customer> custQuery =
    from cust in db.Customers
    where cust.City == "London"
    select cust;

Per altre informazioni sulla creazione di tipi specifici di origini dati, vedere la documentazione dei diversi provider LINQ. Tuttavia, la regola di base è semplice: un'origine dati LINQ è qualsiasi oggetto che supporti l'interfaccia generica IEnumerable<T> o un'interfaccia da essa ereditata, in genere IQueryable<T>.

Nota

È anche possibile usare come origine dati LINQ tipi come ArrayList che supportano l'interfaccia non generica IEnumerable. Per altre informazioni, vedere Come eseguire una query su un ArrayList con LINQ (C#).

La query

La query specifica le informazioni da recuperare dall'origine o dalle origini dati. Una query può anche specificare il modo in cui ordinare, raggruppare e definire le informazioni prima di essere restituite. Una query viene archiviata in una variabile di query e inizializzata con un'espressione di query. Si usa la sintassi di query C# per scrivere query.

La query nell'esempio precedente restituisce tutti i numeri pari dalla matrice di valori interi. L'espressione di query contiene tre clausole: from, where e select. Se si ha dimestichezza con SQL, si sarà notato che l'ordine delle clausole è inverso rispetto all'ordine in SQL. La clausola from specifica l'origine dati, la clausola where applica il filtro e la clausola select specifica il tipo degli elementi restituiti. Tutte le clausole di query sono descritte in dettaglio nella presente sezione. L'aspetto importante per il momento è che in LINQ la variabile di query stessa non effettua alcuna azione e non restituisce dati. Archivia solo le informazioni richieste per generare i risultati quando successivamente viene eseguita la query. Per altre informazioni sul modo in cui le query vengono costruite, vedere Cenni preliminari sugli operatori di query standard (C#).

Nota

Le query possono anche essere espresse usando la sintassi del metodo. Per altre informazioni, vedere Sintassi di query e sintassi di metodi in LINQ.

Classificazione degli operatori di query standard in base alla modalità di esecuzione

Le implementazioni LINQ to Objects dei metodi degli operatori di query standard vengono eseguite in due modi principali: implementazione immediata o posticipata. Gli operatori di query che usano l'esecuzione posticipata possono essere anche suddivisi in due categorie: di flusso e non di flusso.

Immediate

L'esecuzione immediata indica che l'origine dei dati è in lettura e l'operazione viene eseguita una volta. Tutti gli operatori di query standard che restituiscono immediatamente un risultato da scalare. Esempi di tali query sono Count, Max, Average e First. Queste metodi vengono eseguiti senza un'istruzione foreach esplicita poiché la query stessa deve usare foreach per poter restituire un risultato. Queste query restituiscono un singolo valore, non una raccolta IEnumerable. È possibile forzare l'esecuzione immediata di qualunque query usando i metodi Enumerable.ToList o Enumerable.ToArray. L'esecuzione immediata fornisce il riutilizzo dei risultati della query, non della dichiarazione di query. I risultati vengono recuperati una sola volta, quindi archiviati per un uso futuro. Nella query seguente viene restituito un conteggio dei numeri pari nella matrice di origine:

var evenNumQuery =
    from num in numbers
    where (num % 2) == 0
    select num;

int evenNumCount = evenNumQuery.Count();

Per forzare l'esecuzione immediata di una query e memorizzarne nella cache i risultati, è possibile chiamare i metodi ToList o ToArray.

List<int> numQuery2 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToList();

// or like this:
// numQuery3 is still an int[]

var numQuery3 =
    (from num in numbers
        where (num % 2) == 0
        select num).ToArray();

È anche possibile forzare l'esecuzione inserendo il ciclo foreach immediatamente dopo l'espressione di query. Tuttavia, chiamando ToList o ToArray vengono memorizzati nella cache anche tutti i dati di un singolo oggetto della raccolta.

Differito

L'esecuzione posticipata indica che l'operazione non viene eseguita in corrispondenza del punto del codice in cui viene dichiarata la query. L'operazione viene eseguita solo quando la variabile di query viene enumerata, ad esempio tramite un'istruzione foreach. I risultati dell'esecuzione della query dipendono dal contenuto dell'origine dati nel momento in cui viene eseguita la query e non nel momento in cui viene definita. Se la variabile di query viene enumerata più volte, i risultati potrebbero essere diversi ogni volta. Quasi tutti gli operatori query standard con tipo restituito IEnumerable<T> o IOrderedEnumerable<TElement> vengono eseguiti in modo posticipato. L'esecuzione posticipata offre la funzionalità di riutilizzo delle query perché la query recupera i dati aggiornati dall'origine dati ogni volta che i risultati delle query vengono iterati. Il codice seguente mostra un esempio di esecuzione posticipata:

foreach (int num in numQuery)
{
    Console.Write("{0,1} ", num);
}

Dall'istruzione foreach vengono anche recuperati i risultati della query. Ad esempio, nella query precedente la variabile di iterazione num contiene ogni valore (uno alla volta) della sequenza restituita.

Poiché la variabile di query stessa non contiene mai i risultati della query, è possibile eseguirla ripetutamente per recuperare i dati aggiornati. Ad esempio, un'applicazione separata potrebbe aggiornare continuamente un database. Nell'applicazioneè possibile creare una query che recupera i dati più recenti ed eseguirla a intervalli per recuperare i risultati aggiornati.

Gli operatori di query che usano l'esecuzione posticipata possono essere anche suddivisi in due categorie: di flusso e non di flusso.

Streaming

Gli operatori di flusso non devono leggere tutti i dati di origine prima di restituire elementi. In fase di esecuzione, un operatore di flusso esegue l'operazione su ogni elemento di origine che legge e restituisce l'elemento, se appropriato. Un operatore di flusso continua a leggere gli elementi fino a quando non può essere prodotto un elemento di risultato. Ciò significa che potrebbe essere letto più di un elemento di origine per produrre un elemento di risultato.

Non di flusso

Gli operatori non di flusso devono leggere tutti i dati di origine prima di produrre un elemento di risultato. In questa categoria rientrano operazioni quali ordinamento o raggruppamento. In fase di esecuzione, gli operatori di query non di flusso leggono tutti i dati di origine, li inseriscono in una struttura dei dati, eseguono l'operazione e producono gli elementi risultanti.

Tabella di classificazione

La tabella seguente classifica ogni metodo di operatore di query standard in base al metodo di esecuzione.

Nota

Se un operatore è contrassegnato in due colonne, vengono coinvolte due sequenze di input nell'operazione e ogni sequenza viene restituita in modo diverso. In questi casi, viene restituita sempre la prima sequenza nell'elenco dei parametri in modo posticipato e di flusso.

Operatore di query standard Tipo restituito Esecuzione immediata Esecuzione posticipata di flusso Esecuzione posticipata non di flusso
Aggregate TSource X
All Boolean X
Any Boolean X
AsEnumerable IEnumerable<T> X
Average Valore numerico singolo X
Cast IEnumerable<T> X
Concat IEnumerable<T> X
Contains Boolean X
Count Int32 X
DefaultIfEmpty IEnumerable<T> X
Distinct IEnumerable<T> X
ElementAt TSource X
ElementAtOrDefault TSource? X
Empty IEnumerable<T> X
Except IEnumerable<T> X X
First TSource X
FirstOrDefault TSource? X
GroupBy IEnumerable<T> X
GroupJoin IEnumerable<T> X X
Intersect IEnumerable<T> X X
Join IEnumerable<T> X X
Last TSource X
LastOrDefault TSource? X
LongCount Int64 X
Max Valore numerico singolo TSource o TResult? X
Min Valore numerico singolo TSource o TResult? X
OfType IEnumerable<T> X
OrderBy IOrderedEnumerable<TElement> X
OrderByDescending IOrderedEnumerable<TElement> X
Range IEnumerable<T> X
Repeat IEnumerable<T> X
Reverse IEnumerable<T> X
Select IEnumerable<T> X
SelectMany IEnumerable<T> X
SequenceEqual Boolean X
Single TSource X
SingleOrDefault TSource? X
Skip IEnumerable<T> X
SkipWhile IEnumerable<T> X
Sum Valore numerico singolo X
Take IEnumerable<T> X
TakeWhile IEnumerable<T> X
ThenBy IOrderedEnumerable<TElement> X
ThenByDescending IOrderedEnumerable<TElement> X
ToArray Matrice TSource[] X
ToDictionary Dictionary<TKey,TValue> X
ToList IList<T> X
ToLookup ILookup<TKey,TElement> X
Union IEnumerable<T> X
Where IEnumerable<T> X

LINQ to Objects

Il termine "LINQ to Objects" indica l'uso diretto di query LINQ con qualsiasi raccolta IEnumerable o IEnumerable<T>. È possibile usare LINQ per eseguire query su qualsiasi raccolta enumerabile, ad esempio List<T>, Array o Dictionary<TKey,TValue>. La raccolta può essere definita dall'utente o essere un tipo restituito da un'API .NET. Con l'approccio LINQ, è possibile scrivere il codice dichiarativo che descrive i dati da recuperare. LINQ to Objects rappresenta un'ottima introduzione alla programmazione con LINQ.

Le query LINQ offrono tre vantaggi principali rispetto ai cicli foreach tradizionali:

  • Sono più brevi e leggibili, soprattutto quando si filtrano più condizioni.
  • Forniscono funzioni potenti di filtro, ordinamento e raggruppamento con un codice dell'applicazione minimo.
  • Possono essere trasferiti in altre origini dati con modifiche minime o nulle.

Più è complessa l'operazione da eseguire sui dati, maggiore sarà il vantaggio che si potrà trarre dall'uso di LINQ rispetto alle tecniche di iterazione tradizionali.

Archiviare i risultati di una query in memoria

Una query è fondamentalmente un set di istruzioni per il recupero e l'organizzazione dei dati. Le query vengono eseguite in modalità lazy poiché viene richiesto ogni elemento successivo nel risultato. Quando si usa foreach per scorrere i risultati, gli elementi vengono restituiti quando ne viene eseguito l'accesso. Per valutare una query e archiviare i risultati senza eseguire un ciclo di foreach, è sufficiente chiamare uno dei seguenti metodi sulla variabile di query:

Quando si archiviano i risultati della query, è opportuno assegnare l'oggetto Collection restituito a una nuova variabile, come illustrato nell'esempio seguente:

List<int> numbers = [1, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20];

IEnumerable<int> queryFactorsOfFour =
    from num in numbers
    where num % 4 == 0
    select num;

// Store the results in a new variable
// without executing a foreach loop.
var factorsofFourList = queryFactorsOfFour.ToList();

// Read and write from the newly created list to demonstrate that it holds data.
Console.WriteLine(factorsofFourList[2]);
factorsofFourList[2] = 0;
Console.WriteLine(factorsofFourList[2]);

Vedi anche