blexin

Sviluppo
Consulenza e
Formazione IT


Blog

Evolvi la tua azienda

Vediamo insieme quali sono le novità di C# 8

Le novità di C# 8: prima parte

Mercoledì 5 Febbraio 2020

C# è il linguaggio di riferimento del mondo Microsoft .NET e viene aggiornato periodicamente. In Visual Studio 2019, .Net Core 3.x e .Net standard 2.1, troviamo il supporto alla versione 8, che si arricchisce di molte novità che non si limitano ad essere funzionalità del compilatore applicabili a qualsiasi versione del .NET Framework o di .NET Core, ma vi è un'esplicita dipendenza da .NET Core 3. In questo articolo, vi mostrerò la prima parte delle novità introdotte in questa nuova versione.

Dichiarazioni using

Una dichiarazione using indica al compilatore che deve eseguire il Dispose della variabile alla fine dell'ambito di inclusione.
Questa parola chiave è usata quando ci ritroviamo ad usare risorse che implementano l’intefaccia IDisposable. È una best practice effettuare la chiamata al Dispose su questi oggetti per rilasciare immediatamente le risorse e l'uso di using ci garantisce che questo avvenga sempre correttamente. Con C# 8 possiamo omettere le parentesi graffe e le tonde, e dichiarare una variabile semplicemente ponendo using davanti:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines.txt");
    // Notice how we declare skippedLines after the using statement.
    int skippedLines = 0;
    foreach (string line in lines)
    {
    //Some code here...
    }
    // Notice how skippedLines is in scope here.
    return skippedLines;
    // File is disposed here
}

In questo esempio, viene eseguito il Dispose del file quando viene raggiunta la parentesi graffa di chiusura per il metodo. Allo stesso modo, la variabile skippedLines è visibile fino alla fine del metodo. Se volessimo chiudere lo scope prima della fine dello stack, come di consueto, C# ci permette di delineare lo scope di variabili di stack diversi racchiudendo il tutto tra graffe.
Vediamo come sarebbe stato il nostro codice con la sintassi precedente alla nuova versione di C# e dove sarebbe stato eseguito il Dispose:

static int WriteLinesToFile(IEnumerable<string> lines)
{
    // We must declare the variable outside of the using block
    // so that it is in scope to be returned.
    int skippedLines = 0;
    using (var file = new System.IO.StreamWriter("WriteLines.txt"))
    {
         foreach (string line in lines)
         {
         // Some code here...
         }
    } // File is disposed here
    return skippedLines;
}

In questo caso, viene eseguito il Dispose del file quando viene raggiunta la parentesi graffa di chiusura associata all'istruzione using e dobbiamo dichiarare la variabile skippedLines all’esterno del blocco di using altrimenti non sarebbe in scope per essere ritornata.

Funzioni locali statiche

Dalla versione 7.0, C# supporta le funzioni locali : sono metodi privati annidati in un altro membro e possono essere chiamate solo dal relativo membro contenitore e non dall’esterno:

public int Method()
{
    int num;
    LocalFunction();
    return num;

    void LocalFunction() => num = 4;
}

In C# 8 le funzioni locali statiche sono un’estensione delle funzioni locali, è ora possibile aggiungere il modificatore static per assicurarsi che tale funzione locale non faccia riferimento a variabili nell'ambito di inclusione.
Nel codice precedente, la funzione locale LocalFunction accede alla variabile num, dichiarata nell'ambito di inclusione (il metodo Method). Non possiamo quindi dichiarare LocalFunction con il modificatore static.
Nel codice seguente invece, la funzione locale RectangleArea può essere statica perché non accede ad alcuna variabile nell'ambito di inclusione:

double RectangleAreaCalculation()
{
    double x = 10;
    double y = 3;
    return RectangleArea(x, y);

    static double RectangleArea(double length, double width) =>
    length + width;
}

Flussi asincroni

Facciamo subito un semplice esempio di come fino ad ora potevamo gestire un flusso di dati proveniente da un iterator:

public async Task<List<int>> GenerateSequence()
{
    List<int> list = new List<int>() ;
    for (int i = 0; i < 20; i++)
    {
        await Task.Delay(100);
        list.Add(i);
    }
    return list;
}

È chiaro che, con questo approccio, se volessimo stampare gli elementi della lista nella console, dovremmo aspettare di avere a disposizione la lista completa per poi iterare su ogni elemento:

public async void PrintSequence()
{
    List<int> list = await GenerateSequence();
    foreach (int num in list)
    {
        Console.WriteLine(num);
    }
}

Con C# 8 è stato introdotto il supporto ai flussi asincroni che portano il pattern async/await, di cui ho parlato nel mio precedente articolo, a gestire gli iterator. In pratica, i flussi asincroni sono metodi async che ritornano un oggetto di tipo IAsyncEnumerable<T> e che hanno un return espresso con uno yeld (yeld return) per restituire gli elementi successivi nel flusso asincrono appena sono disponibili:

public async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
{
    for (int i = 0; i > 20; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

In definitiva, possiamo usare await per attendere una chiamata asincrona e rendere a sua volta asincrona la funzione GenerateSequence(). La differenza importante è che la funzione non restituisce un Task<IEnumerable<int>>, ma un IAsyncEnumerable<int>
Questa interfaccia contiene membri asincroni per scorrere lo stream. Non è infatti l'oggetto IEnumerable che deve essere restituito in asincrono, ma lo stream degli elementi.
Per utilizzare un flusso asincrono, bisogna aggiungere la parola chiave await prima del foreach quando si enumerano gli elementi del flusso:

await foreach (var number in GenerateSequence())
{
    Console.WriteLine(number);
}

In questo caso, man mano che avremo a disposizione un elemento, verrà “stampato” a console, senza attendere di avere a disposizione tutta la lista.

Diamo uno sguardo alla definizione dell’interfaccia IAsyncEnumerator:

public interface IAsyncEnumerator<out T> : IAsyncDisposable
{
    T Current { get; }

    ValueTask<bool> MoveNextAsync();
}

Potete notare la presenza dell'interfaccia IAsyncDisposable che ci permette di sfogliare e di effettuare il dispose di tutte le risorse, il tutto completamente in maniera asincrona.

Struct ref Disposable

Una struct dichiarata con il modificatore ref non può implementare nessuna interfaccia e quindi non può implementare l’interfaccia IDisposable.
Ma da ora possiamo abilitare una ref struct per l'eliminazione se aggiungiamo un metodo void Dispose pubblico al suo interno. Questo metodo verrà automaticamente consumato dall'istruzione using:

class Program
{
   static void Main(string[] args)
   {
      using (var message = new Message())
      {
       Console.WriteLine("This message will self-destruct!");
      }
   }
}

ref struct Message : IDisposable
{
   public void Dispose()
   {
   }
}

Possiamo applicare questa funzionalità anche alle dichiarazioni readonly ref struct.

Unmanaged constructed types

In C# 7.3 e versioni precedenti, un constructed type (un tipo che include almeno un argomento di tipo) non può essere un tipo non gestito (unmanaged type).
Con C# 8, un constructed type è unmanaged se contiene solo campi di tipi non gestiti.

Ad esempio, dato il seguente tipo di Person<T> generico:

public struct Person<T>
{
    public T Age;
    public T NumberOfSons;
}

Il tipo di Person<int> è un tipo non gestito in C# 8 (perché contiene solo proprietà di tipo int: int è un tipo unmanaged). Come per qualsiasi tipo non gestito, è possibile creare un puntatore ad una variabile di questo tipo o allocare un blocco di memoria nello stack per le istanze di questo tipo:

Span<Person<int>> statisticalResearch = stackalloc[ ]
{
    new Person<int> { Age = 30, NumberOfSons = 1 },
    new Person<int> { Age = 45, NumberOfSons = 3 },
    new Person<int> { Age = 36, NumberOfSons = 0 }
};

Membri di sola lettura

È possibile applicare il modificatore readonly a qualsiasi membro di una struct, indicando di fatto che questo non modificherà il suo stato. Rispetto all’applicazione del modificatore all’intera struttura, questa opzione ci consente un controllo più granulare.
All’interno di un metodo marcato come readonly non è possibile modificare il contenuto di un campo perchè questo genererebbe un errore di compilazione.

public struct Employee
{
    public string FullName { get; set; }
    public double DaysWorked { get; set; }
    public double DailyWages{ get; set; }
    public double Salary => DailyWages * DaysWorked;

    public readonly override string ToString() 
    {
        // Compiling error
   	DaysWorked = 30;

        // Salary is not readonly and generates a warning
  	     return $"Salary for employee {Surname} {Name} is: {Salary}$";
        }
}

Il metodo ToString() non modifica lo stato. Si può indicare questa condizione aggiungendo il modificatore readonly alla dichiarazione di ToString().
La modifica precedente genera un avviso del compilatore, perché ToString accede alla proprietà Salary, che non è contrassegnata readonly:

warning CS8656: Call to non-readonly member 'Employee.Salary.get' from a 'readonly' member results in an implicit copy of 'this'

Il compilatore genera un avviso quando deve creare una copia difensiva. La proprietà Salary non modifica lo stato, quindi è possibile correggere l'avviso aggiungendo il modificatore readonly alla dichiarazione:

public readonly double Salary => DailyWages * DaysWorked;

Prestiamo attenzione: il modificatore readonly è necessario, il compilatore non presuppone che le funzioni di accesso get non modifichino lo stato; è necessario dichiarare readonly in modo esplicito. Sono un’eccezione le proprietà implementate automaticamente
Il compilatore infatti considererà tutti i Getter implementati automaticamente come readonly.

Interfacce

Questa è una novità che ho trovato molto interessante: possiamo ora aggiungere membri alle interfacce e fornire un'implementazione per tali membri. Questa feature supporta anche l'interoperabilità di C# con API destinate ad Android (Java) o Swift, che dispongono già di implementazioni simili. Questa funzionalità del linguaggio è una di quelle che necessita di uno specifico runtime per poter essere supportate e consente agli autori di API di aggiungere metodi ad un'interfaccia senza compromettere l'origine o la compatibilità con le implementazioni esistenti di tale interfaccia. Le implementazioni già esistenti ereditano l'implementazione predefinita.

Supponiamo di avere un’interfaccia come la seguente:

public interface IPrintable
{
    void Print();
}

E una classe che la implementa:

public class EBook : IPrintable
{
    public void Print()
    {
        Console.WriteLine("Print");
    }
}

Se avessimo bisogno di aggiungere una nuova funzionalità, in genere aggiungeremo all’interfaccia un nuovo membro, per esempio il metodo Download:

public interface IPrintable
{
    void Print();
    void Download(string path);
}

La classe EBook restituirebbe però un errore di compilazione simile a: ‘EBook’ does not implement interface member ‘IPrintable.Download(string)’, in quanto non implementa più correttamente l’interfaccia.
C# 8 introduce i default interface methods con i quali possiamo implementare un membro in modo predefinito direttamente nell’interfaccia:

public interface IPrintable
{
    int NumberOfPages { get; set; }
    double PricePerPage { get; set; }
    //Default Get
    double Price { get => NumberOfPages * PricePerPage; } 
    //Short default Get 
    //double Price => NumberOfPages * PricePerPage;
    void Print();
    void Download(string path);
    {
        Console.WriteLine($”Download in {path}”);
    }
}

La classe che implementa l'interfaccia è obbligata ad implementare, come di consueto, il metodo senza body, e può facoltativamente implementare e sovrascrivere i default methods forniti dall’ interfaccia.
I default method sono visibili solo se utilizziamo una variabile dell'interfaccia stessa e non sono visibili tramite la classe che la implementa. Al contrario, risultano visibili solo i membri implementati su di essa.
Le interfacce possono ora includere membri statici (tra cui campi e metodi) e virtual (ma in questo caso una classe che implementa l’interfaccia non può eseguire l’override di un metodo virtual, solo un’interfaccia può farlo).
C# 8 ci permette di specificare anche dei modificatori di accesso per i membri di un’interfaccia, che nelle versioni precedenti erano implicitamente public. Per i membri dell'interfaccia è ora consentito qualsiasi modificatore.

public interface IPrintable
{
    void Print();
    private static string copyright = "© 2005-2011 Blexin s.r.l. All Rights Reserved";
    public virtual void PrintCopyright()
    {        
        Console.WriteLine(copyright);
    }
}

Per il momento ci fermiamo qui, nel prossimo articolo vedremo cosa ci riserva ancora questa nuova versione di C#.

A presto!

Autore

c#

Servizi

Evolvi la tua azienda