Introduzione a LINQ: LINQ-to-Objects
a cura di Alessandro Del Sole (requisiti: conoscenze approfondite di .NET Framework)

Introduzione
Una delle più importanti novità introdotte da Microsoft .NET Framework 3.5 è costituita da LINQ (acronimo di Language INtegrated Query). LINQ è costituito da una serie di estensioni sintattiche per i linguaggi gestiti e permette di eseguire interrogazioni su molteplici tipologie di sorgenti dati, come ad esempio documenti XML (LINQ-to-Xml), database SQL (LINQ-to-SQL), collezioni di oggetti .NET (LINQ-to-Objects) e oggetti Dataset (LINQ-to-Dataset).

LINQ aggiunge ai linguaggi gestiti la possibilità di utilizzare, nel codice, alcune parole chiave che richiamano la sintassi SQL (le cosiddette query expression), permettendo così di eseguire interrogazioni al fine di estrapolare da una fonte dati le informazioni di interesse per lo sviluppatore. Inoltre, nell'approfondire l'argomento LINQ, capirete il perché di molte novità sintattiche delle versioni 2008 dei linguaggi .NET, come i metodi Extension, le espressioni Lambda, i tipi inferiti e i tipi anonimi, novità introdotte, per l'appunto, proprio a supporto di LINQ.

Oltre a costituire una importante caratteristica di .NET 3.5, LINQ è una piattaforma piuttosto articolata e complessa, trattata dettagliatamente nella documentazione MSDN a corredo di Visual Studio 2008. In questo articolo mi propongo di introdurre LINQ-to-Objects, che permette di eseguire query su collezioni di oggetti .NET, all'interno di un'applicazione dimostrativa scritta in Visual Basic 2008.

Prima di proseguire la lettura di questo articolo, è bene che si leggano i due seguenti articoli di Antonio Catucci, disponibili in Area Articoli di VB T&T, che introducono le principali novità sintattiche di Visual Basic 2008 e che torneranno utili anche in questo articolo (sebbene verranno dati brevi cenni di ciascuna novità laddove necessario):

Questa lettura preliminare si rende necessaria poiché nel corso di questo articolo (e nei prossimi dedicati a LINQ) farò spesso riferimento a concetti come l'inferenza dei tipi, metodi Extension e espressioni Lambda già spiegati molto bene da Antonio. L'articolo è corredato, come di consueto, da codice sorgente disponibile in Area Download di VB T&T. Ma, stavolta più che mai, è importante seguire passo per passo quanto descritto nell'articolo, prima di passare alla visione del codice completato, al fine di comprendere i vari concetti esposti.

Addentriamoci ora nella scrittura di codice Visual Basic, preparando una collection di base che andremo poi a interrogare mediante l'utilizzo della sintassi introdotta da LINQ.

Predisposizione del progetto e della collection di esempio
In primo luogo, dopo aver avviato Visual Studio 2008 (o Visual Basic 2008 Express Edition), create una nuova applicazione per la Console, che è sufficiente per i nostri scopi didattici. Ipotizziamo di lavorare in una ferramenta che ha, come giacenza di magazzino, una serie di utensili per ciascuno dei quali viene memorizzata la quantità giacente. Pertanto, predisponiamo una semplice classe chiamata Utensile con due proprietà, Nome e Quantita:

Class Utensile

  'Campi privati per memorizzare i valori delle proprietà
  Dim name As String
  Dim quantity As Integer

  'Ottiene o imposta il nome di un utensile a magazzino
  Property Nome() As String
    Get
      Return name
    End Get
    Set(ByVal value As String)
      name = value
    End Set
  End Property

  'Ottiene o imposta la quantità di un utensile a magazzino
  Property Quantita() As Integer
    Get
      Return quantity
    End Get
    Set(ByVal value As Integer)
      quantity = value
    End Set
  End Property
End Class

La classe non espone, almeno esplicitamente, alcun costruttore. Il perché di questo particolare sarà chiarito tra breve.

Nel modulo, creiamo un metodo chiamato primaQuery, nel quale implementiamo codice che istanzi e riempia un elenco di tre oggetti di tipo Utensile, specificandone nome e quantità giacente:

  Sub primaQuery()

    'Istanzia una nuova lista di utensili
    Dim magazzino As New List(Of Utensile)

    'Utilizzo della c.d. Object Initialization Expression
    magazzino.Add(New Utensile With {.Nome = "Cacciavite", .Quantita = "10"})
    magazzino.Add(New Utensile With {.Nome = "Trapano", .Quantita = "5"})
    magazzino.Add(New Utensile With {.Nome = "Pinze", .Quantita = "15"})

  End Sub

Come potete osservare anche nei commenti, in questo frammento di codice si fa uso della cosiddetta Object Initialization Expression, che permette di inizializzare le proprietà di una classe nel momento in cui la si istanzia, anche se non è implementato alcun costruttore con argomenti.

Ora che abbiamo la base su cui lavorare, andiamo a fare la conoscenza di LINQ.

La prima query con LINQ
Ipotizziamo di voler sapere quali sono gli utensili giacenti in magazzino presenti in quantità maggiore o uguale a dieci unità. Di ciascun utensile vogliamo memorizzare il nome e mostrarlo poi nella finestra della Console. Questo obiettivo si raggiunge mediante il seguente frammento di codice, da aggiungere nel metodo primaQuery di seguito all'esistente:

    Dim utensili = From pezzo In magazzino _
                   Where pezzo.Quantita >= 10 _
                   Select pezzo.Nome

La prima cosa su cui è importante focalizzare l'attenzione è costituita dai cosiddetti tipi inferiti. Infatti, non abbiamo specificato, utilizzando la consueta clausola As, il tipo di dato per le variabili utensili e pezzo, poiché lo fa automaticamente il compilatore. Questo è utile non solo per scrivere meno codice, ma anche perché il compilatore determina il tipo di dato corretto evitando allo sviluppatore di incorrere in errori.

In secondo luogo, passiamo ad osservare la query expression che, come è facile osservare, è costituita da quelle parole chiave tipiche del SQL di cui parlavamo poc'anzi. La query esegue le seguenti operazioni: per ciascun utensile (chiamato pezzo) contenuto nella collection magazzino (From pezzo in magazzino), laddove la quantità dell'utensile sia maggiore o uguale a dieci (Where pezzo.Quantita>=10), seleziona il nome dell'utensile (Select pezzo.nome). Ciascun elemento estratto dalla collection magazzino viene memorizzato nella variabile utensili che, a sua volta, è una collection che può così essere iterata per mostrare il risultato ottenuto, mediante, ad esempio, il seguente ciclo For..Each:

    For Each attrezzo In utensili
      Console.WriteLine(attrezzo)
    Next
    Console.ReadLine()

Avviando l'applicazione, otterremo un risultato simile alla seguente figura (dovete, ovviamente, aggiungere una chiamata al metodo primaQuery all'interno del metodo Main):

Come potete vedere, vengono mostrati i nomi dei soli utensili presenti a magazzino in quantità maggiore o uguale a dieci unità. Ora che avete utilizzato LINQ per la vostra prima query, siete pronti per passare a qualcosa di più complesso: l'ordinamento tramite metodi Extension.

Ordinamento del risultato
LINQ mette a disposizione diverse metodologie per l'ordinamento del risultato di una query. Ad esempio, si può utilizzare una clausola Order By all'interno della query stessa. In questa sede vogliamo vedere come eseguire l'ordinamento decrescente della collection utensili mediante l'utilizzo del metodo Extension chiamato OrderByDescending. Prima di fare questo, è bene precisare che LINQ aggiunge tutta una serie di metodi Extension agli oggetti sui quali si sta lavorando (proprio per questo si chiamano Extension), pertanto l'OrderByDescending è solo uno di questi metodi. Per non confonderci le idee, scriviamo nel modulo un nuovo metodo chiamato secondaQuery. All'interno di esso copiate la prima parte del codice del metodo precedente, fino alla query expression compresa, escludendo, quindi, il ciclo For..Each. Quindi aggiuntete la seguente riga di codice:

    'ordinamento tramite Extension method
    Dim attrezzi As IEnumerable(Of String) = utensili.OrderByDescending(Function(nome) nome)

La variabile attrezzi viene tipizzata come oggetto che rispetta l'interfaccia IEnumerable affinchè possa essere iterato come collezione di stringhe. Come potete osservare, l'oggetto utensili espone il metodo Extension chiamato OrderByDescending, che restituisce una collezione ordinata in modo decrescente a seconda del tipo di dato specificato dall'espressione lambda (Function(nome) nome). Per verificare il risultato dell'ordinamento, si può riscrivere il ciclo For..Each come segue:

    For Each attrezzo In attrezzi
      Console.WriteLine(attrezzo)
    Next
    Console.ReadLine()

Possiamo verificare il risultato dell'ordinamento avviando l'applicazione, come si può vedere in figura:

Avete quindi visto come eseguire l'ordinamento di una collection tramite metodo Extension. Nel prossimo paragrafo ci occupiamo di vedere un'altra importante caratteristica di LINQ, ossia i collegamenti tra fonti dati diverse.

Collegare dati di fonti diverse all'interno di una query
Supponiamo, ora, di voler conoscere la collocazione fisica di ciascuna tipologia di utensile vista in precedenza, ad esempio il ripiano dedicato ai cacciaviti all'interno di uno scaffale e così via. In primo luogo, creiamo una nuova classe chiamata Scaffale, che espone due proprietà Nome (ci serve una proprietà comune con la classe Utensile) e una proprietà chiamata Posizione. Potremmo scriverla nel modo seguente:

