Articoli

Nullable! Questo sconosciuto.

da

Questo articolo è dedicato a coloro che, affascinati da Blazor, ma provenenti da altri framework/librerie (come Angular, React e Vue) hanno lasciato i vari linguaggi JavaScript o TypeScript per usare il tanto amato C#.

Dalla versione 6 del .Net Core, nei suoi template Microsoft ha aggiunto l’impostazione di base per quanto riguarda i tipi nullable.
Di seguito le prime righe di un progetto Blazor dove appunto possiamo notare l’impostazione “enable” dei nullable (linea 4).

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>

Un piccolo ripasso per chi si è affacciato da poco a Blazor ed anche al C#. Ricordiamo che i tipi di dati in C# sono divisi in due categorie: Value Type e Reference Type. Prima della versione 6 del .Net Core tutti i Reference Type erano di default “nullable”, cioè che potevano contenere “null”. Anche il tipo String, trattato come Value Type dal C#, poteva contenere null. Dalla versione 6, come si diceva, è necessario dichiarare tramite codice se un oggetto può contenere il valore null.

Nullable<int> value = null;
Nullable<string> text = null;
Nullable<MyClass> myClass = null;

o più semplicemente

int? value = null;
string? text = null;
MyClass? myClass = null;

La stessa cosa vale per le proprietà, se non sono nullable dobbiamo fornire un valore di partenza

public string Nome { get; set; } = string.Empty;
public string? CodiceFiscale { get; set; } // valore di default = null

Questa nuova impostazione sui nullable, non solo ci impone di dichiarare a priori se quel dato può contenere null, ma attiva l’editor di Visual Studio producendo tutti i warning appena c’è qualche pericolo nel codice.

In cosa consiste il “pericolo”? Quello appunto di usare una variabile come se contenesse un valore quando invece potrebbe contenere null. In breve questa impostazione dovrebbe aiutarci, in fase di creazione del codice, a non cadere nel NullReferenceException in fase di esecuzione. Il warning ci salva da un errore di distrazione, e quindi va “spento” usando un codice adeguato. Questo tipo di codice può essere noioso e molti si chiedono se, per evitare quella eccezione, vale la pena dettagliare tutto il codice con l’operatore null-forgiving già presente dalla versione 8 del C#.

Sappiamo che un componente Blazor può ottenere oggetti (dati o istanze di classi da usare) dall’esterno in vari modi; ad esempio come parametri o come una proprietà proveniente dalla Dependency Injection (attributo [Inject]). Il parametro è più semplice da gestire perché siamo a conoscenza di cosa desideriamo che il contenitore del nostro componente ci invii, mentre, ad esempio, un servizio iniettato sappiamo solo come usarlo ma non sappiamo cos’è, infatti in genere lo invochiamo tramite una interfaccia.

[Inject]
private IUserService UserService { get; set; }

In questo caso non siamo in grado di fornire un valore di default a questa proprietà. L’unica soluzione per “spegnere” il warning è quello di impostarla come nullable.

[Inject]
private IUserService? UserService { get; set; }

Warning soddisfatto. Il problema si ripropone quando andremo ad usare quel servizio. Il precompilatore non accetterà più un codice come questo

var currentUser = UserService.GetCurrentUser();

Giustamente il precompilatore nota che stiamo usando un oggetto che potrebbe essere null e quindi non essere in grado di usare il suo metodo GetCurrentUser.

Non essendo noi direttamente a fornire un’istanza di quel servizio siamo costretti ad usarlo ugualmente e a dire al precompilatore che siamo certi che in quel punto userService non può essere null. Qui entra in gioco l’operatore null-forgiving, ovvero far seguire da un punto esclamativo l’oggetto che vogliamo usare.

var currentUser = UserService!.GetCurrentService();

Curiosità: se invece di impostare la proprietà nella parte code di un componente, o nella parte classe base o partial che accompagna il componente, la impostiamo nella parte razor con questa riga

@inject IUserService UserService;

notiamo che UserService non è nullable nonostante la mancanza di un valore di inizializzazione. Quindi potrebbe essere una buona soluzione usare richieste di Dependency Injection in modalità Razor.

