blexin

Sviluppo
Consulenza e
Formazione IT


Blog

Evolvi la tua azienda

Vediamo insieme come LINQ abbia rivoluzionato il modo con cui accediamo ai dati in .NET

LINQ: un linguaggio per domarli tutti!

Mercoledì 17 Giugno 2020

Una delle peculiarità che contraddistingue il mondo .NET dagli altri stack tecnologici è sicuramente LINQ, acronimo di Language INtegrated Query. Introdotto con .NET Framework 3.5 e Visual Studio 2008, è di fatto il primo framework indipendente dall’architettura e integrato all’interno dei linguaggi C# e Visual Basic.

Con LINQ possiamo eseguire query e manipolare dati, sfruttando un unico modello di programmazione indipendente dalle varie tipologie di fonti. Per capire meglio di cosa si tratti, occorre però fare un piccolo salto nel passato.

Nelle prime versioni di C#, dovevamo utilizzare un ciclo for o foreach per iterare su una collezione, che come sappiamo implementa l’interfaccia IEnumerable, e trovare ad esempio al suo interno un oggetto particolare. Il seguente codice restituisce tutti i clienti di un’azienda con età compresa tra i 19 e i 36 anni (20 - 35 anni):

class Customer
{ 
   public int CustomerID { get; set; } 
   public String CustomerName { get; set; } 
   public int Age { get; set; } 
} 

class Program 
{ 
   static void Main(string[] args) 
   { 
       Customer[] customerArray = { 
          new Customer() { CustomerID = 1, CustomerName = "Joy", Age = 22 }, 
          new Customer() { CustomerID = 2, CustomerName = "Bob", Age = 45 }, 
          new Customer() { CustomerID = 3, CustomerName = "Curt", Age = 25 },
       };
       
       Customer[] customers = new Customer[10]; 
       
       int i = 0; 
       
       foreach (Customer cst in customerArray) 
       { 
           if (cst.Age > 19 && cst.Age < 36) 
           { 
               customers[i] = cst; 
               i++; 
           }
       } 
   } 
}

Quale potrebbe essere un approccio alternativo? Proviamo ad arrivarci per passi, partendo dal concetto di delegate. Un delegate è un tipo che rappresenta riferimenti a metodi con gli stessi parametri e tipo restituito. Esso “delega” al metodo a cui punta l’esecuzione del codice e possiamo dichiararlo in questo modo:

public delegate bool Operations(int number);

Tutti i metodi che prendono in ingresso un numero intero e restituiscono un booleano possono essere puntati da questo delegate. Per esempio, supponiamo di avere in una classe CustomerOperations un metodo:

public bool CustomerAgeRangeCheck(int number)
{
    return number > 19 && number < 36; 
}

Abbiamo la possibilità di registrare uno o più metodi che verranno eseguiti nel momento in cui verrà eseguito il delegate stesso:

Operations op = new Operations(CustomerOperations.CustomerAgeRangeCheck);

O semplicemente:

Operations op = CustomerOperations.CustomerAgeRangeCheck;

Possiamo quindi utilizzare il delegate, che in questo caso restituirà come risultato true:

op(22);

I delegate vengono utilizzati per passare metodi come argomenti ad altri metodi: i gestori di evento e le callback sono un esempio di metodi richiamati tramite delegate.

C# 2.0 ha introdotto i delegate anonimi, è possibile ora utilizzare un metodo anonimo per dichiarare e inizializzare un delegato. Ad esempio possiamo scrivere:

delegate bool CustomerFilters(Customer customer);
 
class CustomerOperations
{
    public static Customer[] FindWhere(Customer[] customers, CustomerFilters  customerFiltersDelegate)
    {
        int i = 0;
        Customer[] result = new Customer[6];
        foreach (Customer customer in customers)
           if (customerFiltersDelegate(customer))
           {
               result[i] = customer;
            	i++;
           }
   
    	return result;
    }
}
 
class Program
{
    static void Main(string[] args)
    {
        Customer[] customers = {
      	   new Customer() { CustomerID = 1, CustomerName = "Joy", Age = 22 },
      	   new Customer() { CustomerID = 2, CustomerName = "Bob", Age = 45 },
      	   new Customer() { CustomerID = 3, CustomerName = "Curt", Age = 25 },
    	};

    	Customer[] filteredCustomersAge = CustomerOperations.FindWhere(customers, delegate (Customer customer)  //Using anonimous delegate
    	   {
              return customer.Age > 19 && customer.Age < 36;
    	   });
    }
}

Quindi con C# 2.0, abbiamo il vantaggio utilizzare i delegate anonimi nella ricerca con criteri diversi, senza la necessità di utilizzare un ciclo for o foreach. Possiamo ad esempio usare la stessa funzione delegato utilizzata nell’esempio precedente, per trovare un cliente il cui “CustomerID” è 3 o il cui nome è “Bob”:

Customer[] filteredCustomersId = CustomerOperations.FindWhere(customers, delegate (Customer customer)  
    	  {
             return customer.CustomerID == 3;
    	  });

Customer[] filteredCustomersName = CustomerOperations.FindWhere(customers, delegate (Customer customer)  
    	  {
             return customer.CustomerName == "Bob";
    	  });

Con l’evoluzione del linguaggio, il team Microsoft ha introdotto dalla versione di C# 3.x nuove funzionalità che rendono il codice ancora più compatto e leggibile, e che sono a diretto supporto di LINQ per eseguire query sui diversi tipi di fonti dati e ottenere gli elementi risultanti in una singola istruzione.

Queste funzionalità sono:

    - Il costrutto var, cioè una variabile locale tipizzata in modo implicito. È fortemente tipizzata come se si fosse dichiarato il tipo stesso, ma è il compilatore a determinarne il tipo utilizzando l’inferenza di tipo (Type Inference) sulla base del valore che gli viene assegnato. Le due dichiarazioni seguenti sono equivalenti dal punto di vista funzionale:

var customerAge = 30; // Implicitly typed.
int customerAge = 30; // Explicitly typed.

    - Gli object initializer, che consentono di assegnare valori a tutte o ad alcune delle proprietà di un oggetto in fase di creazione senza dover richiamare un costruttore seguito da righe di istruzioni di assegnazione.

Customer customer = new Customer { Age = 30, CustomerName = "Adolfo" };

A differenza del seguente codice, nel caso precedente, il tutto viene considerato come una singola operazione.

Customer customer = new Customer();
customer.Age = 30; 
customer.CustomerName = "Adolfo";

    - Gli anonymous type, cioè un tipo in sola lettura, che viene costruito dal compilatore ed è solo il compilatore a conoscerlo. Inoltre, se due o più inizializzatori di oggetti anonimi in un assembly specificano una sequenza di proprietà nello stesso ordine e con gli stessi nomi e tipi, il compilatore considera gli oggetti come istanze dello stesso tipo. I tipi anonimi costituiscono una valida soluzione per raggruppare temporaneamente un set di proprietà nel risultato di una query, senza dover definire un tipo denominato separato.

var customer = new { YearsOfFidelity = 10, Name = "Francesco"}; 

    - Gli extension method, o metodi di estensione, che consentono di "aggiungere" metodi ai tipi esistenti senza creare un nuovo tipo derivato, ricompilare o modificare in altro modo il tipo originale. I metodi di estensione sono metodi statici, ma vengono chiamati grazie alla syntactic sugar introdotta, come se fossero metodi di istanza sul tipo esteso.

public static class StringExtensionMethods
{
    public static string ReverseString(this string input)
    {
        if (string.IsNullOrEmpty(input)) return "";
        return new string(input.ToCharArray().Reverse().ToArray());
    }
}

Gli extension method devono essere definiti in una classe statica. Il primo parametro rappresenta il tipo da estendere e deve essere preceduto dalla keyword this, ulteriori parametri non ne necessitano.

Console.WriteLine("Hello".ReverseString());   //olleH

Da notare che il primo parametro, quello preceduto dal modificatore this, non deve essere specificato nella chiamata al metodo.

    - Le lambda expression, funzioni anonime che possono essere passate come variabile o come parametro nella chiamata di un metodo.

customer => customer.Age > 19 && customer.Age < 36;

L’operatore => è chiamato lambda operator, mentre customer è il parametro in ingresso della funzione. Quello che c’è alla destra del lambda operator rappresenta il corpo della funzione ed il valore che restituisce, in questo caso un booleano.

Arriviamo quindi finalmente alla versione 3.5 di C# e all’introduzione di LINQ.

Semplificando, potremmo dire che LINQ è una libreria di extension method per le interfacce IEnumerable<T> e IQueryable<T>, che ci permette di effettuare diverse operazioni come filtrare, effettuare proiezioni, aggregazioni e ordinamento.

Abbiamo a disposizione diverse implementazioni di LINQ:

  • LINQ to Objects (In-Memory Object Collection)
  • LINQ to Entities (Entity Framework)
  • LINQ to SQL (SQL Database)
  • LINQ to XML (XML Document)
  • LINQ to DataSet (ADO.Net Dataset)
  • Implementando l’interfaccia IQueryable (Altre sorgenti di dati)

Negli esempi precedenti è stata usata una matrice come origine dati, viene quindi supportata implicitamente l'interfaccia generica IEnumerable<T>. I tipi che supportano IEnumerable<T> o un'interfaccia derivata, ad esempio l'interfaccia generica IQueryable<T> sono denominati tipi queryable e ci permettono di eseguire direttamente query LINQ. Se i dati di origine non sono già in memoria come tipo queryable, il provider LINQ deve rappresentarlo come tale.

Come abbiamo detto, le query LINQ sono basate per lo più su tipi generici, introdotti nella versione 2.0 di .NET Framework. Questo vuol dire che se si tenta ad esempio di aggiungere un oggetto Customer a un oggetto List<string>, verrà generato un errore in fase di compilazione. È semplice usare le collection generiche poiché non è necessario eseguire il cast dei tipi in fase di esecuzione.
Se preferiamo possiamo evitare la sintassi generica usando la parola chiave var della quale abbiamo parlato precedentemente.
Vediamo quindi come possiamo ottenere lo stesso risultato che nell’esercizio precedente abbiamo ottenuto utilizzando i delegate anonimi, utilizzando una query LINQ to Object, il costrutto var ed una lambda expression:

var filteredCustomersAge = customers.Where(c => c.Age > 19 && c.Age < 36);

Questo tipo di sintassi è detta Method Syntax.
Nel prossimo esempio, utilizzeremo invece la Query Syntax, una sintassi introdotta per quelli che conoscono già il linguaggio SQL e si sentirebbero quindi a loro agio con questo tipo di approccio:

var filteredCustomersAge =
    from customer in customers
    where customer.Age > 19 && customer.Age < 36
    select customer;

La Query Syntax e la Method Syntax sono semanticamente identiche, molte persone trovano la sintassi delle query più semplice e più facile da leggere.
Nella Query Syntax, gli operatori di query LINQ vengono convertiti in chiamate ai relativi extension method di LINQ in fase di compilazione.

Nel prossimo articolo, continueremo a parlare di LINQ!
Parleremo dell’interfaccia IQueryable<T>, dei relativi metodi di estensione di LINQ e delle differenze con l’interfaccia IEnumerable<T>.
Vedremo inoltre l’utilizzo di LINQ con fonti di dati provenienti da raccolte out-memory, come nel caso di database remoti.

Al prossimo articolo! Stay Tuned!

ISCRIVITI ALLA NEWSLETTER

Autore

Servizi

Evolvi la tua azienda