Class Scaffale

  'Campi privati per memorizzare i valori delle proprietà
  Dim name As String
  Dim position As String

  'Ottiene o imposta il nome di un utensile a magazzino
  Property Nome() As String
    Get
      Return name
    End Get
    Set(ByVal value As String)
      name = value
    End Set
  End Property

  'Ottiene o imposta la collocazione di un utensile nello scaffale
  Property Posizione() As String
    Get
      Return position
    End Get
    Set(ByVal value As String)
      position = value
    End Set
  End Property
End Class

A questo punto è conveniente scrivere nel modulo un terzo metodo, chiamato terzaQuery, all'interno del quale aggiungeremo, oltre alla collection magazzino vista in precedenza, anche una nuova collection chiamata posizioni e che conterrà un elenco che rappresenta la posizione di ciascun utensile all'interno di un determinato scaffale. Il codice è il seguente:

  Sub terzaQuery()
    Dim magazzino As New List(Of Utensile)

    magazzino.Add(New Utensile With {.Nome = "Cacciavite", .Quantita = "10"})
    magazzino.Add(New Utensile With {.Nome = "Trapano", .Quantita = "5"})
    magazzino.Add(New Utensile With {.Nome = "Pinze", .Quantita = "15"})

    Dim posizioni As New List(Of Scaffale)
    posizioni.Add(New Scaffale With {.Nome = "Cacciavite", .Posizione = "Primo ripiano"})
    posizioni.Add(New Scaffale With {.Nome = "Trapano", .Posizione = "Secondo ripiano"})
    posizioni.Add(New Scaffale With {.Nome = "Pinze", .Posizione = "Terzo ripiano"})

  End Sub

Ora eseguiremo una nuova interrogazione finalizzata ad estrarre tutti gli utensili presenti in quantità maggiore o uguale a dieci, recuperando, per ciascun oggetto, la posizione all'interno dello scaffale. Di seguito al codice sopra riportato, aggiungiamo la seguente query expression:

    Dim utensili = From pezzo In magazzino _
                   Where pezzo.Quantita >= 10 _
                   Join posizione In posizioni On pezzo.Nome Equals posizione.Nome _
                   Select posizione

Abbiamo utilizzato una clausola Join che completa l'interrogazione nel modo seguente: laddove per ciascun pezzo in magazzino (From pezzo In magazzino) l'arnese stesso sia presente in quantità maggiore o uguale a dieci unità (Where pezzo.Quantita>=10), esamina contemporaneamente la collection posizioni e, se per la posizione esaminata la proprietà Nome è uguale alla proprietà Nome del pezzo, seleziona la posizione stessa.

La variabile utensili è ancora una collection che può essere iterata nel modo seguente:

    For Each attrezzo In utensili
      Console.WriteLine(String.Concat("L'attrezzo ", attrezzo.Nome, " è riposto nel ", _
                                      attrezzo.Posizione))
    Next
    Console.ReadLine()

La variabile di ciclo attrezzo contiene le proprietà Nome e Posizione estratte tramite la query, per ciascun utensile presente in magazzino in quantità maggiore o uguale a dieci.

Basterà avviare l'applicazione per vedere che l'obiettivo prefisso è stato raggiunto:

Utilizzo dei tipi anonimi
La query expression precedente può essere riscritta facendo uso dei tipi anonimi, nel modo seguente:

    Dim utensili = From pezzo In magazzino _
                   Where pezzo.Quantita >= 10 _
                   Join posizione In posizioni On pezzo.Nome Equals posizione.Nome _
                   Select New With {posizione.Nome, posizione.Posizione}

In questo modo, la variabile utensili assume la forma di una collection di tipi anonimi. Ad ogni buon conto, il risultato rimane invariato e potete provare riavviando l'applicazione.

Brevi spunti di studio
Oltre ad ordinare i dati, LINQ fornisce metodi per il loro raggruppamento. Questo può avvenire mediante la clausola Group By o la clausola Group usata in congiunzione con Join. La documentazione MSDN a corredo di Visual Studio 2008 offre gli strumenti necessari per approfondire ambedue le possibilità.

Conclusioni
LINQ mette a disposizione dello sviluppatore strumenti estremamente versatili, anche se a volte complessi, per lavorare con grandi quantità di dati. In questo articolo abbiamo solamente introdotto LINQ-to-Objects, mentre in un prossimo articolo introdurremo LINQ-to-Xml e vedremo come Visual Basic 2008 offra supporto in-line per i documenti XML e come LINQ metta a disposizione classi molto più potenti rispetto al passato per manipolare questo tipo di documenti. Avete comunque acquisito le nozioni di partenza per eseguire delle query di base su oggetti .NET utilizzando la nuova sintassi di Visual Basic 2008.

Vi ricordo, infine, che sul mio blog sono disponibili alcuni screen-cast che ho realizzato intorno a LINQ-to-Objects e che potete scaricare liberamente. Per ulteriori informazioni potete contattarmi al mio indirizzo e visitare il mio blog.