
Nel precedente articolo, abbiamo visto la prima parte delle novità introdotte dalla versione di C# 8, vediamo ora insieme cosa altro ci mette a disposizione questa nuova versione del linguaggio di casa Microsoft.
Nuova Switch Expression
Sono state rilasciate le nuove Switch Expression, che introducono un modo compatto per scrivere espressioni switch/case.
Vi mostro subito di cosa sto parlando con un esempio:
public static string GetWaterCondition(WaterCondition condition) =>
condition.Sea switch
{
Sea.Calm => "The sea is calm",
Sea.Rough => "The sea is rough",
Sea.VRough => "The sea is very rough",
_ => "Undefined"
};
Come potete vedere:
- La variabile precede la parola chiave switch.
- Gli elementi “case” e “:”vengono sostituiti con “=>”.
- Il caso default è sostituito con un underscore (o discard) ”_”.
- I corpi sono espressioni e non istruzioni.
Criteri per le proprietà
Grazie alla sintassi precedente possiamo sfruttare anche il property matching. Invece di specificare l’oggetto da valutare, possiamo indicare le proprietà stesse nelle condizioni:
public static string GetWaterCondition(WaterCondition condition) => condition switch
{
{Sea: Sea.Calm} => "The sea is calm",
{Sea: Sea.Rough} => "The sea is rough",
{Sea: Sea.VRough, Wind: Wind.Strong} => "There’s a storm!",
{Sea: Sea.VRough} => "The sea is very rough",
null => “Unknown”,
_ => "Undefined"
};
Come prima, valutiamo la proprietà Sea, ma abbiamo la possibilità di valutare anche la proprietà Wind e, oltre al valore di default (“_”), possiamo controllare anche se il valore è nullo. Dobbiamo prestare attenzione, però, all’ordine di valutazione: nello snippet di codice precedente dobbiamo necessariamente prima valutare la condizione più specifica con il mare molto agitato e il vento forte (There’s a storm!
), per poi passare a quella in cui il mare è molto agitato (The sea is very rough
).
Criteri per le tuple
Simile al property pattern visto poco fa, nel tuple pattern, possiamo invece utilizzare le tuple come argomento dello switch:
public string Location(string country, string city)
=> (country, city) switch
{
("IT", "Naples") => "Naples, Italy (Campania)”,
(_, "Naples") => "Italy (Campania),
United States (Utah),
United States (Texas),
United States (Florida),
United States (New York)",
("IT", _) => "Italy",
_ => "Not Found", //Short form for ( _, _) => "Not Found"
};
Criteri per la posizione
Alcuni oggetti implementano direttamente o indirettamente il deconstruct introdotto con C# 7, il quale , in pratica, “appiattisce” un oggetto in una lista di parametri.
Utilizzando insieme il deconstruct e la clausola when in un’istruzione switch possiamo sfruttare il positional pattern. In questo esempio, possiamo vedere quanto sia facile mappare un punto in un piano cartesiano per capire su quale quadrante è posizionato.
Supponiamo di avere un’implementazione di questo tipo:
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}
L’oggetto Point viene decostruito ed inserito in una tupla, viene poi utilizzata la clausola when per determinare in quale quadrante si trova il punto:
private string GetPointPosition(Point point) => point switch
{
(0, 0) => "origin",
var (x, y) when x > 0 && y > 0 => "dx-up",
var (x, y) when x < 0 && y > 0 => "sx-up",
var (x, y) when x < 0 && y < 0 => "sx-down",
var (x, y) when x > 0 && y < 0 => "dx-down",
var (_, _) => "on border",
_ => "unknown"
};
Indici ed intervalli
Sono stati introdotti due nuovi tipi:
- System.Index che rappresenta un indice in una sequenza.
- System.Range che rappresenta un intervallo secondario di una sequenza.
Sono stati introdotti inoltre due nuovi operatori:
- L’operatore “^” che specifica che un indice parte dalla fine della sequenza.
- L’operatore di intervallo “..” che specifica l’inizio e la fine di un intervallo come operandi.
Vediamo di capire meglio di cosa stiamo parlando prendendo in considerazione una matrice daysOfWeek:
var daysOfWeek = new string[]
{
// Index from start Index from end
"Monday", // 0 ^7
"Tuesday", // 1 ^6
"Wednesday", // 2 ^5
"Thursday", // 3 ^4
"Friday", // 4 ^3
"Saturday", // 5 ^2
"Sunday" // 6 ^1
// 7 (or daysOfWeek.Length) ^0
};
L’indice [0] è uguale all’indice [^7] in questo caso “Monday”.
L’indice [7] è uguale all’indice [^0] ossia daysOfWeek[daysOfWeek.Length] e quindi genererebbero un’eccezione. Per qualsiasi numero n, l’indice ^n è uguale a daysOfWeek.Length – n.
Un intervallo invece (“..”) specifica inizio e fine di un intervallo. L’inizio dell’intervallo è inclusivo, ma la fine non è inclusa nell’intervallo.
Abbiamo, quindi, diverse opzioni per ottenere intervalli specifici:
var allDays = daysOfWeek[..]; //Contains all the days of week
var startOfTheWeek = daysOfWeek[..2]; // "Monday", "Tuesday"
var weekend = daysOfWeek[5..]; // "Saturday", "Sunday"
var someDays = daysOfWeek[^5..^3]; // "Wednesday", "Thursday"
I due nuovi tipi, Index e Range, possono essere creati tramite i rispettivi costruttori, oppure tramite uno shortcut di C# 8, come nel seguente esempio:
// daysOfWeek[0]
Index monday = 0;
// "Wednesday", "Thursday"
Range someDays = 2..4;
L’intervallo può quindi essere utilizzato come variabile:
var days = daysOfWeek[someDays];
Tipi riferimento nullable
Come sappiamo, un oggetto può essere di tipo valore, appartenente quindi ai tipi primitivi come numeri, struct ecc.
Risiedono in una zona di memoria detta stack e hanno sempre un valore, anche se questo è una costante predefinita. I tipi di riferimento, invece, come tutte le classi e le stringhe, vanno istanziati nel managed heap e il loro riferimento mantenuto tramite una variabile. Quest’ultima, però, può assumere un valore nullo. Avere una variabile a null vuol dire non puntare a nessuno oggetto.
Una delle funzionalità aggiunte in C# 8 che ha avuto più risalto è stata l’introduzione del nuovo flag Nullable che aggiunto direttamente nel file con estensione .csproj, può abilitare o disabilitare il contesto di annotazione nullable e il contesto di avviso nullable. Le opzioni valide sono le seguenti:
- enable – Il contesto dell’annotazione nullable e il contesto dell’avviso nullable sono abilitati: le variabili di un tipo riferimento sono non nullable. Tutti gli avvisi relativi al supporto dei valori Null sono abilitati.
- warnings – Il contesto dell’annotazione nullable è disabilitato, mentre il contesto dell’avviso nullable è abilitato: le variabili di un tipo riferimento sono indipendenti dai valori. Tutti gli avvisi relativi al supporto dei valori null sono abilitati.
- annotations – Il contesto dell’annotazione nullable è abilitato, mentre il contesto dell’avviso nullable è disabilitato: le variabili di un tipo di riferimento non ammettono valori null. Tutti gli avvisi relativi al supporto dei valori null sono disabilitati.
- disable – Il contesto dell’annotazione nullable e il contesto dell’avviso nullable sono disabilitati: le variabili di un tipo riferimento sono indipendenti dai valori, come nelle versioni precedenti di C#. Tutti gli avvisi relativi al supporto dei valori null sono disabilitati.
<Nullable>enable</Nullable>
Di fatto stiamo dicendo che il compilatore deve controllare come utilizziamo e come assegniamo qualsiasi tipo di riferimento.
All’interno di un contesto delle annotazioni nullable, qualsiasi variabile di un tipo riferimento viene considerata come un tipo riferimento non nullable. Se si vuole indicare che una variabile può essere null, è necessario aggiungere “?” al nome del tipo per dichiarare la variabile come un tipo riferimento nullable
string? name;
Per i tipi riferimento non nullable, il compilatore usa l’analisi di flusso per garantire che le variabili locali vengano inizializzate su un valore diverso da null al momento della dichiarazione. I campi devono essere inizializzati durante la costruzione. Una variabile deve essere impostata da uno dei costruttori disponibili o da un inizializzatore ad un valore diverso da null. Inoltre, non è possibile assegnare ai tipi riferimento un valore che potrebbe essere null.
I tipi riferimento nullable non vengono invece controllati per verificare se vengano assegnati o inizializzati su null. Il compilatore però usa l’analisi di flusso per garantire che qualsiasi variabile di un tipo riferimento nullable venga controllata per i valori null prima dell’accesso o prima che venga assegnata a un tipo riferimento non nullable.
In sostanza, se la caratteristica viene attivata, gli oggetti non dichiarati esplicitamente come Nullable, se vengono settati o c’è la possibilità che possano assumere un valore null, genereranno un warning in compilazione.
È possibile dire al compilatore che siamo sicuri che una variabile (es: name), non possa essere nulla, e quindi evitarne il controllo, utilizzando l’operatore “!”.
name!.Length;
È anche possibile usare le direttive per impostare questi stessi contesti ovunque nel progetto, non solo per singolo file, ma anche in qualunque parte all’interno di una classe:
#nullable enable
: imposta il contesto di annotazione nullable e il contesto di avviso nullable su enabled.#nullable disable
: imposta il contesto di annotazione nullable e il contesto di avviso nullable su disabled.#nullable restore
: ripristina il contesto di annotazione nullable e il contesto di avviso nullable nelle impostazioni del progetto.#nullable disable warnings
: imposta il contesto di avviso nullable su disabled.#nullable enable warnings
: imposta il contesto di avviso nullable su Enabled.#nullable restore warnings
: ripristina il contesto di avviso nullable nelle impostazioni del progetto.#nullable disable annotations
: imposta il contesto di annotazione nullable su disabled.#nullable enable annotations
: imposta il contesto di annotazione nullable su enabled.#nullable restore annotations
: ripristina il contesto di avviso delle annotazioni nelle impostazioni del progetto.
Per impostazione predefinita, i contesti di annotazione e avviso nullable sono disabilitati. Ciò significa che il codice esistente viene compilato senza modifiche e senza generare nuovi avvisi.
Le regole per il controllo sono davvero tante e non voglio dilungarmi oltre. Se siete curiosi di approfondire l’argomento, controllate la documentazione ufficiale.
Null-coalescing assignment
È stato introdotto anche l’operatore di assegnazione di unione null “??=”.
È possibile usare l’operatore “??=” per assegnare il valore dell’operando destro di una assegnazione, all’operando sinistro solo se l’operando sinistro restituisce null.
List<int> numbers = null;
numbers ??= new List<int>();
Stackalloc nelle espressioni annidate
L’operatore stackalloc alloca un blocco di memoria nello stack. Un blocco di memoria allocato nello stack, creato durante l’esecuzione del metodo, viene automaticamente eliminato alla restituzione del metodo.
Con C# 8, se il risultato di un’espressione stackalloc è di tipo System.Span<T> o System.ReadOnlySpan<T>, è possibile usare l’espressione stackalloc in altre espressioni:
Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var indx = numbers.IndexOfAny(stackalloc[] { 2, 4, 6 ,8 });
Console.WriteLine(indx); // output: 1
Miglioramento delle stringhe verbatim interpolate
L’ordine dei token per le stringhe interpolate, finora prevedeva necessariamente che il token “$” precedesse il token “@”.
Adesso l’ordine dei due token è indifferente.
Entrambe le stringhe:
$@"..."
@$"..."
sono stringhe interpolate valide.
Con questo concludiamo il nostro percorso alla scoperta delle novità di C#8. Le novità sono tante e c’è tanto da imparare, anche perché stanno già lavorando alla versione 9….
Al prossimo articolo!