in C#, Informatica

Novità in C# 10

Puntuale come ogni anno ecco il rilascio di C# 10, conteporaneamente al .NET 6. Come per le versioni precedenti non si tratta di veri e propri stravolgimenti, ma di migliorie.

Ecco di seguito alcune delle nuove features che sono state introdotte:

  • Namespace File scoped
  • Global using
  • Implicit Using
  • Extended property patterns
  • Record structs
  • Constant interpolated strings
  • Seal overriden ToString() method on records
  • Static abstract members in interfaces
  • Utilizzo di attributi nelle lamda expression
  • Automatically infer a “natural” type for a lambda
  • Explicitly specify the return type for a lambda

Namespace file scoped

Uno degli obiettivi dell’introduzione del namespace file scoped è quello di rendere più pulito e leggibile il codice. In un file C#, subito dopo l’inclusione degli using (che possiamo semplificare utilizzando il global using che vedremo in seguito), è presente la definizione del namespace. Subito dopo, inizia la definizione della classe. In pratica:

namespace MyWorkSpace
{
  public class MyClass
  {
    ...
  }
}

In C# 10 è possibile eliminare l’inclusione della classe all’interno del namespace, semplicemente dichiarando il namespace, preceduto dalla keyword namespace. In pratica:

namespace MyWorkSpace;

public class MyClass
{
  ...
}

Global using

In ogni file C#, all’inizio, sono inseriti tramite using a tutte le reference che dovranno essere utilizzate. A volte, però, l’inserimento delle using può essere ridondante, perchè buona parte di queste inclusioni viene ripetuto (ad esempio se si sta lavorando con ASP.NET).

Prendiamo, come esempio, il seguente codice:

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

dove sono riportati (solo alcuni) degli using presenti all’interno di un singolo file C#.

In C# 10 è possibile utilizzare la parola chiave global. Utilizzando global using è possibile definire using per l’intero progetto. E’ consigliato utilizzare un file separato chiamato ad esempio using.cs all’interno del quale includere tutti i global using. L’introduzione della parola chiave global consente di semplificare ogni singolo file C# concentrando le inclusioni globali all’interno di un unico file. Spesso questa funzionalità viene riportata come “elimination of vertical waste“.

Ovviamente è possibile definire global anche delle inclusioni static, come ad esempio:

global using static System.Console;

Implicit Using

Insieme all’introduzione del global static è stata introdotta una nuova feature chiamata implicit usings, che consente di inserire in automatico gli using che sono maggiormenti utilizzati in base al tipo di progetto. E’ possibile attivarla all’interno del file del progetto mediante:

 <ImplicitUsings>enable</ImplicitUsings>

Per i progetti nuovi, non è necessario abilitare questa opzione perchè è già abilitata di default.

All’interno del file di progetto è anche possibile effettuare l’inclusione/esclusione specifici namespace usando:

<ItemGroup>
  <Using Remove="System.Threading " />
  <Using Include="System.IO.Pipes" />
</ItemGroup>

Nell’esempio precedente viene rimosso il namespace System.Threading e viene incluso System.IO.Pipes.

Extended property patterns

A volte abbiamo a che fare con delle proprietà annidate, in pratica delle proprietà di proprietà. Nelle versioni precedenti di C# era possibile gestire tramite pattern matching questo tipo di proprietà, ma il codice risultava piuttosto articolato (per non dire brutto).

Prendiamo ad esempio codice, scritto in C#9, di una switch definita tramite pattern matching:

if (e is MethodCallExpression { Method: { Name: "MethodName" } })

il codice risulta piuttosto leggibile, anche se sarebbe molto piu comodo introdurre un . all’interno per accedere alla proprietà name di method. In C# 10, è ora possibile scrivere il codice precedente come:

if (e is MethodCallExpression { Method.Name: "MethodName" })

rendendo di fatto il codice ancora più leggibile e soprattutto intuitivo.

Record structs

I record sono stati introdotti all’interno di C# 9 e sono principalmente utilizzati per la memorizzazione di dati immutabili. La parola chiave record viene solitamente utilizzata i tipi a riferimento che forniscono funzionalità per l’incapsulamento dei dati.

In C# 9 i record potevano utilizzare record soltanto con delle classi:

public record MyClass(string FirstName, string LastName);

In C# 10 è possibile utilizzare record anche con dei tipi struct:

public readonly record struct MyClass(double X, double Y, double Z);

Ad esempio, possiamo definire un record struct utilizzando:

public readonly record struct Framework(string? Name, int Version, CodingLanguage? CodingLanguage);

ed inizializzarlo tramite:

 var framework = new Framework
        {
            Name = ".NET",
            Version = 6,
            CodingLanguage = new CodingLanguage { Name = "C#" }
        };

E’ da notare che non è necessario definire le proprietà come readonly record struct, passando semplicemente il nome della proprietà nel costruttore, verrà “bindato” al valore della proprietà all’interno della struct.

Constant interpolated strings

In C# è possibile utilizzare le constant interpolated strings per aggiungere un oggetto ad una stringa. Ad esempio

var languageReleasePrefix = "C# 10";
var languageRelease = $"{languageReleasePrefix} to be released in November 2021.";

nella seconda riga viene inserita la frase “C# 10” all’interno della stringa languageRelease. Nel caso in cui, però, la stringa languageRelease fosse dichiarata come const, il compilatore genererebbe un errore.

In C# 10 è ora possibile definire una stringa interpolata come costante:

const string languageReleasePrefix = "C# 10";
const string languageRelease = $"{languageReleasePrefix} to be released in November 2021.";

Seal overriden ToString() method on records

In C# 10, come avviene normalmente per le classi, viene impedito di fare l’override del metodo toString(). Prendiamo ad esempio il record Person così definito:

public record Person
    {
        public string FirstName {  get; init; }

        public string LastName { get; init; }

        public sealed override string ToString()
        {
            return $"My name is {FirstName} {LastName}";
        }
    }

Nell’esempio viene effettuato l’override del metodo ToString(), ma utilizzando la keyword sealed non è possibile effettuare l’override dai record derivati. Supponendo di voler creare un record Student che eredita dal record Person e che cerca di effettuare l’override di ToString():

 public record Student : Person
    {
        public override string ToString() sealed
        {
            return $"My name is {FirstName} {LastName}";
        }
    }

verrà generato un errore in fase di compilazione, perchè su tale metodo non è possibile effettuare l’override (essendo dichiarato come sealed nel record base).

Static abstract members in interfaces

Molti tipi hanno metodi statici come ParseCreate oppure operatori come + o - (che in realtà sono metodi statici). Al momento però non è possibile utilizzarli all’interno di codice generico, perchè non possono essere definiti all’interno di interfacce o nelle classi base.

L’obiettivo di questa nuova funzionalità, introdotta in C#10) è poter astrarre questi membri nelle interfacce in modo che possano essere utilizzati nel codice generico.

Supponiamo di voler scrivere un metodo che esegue la somma di due numeri. E’ necessario definire il tipo di numero su cui stiamo lavorando (int, double, ecc…). Non è possibile scrivere un metodo generico che funzioni con tutti i tipi di numero, perchè non abbiamo modo di esprimere il fatto che l’argomento di tipo generico deve avere un operatore di addizione. Questo fino al rilascio di C# 10. Infatti, in C# 10 è possibile definire l’interfaccia come:

public interface IAddable<T> where T : IAddable<T>
{
    static abstract T Zero { get; }
    static abstract T operator +(T t1, T t2);
}

Nell’ipotesi che l’interfaccia IAddable sia implementata da tutti i tipi numerici, possiamo pensare di scrivere un metodo come il seguente, per poter eseguire la somma di qualsiasi tipo di numero:

public static T Sum<T>(params T[] numbers) where T : IAddable<T>
{
    T sum = T.Zero;
    foreach (T number in numbers)
    {
        sum += number;
    }
    return sum;
}

Utilizzo di attributi nelle lambda expression

Prima di C# 10 non era possibile utilizzare attributi all’interno delle lambda expression. Da C# 10 ora è possibile. Ad esempio, possiamo scrivere del codice del tipo:

Func<int, bool> isEven = [Pure] n => n % 2 == 0;

Automatically infer a “natural” type for a lambda

Storicamente le lambda expression non avevano un tipo intrinseco ritornato. Il tipo veniva dedotto in base al tipo del valore assegnato. Ad esempio:

Func<int, bool> isEven = (int n) => n % 2 == 0;

isEven veniva dedotto da Func<int, bool>.

In C# 10 è possibile utilizzare var da associare al valore ritornato: sarà il compilatore a dedurre un tipo delegato “naturale”. Diventa quindi possibile scrivere:

var isEven = (int n) => n % 2 == 0;

Il tipo dedotto sarà sempre una variante di System.Func<…> oppure delle System.Action<…> a seconda dei parametri e dei tipi ritornati.

Ovviamente il compilatore sarà in grado di dedurre il tipo solo nel caso gli verranno fornite tutte le informazioni necessarie, soprattutto i tipi dei parametri.

Explicitly specify the return type for a lambda

C# 10 permette di specificare un tipo di ritorno per le lambda expression. In precedenza una lambda doveva essere assegnata ad un tipo delegato specifico, ma introducendo la “natural inference” potrebbe essere necessario specificare il tipo restituito (magari diverso da quello dedotto “automaticamente”).

var oneTwoThreeArray = () => new[]{1, 2, 3}; // il tipo sarebbe Func<int[]>
var oneTwoThreeList = IList<int> () => new[]{1, 2, 3}; // il tipo è stato modificato in Func<IList<int>>