Bene, abbiamo visto come tenere a bada il precompilatore. Prendiamo ora come esempio il codice seguente:

class Person {
     public string NomeCompleto { get; set; } = string.Empty;
     public Recapito? Recapito{ get; set; }
}
class Recapito {
     public string Indirizzo { get; set; } = string.Empty;
     public string CAP { get; set; } = string.Empty;
     public string Comune { get; set; } = string.Empty;
}

Ci viene fornita come parametro una List<Person> e vogliamo mostrare in una tabella il nome e il comune di provenienza. Possiamo decidere se il parametro può essere null. Lo facciamo per complicarci la vita perché avremmo pure potuto fornirgli una valore di default come una List vuota.

[Parameter]
public List<Person>? Items { get; set; }
@if(Items != null) {
     <table>
     @foreach(var item in Items) {
          <tr>
               <td>@item.NomeCompleto</td>
               <td>@item.Recapito?.Comune</td>
          </tr>
     }
     </table>
}

Nella riga del foreach possiamo usare tranquillamente Items perché si trova all’interno di un if che ha già provveduto a verificare che il contenuto di Items non sia null. Cosa diversa invece per il contenuto di Recapito. In questo caso non sappiamo se il dato è presente ma vogliamo comunque avere un risultato visto che anche il null ci va bene, per cui facciamo seguire alla proprietà nullable l’operatore Null-conditional operator.

Otteniamo in questo modo null se la proprietà lo è, oppure il valore di quest’ultima. Otterremo così una lista di persone con il comune, se il recapito è valorizzato, oppure una cella vuota. Se proprio non ci garba un risultato null allora possiamo invocare l’operatore null-coalescing (??) per impostare il valore in alternativa.

<td>@(item.Recapito?.Comune ?? "Non presente")</td>

Attenzione! Il warning viene spento anche usando l’operatore null-forgiving, dicendo al precompilatore che siamo certi che Recapito non sia null (grosso errore), mentre proprio in caso di null non possiamo più accedere alla proprietà Comune generando una NullReferenceException.

Quando trattiamo con modelli di un database è facile trovare proprietà nullable. Ad esempio nel codice sopra il Recapito è nullable perché non sempre è necessario farci restituire tutti i dati da una query, anche se per nostra impostazione il dato Recapito è obbligatorio (caso precedente). Ammettiamo invece di richiedere tutti i dati dal database, recapito compreso. Ecco che in questo caso siamo certi a priori che Recapito non può essere null (altrimenti il problema è altrove) e quindi possiamo usare anche qui l’operatore null-forgiving (!.)

<td>@item.Recapito!.Comune</td>

Complichiamo le cose proponendo un codice improbabile. Ammettiamo di avere un palazzo con 10 piani e ogni piano può essere affittato ad una persona. Questo vuol dire che abbiamo un array di 10 elementi che può contenere un Person nullable (piano senza inquilino); e per concludere, lo stesso array può essere null.

Person?[]? piani = null; // Più leggibile usando List<Person?>? piani = null
// elaborazione dell'array
String? nome = piani![3]?.NomeCompleto;

Lo scopo è quello di sapere il nome dell’eventuale occupante del quarto piano dando per certo che l’array contiene 10 elementi (!.) ma non sappiamo se è occupato (?.). Per questo motivo anche la variabile nome è di tipo nullable, appunto per “gestire” l’eventuale null prodotto dall’operatore (?.)

Riassumendo, se intendiamo proseguire nello scrivere codice che osservi l’impostazione nullable a enable basta seguire due regole:

  • Se stiamo trattando un oggetto nullable da cui ci aspettiamo obbligatoriamente la presenza di un’istanza, verifichiamo con un if che non sia null (come abbiamo visto questo spegne tutti i warning al suo interno). Oppure se siamo certi che non sia null usiamo l’operatore null-forgiving (!.)
  • Nel caso invece che anche un risultato null sia gestito allora possiamo usare l’operatore Null-conditional operator (?.)

Spero di avervi aiutato a chiarire l’uso dei nullable!

Alla prossima

Scritto da: